scripts/recipes/migrate-issue.mjs (408 lines of code) (raw):

async function visualDelay(time) { return new Promise((resolve)=>setTimeout(resolve, time)); } async function fetchFrontFromIssue (frontsIssueUrl, frontsIssueId, frontsHeaders, frontName){ const frontsResponse = await fetch(frontsIssueUrl, { method: "GET", headers: frontsHeaders, }); if (frontsResponse.status !== 200) { console.error( `Error getting issue data from Fronts tool: ${ frontsResponse.status } ${frontsResponse.statusText} ${await frontsResponse.text()}` ); process.exit(1); } /** * @type {{ * id: string; * edition: string; * issueDate: string; // YYYY-MM-dd * createdOn: number; * createdBy: string; * createdEmail: string; * launchedOn?: number; * launchedBy: string; * launchedEmail: string; * fronts: Array<{ * id: string; * displayName: string; * isHidden: boolean; * updatedOn?: number; * updatedBy?: string; * updatedEmail?: string; * collections: Array<{ * id: string; * displayName: string; * prefill?: EditionsPrefill; * isHidden: boolean; * lastUpdated?: number; * updatedBy?: string; * updatedEmail?: string; * items: any[]; * }>; * }>; * supportsProofing: boolean; * lastProofedVersion?: string; * platform: string; * }} */ const issueJson = await frontsResponse.json(); console.log("Got Fronts data."); const front = issueJson.fronts.find( (front) => front.displayName === frontName ); if (!front) { console.error( `No front found with name ${frontName} in issue ${frontsIssueId}` ); throw new Error(`No front found with name ${frontName} in issue ${frontsIssueId}`) } return front; } async function migrateFront( frontsIssueId, curationPath, curation, stage, frontName, frontsHeaders, frontsBaseUrl, dryRun ) { console.log(`Migrating curation data from ${curationPath} to ${stage} Fronts tool, issue: ${frontsIssueId}, front name: ${frontName}, dry run: ${dryRun}.`); await visualDelay(1000); const frontsIssueUrl = `${frontsBaseUrl}/editions-api/issues/${frontsIssueId}`; console.log(`Got curation data.\nFetching fronts issue data from ${frontsIssueUrl}...`); let front = await fetchFrontFromIssue(frontsIssueUrl, frontsIssueId, frontsHeaders, frontName); const collectionNamesInFrontsTool = front.collections.map( (col) => col.displayName ); const collectionTitlesMissingInFronts = curation .filter((col) => !collectionNamesInFrontsTool.includes(col.title.trim())) .map((col) => col.title); if (collectionTitlesMissingInFronts.length) { // Collections are added from the top, so we add the last collection first for (const title of collectionTitlesMissingInFronts.reverse()) { const newCollectionResponse = await fetch( `${frontsBaseUrl}/editions-api/fronts/${front.id}/collection?name=${encodeURIComponent(title)}`, { method: "PUT", headers: frontsHeaders, } ); if (newCollectionResponse.status !== 200) { console.error( `Error creating new collection: ${ newCollectionResponse.status } ${ newCollectionResponse.statusText } ${await newCollectionResponse.text()}` ); process.exit(1); } else { console.log( `Collection with title ${title} added to front ${front.id}` ); } } //reload the fronts to ensure we have up-to-date data front = await fetchFrontFromIssue(frontsIssueUrl, frontsIssueId, frontsHeaders, frontName); } const titleMap = Object.values(front.collections).reduce( (acc, collection) => ({ ...acc, [collection.displayName]: collection, }), {} ); console.log( `Mapped titles to collections: \n\n${JSON.stringify( Object.entries(titleMap).map(([title, col]) => `${title}: ${col.id}`), undefined, " " )}` ); const skippedCollections = []; const updatedCollections = curation.flatMap((collection) => { const frontCollection = titleMap[collection.title.trim()]; const frontCollectionId = frontCollection?.id; if (!frontCollectionId) { console.log( `No id found mapping the title ${collection.title} to the existing front – skipping this collection` ); skippedCollections.push(collection.title); return []; } const items = collection.items.flatMap((item, index) => { const cardMeta = { uuid: crypto.randomUUID(), frontPublicationDate: Date.now(), }; const cardType = item.chef ? "chef" : item.recipe ? "recipe" : item.collection ? "collection" : undefined; if (!cardType) { console.log(`No card type for ${item}`); return []; } switch (cardType) { case "recipe": { return [ { ...cardMeta, id: item.recipe.id, cardType: "recipe", }, ]; } case "chef": { const {id, bio, image, foregroundHex, backgroundHex} = item.chef; return [ { ...cardMeta, id: id, frontPublicationDate: Date.now(), cardType: "chef", meta: { bio: bio, ...(foregroundHex ? { chefTheme: { id: "custom", palette: { foregroundHex: foregroundHex, backgroundHex: backgroundHex, }, }, } : {}), ...(image ? { chefImageOverride: { src: image, origin: image, }, } : {}), }, }, ]; } case "collection": { const {title, image, lightPalette, darkPalette, recipes} = item.collection; return [ { ...cardMeta, id: crypto.randomUUID(), cardType: "feast-collection", meta: { title, supporting: recipes.map((id) => ({ cardType: "recipe", id, uuid: crypto.randomUUID(), frontPublicationDate: Date.now(), })), ...(lightPalette && darkPalette ? { feastCollectionTheme: { id: "custom", lightPalette, darkPalette, ...(image ? {imageURL: image} : {}), }, } : {}), }, }, ]; } default: { console.warn("++?????++ Out of Cheese Error. Redo From Start."); } } }); return { ...frontCollection, items, }; }); if (dryRun) { console.log( `Dry run, stopping. Would have updated ${ updatedCollections.length } collections: \n\n ${JSON.stringify( updatedCollections, undefined, " " )}` ); } else { for (const updatedCollection of updatedCollections) { const body = JSON.stringify({ id: updatedCollection.id, collection: updatedCollection, }); const bodySize = new Blob([body]).size; const response = await fetch( `${frontsBaseUrl}/editions-api/collections/${updatedCollection.id}`, { method: "PUT", headers: frontsHeaders, body, } ); if (response.status !== 200) { console.error( `Error putting new collection data for collection ${ updatedCollection.id } from Fronts tool. Attempted to post \n\n ${JSON.stringify( updatedCollection, null, " " )}. \n\n Response was: ${response.status} ${ response.statusText } ${await response.text()}` ); throw new Error("Error putting new collection"); } else { console.log( `Written updated collection with name: ${ updatedCollection.displayName }, id: ${ updatedCollection.id } payload ${new Intl.NumberFormat().format(bodySize)} bytes` ); } } console.log(`Updated ${updatedCollections.length} collections`); if (updatedCollections.length < curation.length) { console.warn( `${ curation.length - updatedCollections.length } collections were not mapped onto the front. Do the collection names match exactly? \n\n ${skippedCollections.join( "\n" )}` ); } } } async function getCuration(curationBaseUrl, curationPath, curationType, date) { //------Get CURATION.JSON --------// const curationUrl = `${curationBaseUrl}/${curationPath}/${curationType}/${date}/curation.json`; console.log(`Fetching curation data from ${curationUrl} ...`); const curationResponse = await fetch(curationUrl); if (curationResponse.status === 200) { return curationResponse.json(); } else { //---Dont proceed if Curation.json does not exists for that date---// console.error( `Error getting issue data from Fronts tool: ${curationResponse.status} ${ curationResponse.statusText } ${await curationResponse.text()}` ); return undefined; } } export async function migrateIssue( curationBaseUrl, curationPath, date, frontsBaseUrl, frontsHeaders, editionName, stage, dryRun ) { const curation = { allRecipes: await getCuration(curationBaseUrl, curationPath, 'all-recipes', date), meatFree: await getCuration(curationBaseUrl, curationPath, 'meat-free', date) } if(!curation.allRecipes && !!curation.meatFree) { throw new Error(`Missing all-recipes curation for ${date}`); } else if(!curation.meatFree && !!curation.allRecipes) { throw new Error(`Missing meat-free curation for ${date}`) } else if(!curation.allRecipes && !curation.meatFree) { throw new Error(`Missing both meat-free and all-recipes curation for ${date}`); } //---Proceed to FRONT work if curation.json is present---// /** * @type {Array<{ * title: string, * body: string, * id: string, * items: Array< * { recipe: { id: string } } | * { chef: { id: string }} | * { collection: { recipes: string[] }} * >} * >} */ const fetchIssueForTheDate = async (editionName, start, end) => { const path = `${frontsBaseUrl}/editions-api/editions/${editionName}/issues?dateFrom=${start}&dateTo=${end}` console.log(`start fetching issue ${path} if available...`) const resp = await fetch(path, { method: 'get', mode: 'cors', headers: frontsHeaders, credentials: 'include', }); console.log("response when fetching issue if available = " + resp.status) const content = await resp.json(); console.log(content); return content[0]; }; const issueFoundForTheDate = await fetchIssueForTheDate(editionName, date, date); let frontsIssueId = "" if (issueFoundForTheDate && issueFoundForTheDate.id) { //Skip rest of work of data migration if issue exists on the date console.log(`Yes we already have existing issue ${issueFoundForTheDate.id}`) frontsIssueId = issueFoundForTheDate.id } else { //Proceed to create new issue and migration of data in to it. const createIssue = async (editionName, date) => { const path = `${frontsBaseUrl}/editions-api/editions/${editionName}/issues`; console.log("start creating issue...") const resp = await fetch(path, { method: 'post', mode: 'cors', headers: frontsHeaders, credentials: 'include', body: JSON.stringify({ issueDate: `${date}` }), }).then((response) => { console.log("response = " + response.status) return response.json(); }); return resp } const frontsIssue = await createIssue(editionName, date); frontsIssueId = frontsIssue.id; } if(!frontsIssueId) { console.error(`No Front is available in Fronts tool so unable to migrate: ${editionName} ${date}`); } else { console.log(`Issue is created: ${frontsIssueId}`); for(const frontName of ['All Recipes', 'Meat-Free']) { try { await migrateFront( frontsIssueId, curationPath, frontName === 'All Recipes' ? curation.allRecipes : curation.meatFree, stage, frontName, frontsHeaders, frontsBaseUrl, dryRun ); } catch(err) { console.error(`Unable to migrate from ${frontName} for ${editionName} ${date}: ${err}`); await visualDelay(5000); } } } }