Some checks failed
thehideout/TheHideout-API/pipeline/head There was a failure building this commit
366 lines
12 KiB
JavaScript
366 lines
12 KiB
JavaScript
import { extractRichText, transformContent } from "../utils/notion.js";
|
|
import { addToNotionDataSource, updateNotionPage } from "../utils/notion.js";
|
|
import {
|
|
fetchSmoobuProperties,
|
|
fetchSmoobuPropertyDetails,
|
|
} from "../utils/smoobu.js";
|
|
import _ from "lodash";
|
|
import diff from "microdiff";
|
|
|
|
export async function getProperties(env, cached = false) {
|
|
try {
|
|
// If cached=true, try to get from Cloudflare Cache API first
|
|
if (cached) {
|
|
try {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(
|
|
`https://${env.CACHE_URL || "cache"}/properties`
|
|
);
|
|
const cachedResponse = await cache.match(cacheKey);
|
|
|
|
if (cachedResponse) {
|
|
const cachedData = await cachedResponse.json();
|
|
console.log("Properties retrieved from Cloudflare cache");
|
|
return cachedData;
|
|
}
|
|
} catch (cacheError) {
|
|
console.log("Cache miss or error, falling back to KV:", cacheError);
|
|
}
|
|
}
|
|
|
|
// Fall back to KV storage
|
|
const kvData = await env.CONTENT_KV.get(env.PROPERTIES_KEY, {
|
|
type: "json",
|
|
});
|
|
|
|
// If we were trying to use cache but it failed, store the KV data in cache
|
|
if (cached && kvData) {
|
|
try {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(
|
|
`https://${env.CACHE_URL || "cache"}/properties`
|
|
);
|
|
|
|
const response = new Response(JSON.stringify(kvData), {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Cache-Control": "max-age=60", // 1 minute TTL
|
|
ETag: `"properties-${Date.now()}"`, // Add ETag for cache validation
|
|
},
|
|
});
|
|
|
|
await cache.put(cacheKey, response);
|
|
console.log("Properties stored in Cloudflare cache after KV fallback");
|
|
} catch (cacheError) {
|
|
console.warn("Error storing in cache after KV fallback:", cacheError);
|
|
}
|
|
}
|
|
|
|
return kvData || [];
|
|
} catch (error) {
|
|
console.log("Error fetching properties cache:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function storeProperties(env, properties) {
|
|
try {
|
|
// Always store in KV first
|
|
await env.CONTENT_KV.put(env.PROPERTIES_KEY, JSON.stringify(properties));
|
|
console.log("Properties stored in KV.");
|
|
|
|
// Purge the Cloudflare Cache API
|
|
try {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(
|
|
`https://${env.CACHE_URL || "cache"}/properties`
|
|
);
|
|
|
|
await cache.delete(cacheKey);
|
|
console.log("Properties cache purged successfully.");
|
|
} catch (cacheError) {
|
|
console.warn(
|
|
"Error purging Cloudflare cache, but KV was updated successfully:",
|
|
cacheError
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error storing properties cache:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function transformSmoobuProperty(env, property) {
|
|
console.log("Transforming property:", property.id);
|
|
let street = property.location?.street || "";
|
|
// Remove leading number and whitespace if present
|
|
street = street.replace(/^\d+\s*/, "");
|
|
// Split by commas and join with newlines
|
|
street = street
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.join("\n");
|
|
console.log(
|
|
"Transformed property:",
|
|
property.amenities.map((feature) => feature.replace(/,/g, ";")) || []
|
|
);
|
|
return {
|
|
smoobuId: property.id,
|
|
name: property.name || "Unknown Property",
|
|
maxOccupancy: property.rooms?.maxOccupancy || 0,
|
|
timezone: property.timeZone || "UTC",
|
|
bedrooms: property.rooms?.bedrooms || 0,
|
|
bathrooms: property.rooms?.bathrooms || 0,
|
|
doubleBeds: property.rooms?.doubleBeds || 0,
|
|
singleBeds: property.rooms?.singleBeds || 0,
|
|
sofaBeds: property.rooms?.sofaBeds || 0,
|
|
sofas: property.rooms?.couches || 0,
|
|
childBeds: property.rooms?.childBeds || 0,
|
|
queenSizeBeds: property.rooms?.queenSizeBeds || 0,
|
|
kingSizeBeds: property.rooms?.kingSizeBeds || 0,
|
|
address: property.location
|
|
? `${street || ""}\n${property.location.city || ""}\n${
|
|
property.location.zip || ""
|
|
}`.trim()
|
|
: "Unknown Address",
|
|
features:
|
|
property.amenities.map((feature) => feature.replace(/,/g, ";")) || [],
|
|
minPrice: property.price?.minimal ? parseFloat(property.price.minimal) : 0,
|
|
maxPrice: property.price?.maximal ? parseFloat(property.price.maximal) : 0,
|
|
};
|
|
}
|
|
|
|
export function diffProperties(newList, oldList) {
|
|
// Keys to check for changes (set to null to check all keys)
|
|
const keysToCheck = [
|
|
"name",
|
|
"maxOccupancy",
|
|
"bedrooms",
|
|
"bathrooms",
|
|
"address",
|
|
"features",
|
|
"minPrice",
|
|
"maxPrice",
|
|
"timezone",
|
|
"doubleBeds",
|
|
"singleBeds",
|
|
"sofaBeds",
|
|
"couches",
|
|
"childBeds",
|
|
"queenSizeBeds",
|
|
"kingSizeBeds",
|
|
];
|
|
|
|
// Helper: index by smoobuId using lodash
|
|
const oldById = _.keyBy(oldList || [], "smoobuId");
|
|
const newById = _.keyBy(newList || [], "smoobuId");
|
|
|
|
// toAdd: in newList but not in oldList
|
|
const toAdd = newList.filter((p) => !oldById[p.smoobuId]);
|
|
|
|
// toDelete: in oldList but not in newList
|
|
const toDelete = oldList.filter((p) => !newById[p.smoobuId]);
|
|
|
|
// toUpdate: in both, but with different content (using microdiff)
|
|
const toUpdate = newList.filter((p) => {
|
|
const old = oldById[p.smoobuId];
|
|
if (!old) return false;
|
|
|
|
// If syncName is false, exclude name from comparison
|
|
// If syncPrice is false, exclude prices from comparison
|
|
const { smoobuId, notionId, name, minPrice, maxPrice, ...restNew } = p;
|
|
const {
|
|
smoobuId: _,
|
|
notionId: __,
|
|
name: oldName,
|
|
minPrice: oldMinPrice,
|
|
maxPrice: oldMaxPrice,
|
|
...restOld
|
|
} = old;
|
|
|
|
// Prepare objects for comparison
|
|
let newObj, oldObj;
|
|
|
|
// Build objects conditionally based on sync flags
|
|
newObj = { ...restNew };
|
|
oldObj = { ...restOld };
|
|
|
|
if (old.syncName !== false) {
|
|
// Include name in comparison when syncName is true or undefined
|
|
newObj.name = name;
|
|
oldObj.name = oldName;
|
|
}
|
|
|
|
if (old.syncPrice !== false) {
|
|
// Include prices in comparison when syncPrice is true or undefined
|
|
newObj.minPrice = minPrice;
|
|
newObj.maxPrice = maxPrice;
|
|
oldObj.minPrice = oldMinPrice;
|
|
oldObj.maxPrice = oldMaxPrice;
|
|
}
|
|
|
|
// Use microdiff to get differences
|
|
const differences = diff(oldObj, newObj);
|
|
|
|
// If no keysToCheck specified, return true if any differences found
|
|
if (!keysToCheck) {
|
|
return differences.length > 0;
|
|
}
|
|
|
|
// Filter differences to only include keys we care about
|
|
const relevantDifferences = differences.filter((change) => {
|
|
// Check if the changed path starts with any of our keysToCheck
|
|
const path = change.path.join(".");
|
|
return keysToCheck.some((key) => path.startsWith(key));
|
|
});
|
|
|
|
return relevantDifferences.length > 0;
|
|
});
|
|
|
|
return { toAdd, toUpdate, toDelete };
|
|
}
|
|
|
|
// Transform Notion property data to desired format
|
|
export async function transformNotionProperty(env, notionProperty) {
|
|
const properties = notionProperty.properties;
|
|
|
|
const active = properties["Active"].checkbox || false;
|
|
|
|
// Extract image URLs (handle both external and file images)
|
|
const images = (properties["Images"]?.files || [])
|
|
.map((f) => f?.external?.url || f?.file?.url)
|
|
.filter(Boolean);
|
|
|
|
// Extract slug from formula
|
|
const slug = properties.Slug?.formula?.string || "unknown";
|
|
|
|
const maxPrice = properties["Max Price"]?.number || 0;
|
|
const minPrice = properties["Min Price"]?.number || 0;
|
|
const maxOccupancy = properties["Max Occupancy"]?.number || 0;
|
|
const bedrooms = properties["Bedrooms"]?.number || 0;
|
|
const bathrooms = properties["Bathrooms"]?.number || 0;
|
|
const doubleBeds = properties["Double Beds"]?.number || 0;
|
|
const singleBeds = properties["Single Beds"]?.number || 0;
|
|
const sofaBeds = properties["Sofa Beds"]?.number || 0;
|
|
const sofas = properties["Sofas"]?.number || 0;
|
|
const childBeds = properties["Child Beds"]?.number || 0;
|
|
const queenSizeBeds = properties["Queen Size Beds"]?.number || 0;
|
|
const kingSizeBeds = properties["King Size Beds"]?.number || 0;
|
|
const timezone = properties["Timezone"]?.select?.name || "UTC";
|
|
|
|
const bookingcomLink = properties["Booking.com Link"]?.url || "#";
|
|
const airbnbLink = properties["Airbnb Link"]?.url || "#";
|
|
|
|
const address = await extractRichText(
|
|
properties["Address"]?.rich_text || "unknown"
|
|
);
|
|
|
|
const smoobuId = properties["Smoobu ID"]?.number || null;
|
|
|
|
const syncName = properties["Sync Name"]?.checkbox || false;
|
|
const syncPrice = properties["Sync Price"]?.checkbox || false;
|
|
|
|
const features = properties["Features"]?.["multi_select"].map((feature) =>
|
|
feature.name.replace(/,/g, ";")
|
|
);
|
|
// Extract name from title
|
|
const name = properties.Name?.title?.[0]?.plain_text || "Untitled";
|
|
|
|
// Fetch and transform the actual page content from Notion blocks
|
|
const content = await transformContent(env, notionProperty.id);
|
|
|
|
return {
|
|
notionId: notionProperty.id,
|
|
name: name,
|
|
images: images,
|
|
slug: slug,
|
|
maxPrice: maxPrice,
|
|
minPrice: minPrice,
|
|
maxOccupancy: maxOccupancy,
|
|
bedrooms: bedrooms,
|
|
bathrooms: bathrooms,
|
|
doubleBeds: doubleBeds,
|
|
singleBeds: singleBeds,
|
|
sofaBeds: sofaBeds,
|
|
sofas: sofas,
|
|
childBeds: childBeds,
|
|
queenSizeBeds: queenSizeBeds,
|
|
kingSizeBeds: kingSizeBeds,
|
|
timezone: timezone,
|
|
address: address,
|
|
features: features,
|
|
bookingcomLink: bookingcomLink,
|
|
airbnbLink: airbnbLink,
|
|
content,
|
|
smoobuId,
|
|
syncName,
|
|
syncPrice,
|
|
active,
|
|
};
|
|
}
|
|
|
|
// --- Property Utilities moved from routes/properties.js ---
|
|
|
|
export async function addPropertyToNotion(env, property) {
|
|
console.log("Adding property to Notion:", property.smoobuId);
|
|
const details = await fetchSmoobuPropertyDetails(env, property.smoobuId);
|
|
const notionProps = {
|
|
Name: details.name,
|
|
"Max Occupancy": details.rooms.maxOccupancy,
|
|
Bedrooms: details.rooms.bedrooms,
|
|
Bathrooms: details.rooms.bathrooms,
|
|
"Double Beds": details.rooms.doubleBeds,
|
|
"Single Beds": details.rooms.singleBeds,
|
|
"Sofa Beds": details.rooms.sofaBeds,
|
|
Sofas: details.rooms.couches,
|
|
"Child Beds": details.rooms.childBeds,
|
|
"Queen Size Beds": details.rooms.queenSizeBeds,
|
|
"King Size Beds": details.rooms.kingSizeBeds,
|
|
Timezone: { select: { name: details.timeZone || "UTC" } },
|
|
Address: `${details.location.street}\n${details.location.city}\n${details.location.zip}`,
|
|
Features: details.amenities,
|
|
"Min Price": parseFloat(details.price.minimal),
|
|
"Max Price": parseFloat(details.price.maximal),
|
|
"Smoobu ID": details.id,
|
|
};
|
|
Object.keys(notionProps).forEach(
|
|
(k) => notionProps[k] === undefined && delete notionProps[k]
|
|
);
|
|
const res = await addToNotionDataSource(notionProps, env.PROPERTIES_DB);
|
|
return { ...property, notionId: res.id };
|
|
}
|
|
|
|
export async function updateNotionProperty(env, property, notionId) {
|
|
console.log("Updating property in Notion:", property.smoobuId);
|
|
// Use the transformed property data directly instead of fetching from Smoobu again
|
|
const notionProps = {
|
|
...(property.syncName ? { Name: property.name } : {}),
|
|
"Max Occupancy": property.maxOccupancy,
|
|
Bedrooms: property.bedrooms,
|
|
Bathrooms: property.bathrooms,
|
|
"Double Beds": property.doubleBeds,
|
|
"Single Beds": property.singleBeds,
|
|
"Sofa Beds": property.sofaBeds,
|
|
Sofas: property.sofas,
|
|
"Child Beds": property.childBeds,
|
|
"Queen Size Beds": property.queenSizeBeds,
|
|
"King Size Beds": property.kingSizeBeds,
|
|
Timezone: { select: { name: property.timezone || "UTC" } },
|
|
Address: property.address,
|
|
Features: property.features,
|
|
...(property.syncPrice
|
|
? { "Min Price": property.minPrice, "Max Price": property.maxPrice }
|
|
: {}),
|
|
};
|
|
Object.keys(notionProps).forEach(
|
|
(k) => notionProps[k] === undefined && delete notionProps[k]
|
|
);
|
|
await updateNotionPage(notionId, notionProps);
|
|
return { ...property, notionId };
|
|
}
|
|
|
|
export async function deleteNotionProperty(env, notionId) {
|
|
console.log("Deleting property from Notion:", notionId);
|
|
await updateNotionPage(notionId, {}, true, true);
|
|
}
|