Some checks failed
Build and publish / build (push) Failing after 17s
Source moved verbatim from mp/cms/ on 2026-04-29; mp was the first concrete adapter consuming the libreshop toolkit. Builds and publishes git.librete.ch/libreshop/cms on every main / v* push via the standard .gitea/workflows/build.yml shared across libreshop components.
274 lines
9.5 KiB
TypeScript
274 lines
9.5 KiB
TypeScript
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<T> = {
|
|
action: "create" | "update" | "delete";
|
|
params: {
|
|
data?: T;
|
|
where?: { id: number };
|
|
select?: string[];
|
|
populate?: string[];
|
|
};
|
|
state?: {
|
|
attribute?: string;
|
|
};
|
|
result?: T;
|
|
};
|
|
|
|
export const createComponentLifecycle = <T extends ProductCover | ProductPattern | ProductPages | ProductRuling | ProductImage>(
|
|
componentType: "cover" | "pattern" | "pages" | "ruling" | "image"
|
|
) => ({
|
|
async afterCreate(event: ComponentEvent<T>) {
|
|
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<T>) {
|
|
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<T>) {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|