import { Strapi } from "@strapi/strapi"; import { productDefaultParams } from "../api/product/services/product"; import { ProductCover, ProductImage, ProductPages, ProductPattern, ProductRuling } from "../../types"; import { formatSlug } from "./formatSlug"; import { ID } from "@strapi/database/dist/types"; type ComponentEvent = { action: "create" | "update" | "delete"; params: { data?: T; where?: { id: number }; select?: string[]; populate?: string[]; }; state?: { attribute?: string; }; result?: T; }; export const createComponentLifecycle = ( componentType: "cover" | "pattern" | "pages" | "ruling" | "image" ) => ({ async afterCreate(event: ComponentEvent) { const { result } = event; strapi.log.verbose(`app:v:${componentType}-lifecycle: Created ${JSON.stringify({ componentType, id: result.id })}`); if (componentType === "image") { return; } try { await generateNewProducts(strapi, componentType, parseInt(result.id as string)); } catch (error) { strapi.log.error(`app:e:${componentType}-lifecycle: Error generating products ${JSON.stringify({ error, componentId: result.id })}`); } }, async afterUpdate(event: ComponentEvent) { const { result, params } = event; // Only update products if price changed try { await updateRelatedProducts(strapi, componentType, parseInt(result.id as string)); } catch (error) { strapi.log.error( `app:e:${componentType}-lifecycle: Error updating products ${JSON.stringify({ error, componentId: result.id })}` ); } }, async beforeDelete(event: ComponentEvent) { const { id } = event.params.where; try { // Find all products using this component strapi.log.debug( `app:d:${componentType}-lifecycle: Deleting related products`, JSON.stringify({ filters: { [componentType]: { id } } }) ); const products = await strapi.entityService.findMany("api::product.product", { filters: { [componentType]: { id } } }); // Delete related products // TODO: Deleting too many products if (products.length) { for (const product of products) { await strapi.entityService.delete("api::product.product", product.id); } } } catch (error) { strapi.log.error( `app:e:${componentType}-lifecycle: Error deleting related products`, JSON.stringify({ error, componentId: id }) ); throw error; // Propagate error to prevent component deletion if products can't be deleted } } }); async function generateNewProducts(strapi: Strapi, componentType: string, componentId: number) { // Fetch all necessary components const [covers, patterns, pages, rulings] = await Promise.all([ componentType === "cover" ? [await strapi.entityService.findOne("api::product-cover.product-cover", componentId)] : strapi.entityService.findMany("api::product-cover.product-cover"), componentType === "pattern" ? [await strapi.entityService.findOne("api::product-pattern.product-pattern", componentId)] : strapi.entityService.findMany("api::product-pattern.product-pattern"), componentType === "pages" ? [await strapi.entityService.findOne("api::product-page.product-page", componentId)] : strapi.entityService.findMany("api::product-page.product-page"), componentType === "ruling" ? [await strapi.entityService.findOne("api::product-ruling.product-ruling", componentId)] : strapi.entityService.findMany("api::product-ruling.product-ruling") ]); strapi.log.debug(`app:d:lifecycle-factory ${JSON.stringify({ covers, patterns, pages, rulings })}`); // Generate new combinations for (const cover of covers) { for (const pattern of patterns) { for (const page of pages) { for (const ruling of rulings) { const name = `${pattern.name} ${cover.name} · ${page.name} · ${ruling.name}`; const slug = formatSlug(name); const existing = await strapi.entityService.findMany("api::product.product", { filters: { slug } }); if (existing.length) { strapi.log.warn(`app:w:lifecycle-factory: Product "${name}" already exists with ID ${existing[0]?.id}`); strapi.log.debug(`app:d:lifecycle-factory: ${JSON.stringify({ existing })}`); continue; } const categoriesFilter = { filters: { product_cover: cover.id, product_pattern: pattern.id } }; const categories = await strapi.service("api::product-category.product-category").categories(categoriesFilter); if (categories) { strapi.log.debug(`app:d:lifecycle-factory: Attaching product-image with ID ${categories[0]?.id}`); } else { strapi.log.warn("app:w:lifecycle-factory: Product-image not found"); } const images = categories ? categories[0]?.id : null; const data = { name, slug, images, cover: cover.id, pattern: pattern.id, pages: page.id, ruling: ruling.id, // TODO: Might be strapi v5 syntax // https://docs.strapi.io/cms/migration/v4-to-v5/breaking-changes/publication-state-removed status: "draft", publishedAt: null }; await strapi.entityService.create("api::product.product", { data }); } } } } } async function updateRelatedProducts(strapi: Strapi, componentType: string, componentId: number) { // Find all related products with full population const products = await strapi.entityService.findMany("api::product.product", { filters: { [componentType === "image" ? "images" : componentType]: { id: componentId } }, ...productDefaultParams, publicationState: "preview" }); // @ts-expect-error const parent = await strapi.entityService.findOne(`api::product-${componentType}.product-${componentType}`, componentId); strapi.log.info(`app:i:lifecycle-factory: Updating ${products.length} products related to ${componentType} with ID ${componentId}`); for (const product of products) { try { // Get the current components for this product const cover = product.cover?.id; const pattern = product.pattern?.id; const pages = product.pages?.id; const ruling = product.ruling?.id; // Determine if this product should still exist based on component availability const componentsExist = await validateComponents(strapi, cover, pattern, pages, ruling); if (!componentsExist) { // Delete products with missing components strapi.log.info(`app:i:lifecycle-factory: Deleting product ${product.id} due to missing components`); await strapi.entityService.delete("api::product.product", product.id); continue; } // Regenerate product name and slug based on current components const [coverData, patternData, pagesData, rulingData] = await Promise.all([ strapi.entityService.findOne("api::product-cover.product-cover", cover), strapi.entityService.findOne("api::product-pattern.product-pattern", pattern), strapi.entityService.findOne("api::product-page.product-page", pages), strapi.entityService.findOne("api::product-ruling.product-ruling", ruling) ]); const name = `${patternData.name} ${coverData.name} · ${pagesData.name} · ${rulingData.name}`; const slug = formatSlug(name); // Find the correct category/product-image const categoriesFilter = { filters: { product_cover: cover, product_pattern: pattern } }; const categories = await strapi.service("api::product-category.product-category").categories(categoriesFilter); const images = categories?.length ? categories[0]?.id : null; // Check if a product with this name already exists (but is not this one) const existing = await strapi.entityService.findMany("api::product.product", { filters: { slug, id: { $ne: product.id } } }); if (existing.length) { // Duplicate found - delete this one if it's newer if (new Date(product.createdAt) > new Date(existing[0].createdAt)) { strapi.log.warn(`app:w:lifecycle-factory: Deleting duplicate product ${product.id} in favor of existing ${existing[0].id}`); await strapi.entityService.delete("api::product.product", product.id); } continue; } // Update the product with refreshed data const updateData = { name, slug, images // Don't update the publishedAt status here, as it's handled by updateRelatedProductsStatus }; strapi.log.verbose(`app:v:lifecycle-factory: Updating product ${product.id} with ${JSON.stringify(updateData)}`); await strapi.entityService.update("api::product.product", product.id, { data: updateData }); } catch (error) { strapi.log.error(`app:e:lifecycle-factory: Error updating product ${product.id}`, error); } } strapi.log.info(`app:i:lifecycle-factory: ✔ Finished updating products related to ${componentType} ${componentId}`); } // Helper function to validate that all components of a product exist async function validateComponents(strapi: Strapi, coverId: ID, patternId: ID, pagesId: ID, rulingId: ID): Promise { try { const [cover, pattern, pages, ruling] = await Promise.all([ coverId ? strapi.entityService.findOne("api::product-cover.product-cover", coverId) : null, patternId ? strapi.entityService.findOne("api::product-pattern.product-pattern", patternId) : null, pagesId ? strapi.entityService.findOne("api::product-page.product-page", pagesId) : null, rulingId ? strapi.entityService.findOne("api::product-ruling.product-ruling", rulingId) : null ]); return !!(cover && pattern && pages && ruling); } catch (error) { strapi.log.error(`app:e:lifecycle-factory: Error validating components`, error); return false; } }