import { factories } from "@strapi/strapi"; import { sanitize } from "@strapi/utils"; import { Result, PaginatedResult } from "@strapi/types/dist/modules/entity-service/result"; import { ID } from "@strapi/database/dist/types"; import { Product, ProductPattern } from "../../../../types"; import { productDefaultParams, calculateTotalProductPrice } from "../services/product"; import { maxProductsSitemap } from "../../../../config/constants"; export interface ProductVariantParams { populate: { pattern: any; cover: any; ruling: any; pages: any; images: any; }; pagination: { page: number; pageSize: number; }; filters?: any; publicationState?: "live" | "preview"; } const productVariantParams: ProductVariantParams = { populate: { pattern: { fields: ["id"] }, cover: { fields: ["id"] }, ruling: { fields: ["id"] }, pages: { fields: ["id"] }, images: { populate: { images: { fields: ["formats", "url"] } } } }, pagination: { page: 1, pageSize: 999 }, publicationState: "live" }; /** * Product controller with custom actions */ export default factories.createCoreController("api::product.product", ({ strapi }) => ({ /** * Find a single product by ID */ findOne: async (ctx) => { const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product")); const productUnsafe = await strapi.service("api::product.product").findOne(id, productDefaultParams); const product = (await sanitize.contentAPI.output(productUnsafe, strapi.getModel("api::product.product"))) as Product; return { ...product, totalProductPrice: calculateTotalProductPrice(product) }; }, /** * Find products with the lowest price ruling and pages */ find: async (ctx): Promise<{ data: Result<"api::product.product">[]; meta: { pagination: any } }> => { try { const params = await sanitize.contentAPI.query(ctx.query, strapi.getModel("api::product.product")); const filtersParams = (params?.filters as Record) ?? {}; const populateParams = (params?.populate as Record) ?? {}; const paginationParams = (params?.pagination as Record) ?? {}; const mergedParams = { ...productDefaultParams, filters: filtersParams, populate: { ...productDefaultParams.populate, ...populateParams }, pagination: paginationParams }; strapi.log.debug(JSON.stringify({ mergedParams })); const response = await strapi.service("api::product.product").find(mergedParams); const { pagination } = response; const dataUnsafe = (await sanitize.contentAPI.output( response.results, strapi.getModel("api::product.product") )) as Result<"api::product.product">[]; const data = dataUnsafe.map((product: Product) => { return { ...product, totalProductPrice: calculateTotalProductPrice(product) }; }); return { data, meta: { pagination } }; } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not fetch products"); } }, /** * Get all variants for a product * @returns {Promise} All variants of the product */ allVariants: async (ctx): Promise => { try { const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product")); strapi.log.verbose(`Fetching variants for product ${id}`); const product = await getProductDetails(id as ID); const variants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", { ...productVariantParams, filters: { pattern: { id: { $eq: product.pattern.id } } } }); if (!variants) { return ctx.notFound(`Could not find variants for product ${id}`); } return Promise.all(variants.map((variant) => sanitize.contentAPI.output(variant, strapi.getModel("api::product.product")))) as Promise< Product[] >; } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not fetch product variants"); } }, /** * Get variants by pattern for a product * @returns {Promise<{ allProductPattern: ProductPattern[], productVariants: Product[], patterns: Array }>} Variants grouped by pattern */ variantsByPattern: async ( ctx ): Promise<{ allProductPattern: ProductPattern[]; productVariants: Product[]; patterns: Array; }> => { try { const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product")); strapi.log.verbose(`Fetching variants by pattern for product ${id}`); const product = await getProductDetails(id as ID); const { pattern, cover, ruling, pages } = product; const productVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>("api::product.product", { ...productVariantParams, filters: { $and: [ { id: { $ne: id } }, { pattern: { id: { $ne: pattern.id } } }, { cover: { id: { $eq: cover.id } } }, { ruling: { id: { $eq: ruling.id } } }, { pages: { id: { $eq: pages.id } } } ] } }); const sanitizedVariants = (await Promise.all( productVariants.map((variant) => sanitize.contentAPI.output(variant, strapi.getModel("api::product.product"))) )) as Product[]; const allProductPatternResponse = (await strapi.service("api::product-pattern.product-pattern").find({ fields: ["id", "name", "description"], populate: { image: { fields: ["url"] } }, filters: { id: { $ne: pattern.id } }, publicationState: "live" })) as PaginatedResult<"api::product-pattern.product-pattern">; const allProductPattern = allProductPatternResponse.results; return { allProductPattern, productVariants: sanitizedVariants, patterns: allProductPattern.map((pattern) => ({ ...pattern, productVariant: sanitizedVariants.find((variant) => variant?.pattern?.id === pattern.id) })) }; } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not fetch variants by pattern"); } }, /** * Get variants for a product * @returns {Promise<{ pages: any[], cover: any[], ruling: any[] }>} Variants grouped by pages, cover, and ruling */ variants: async ( ctx ): Promise<{ pages: any; cover: any; ruling: any; }> => { try { const { id } = await sanitize.contentAPI.query(ctx.params, strapi.getModel("api::product.product")); strapi.log.verbose(`Fetching variants for product ${id}`); const product = await getProductDetails(id as ID); const allProductPatternVariants = await strapi.entityService.findMany<"api::product.product", ProductVariantParams>( "api::product.product", { ...productVariantParams, filters: { $and: [{ pattern: { id: { $eq: product.pattern.id } } }, { id: { $ne: id } }] } } ); const [allProductPages, allProductRulings, allProductCovers] = await Promise.all([ strapi .service("api::product-page.product-page") .find({ fields: ["id", "name"] }) as PaginatedResult<"api::product-pattern.product-pattern">, strapi.service("api::product-ruling.product-ruling").find({ fields: ["id", "name"], populate: { icon: { fields: ["url"] } } }) as PaginatedResult<"api::product-ruling.product-ruling">, strapi.service("api::product-cover.product-cover").find({ fields: ["id", "name", "binding", "price"], populate: { icon: { fields: ["url"] } } }) as PaginatedResult<"api::product-cover.product-cover"> ]); const sanitizedVariants = (await Promise.all( allProductPatternVariants.map((variant) => sanitize.contentAPI.output(variant, strapi.getModel("api::product.product"))) )) as Product[]; // For pages variants: find products with same pattern, cover, and ruling, but different pages const pagesVariants = allProductPages.results.map((pages) => ({ ...pages, productVariant: sanitizedVariants.find( (variant) => variant.id !== product.id && variant.pages.id === pages.id && variant.cover.id === product.cover.id && variant.ruling.id === product.ruling.id ) })); // For cover variants: find products with same pattern and ruling but different covers const coverVariants = allProductCovers.results.map((cover) => { // Find any product with this cover and the same pattern const matchingVariant = sanitizedVariants.find( (variant) => variant.id !== product.id && variant.cover.id === cover.id && variant.pattern.id === product.pattern.id && variant.ruling.id === product.ruling.id ); return { ...cover, productVariant: matchingVariant }; }); // For ruling variants: find products with same pattern, cover, and pages, but different ruling const rulingVariants = allProductRulings.results.map((ruling) => ({ ...ruling, productVariant: sanitizedVariants.find( (variant) => variant.id !== product.id && variant.ruling.id === ruling.id && variant.pages.id === product.pages.id && variant.cover.id === product.cover.id ) })); return { pages: pagesVariants, cover: coverVariants, ruling: rulingVariants }; } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not fetch product variants"); } }, /** * Publish or unpublish products by filter * @returns {Promise<{published: number, dryRun: boolean}>} The number of products published/unpublished and dryRun status */ publishByFilter: async (ctx): Promise<{ published: number; dryRun: boolean }> => { try { const { filters, publish = true, dryRun = false } = ctx.request.body; if (!filters) { return ctx.badRequest("Filters are required"); } strapi.log.debug(`Publishing products with filters: ${JSON.stringify(filters)}, publish: ${publish}, dryRun: ${dryRun}`); return await strapi.service("api::product.product").publishByFilter(filters, publish, dryRun); } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not publish products"); } }, /** * Find products with only the cheapest variant per cover+pattern combination */ findCheapest: async (ctx): Promise<{ data: Result<"api::product.product">[]; meta: { pagination: any } }> => { try { const params = await sanitize.contentAPI.query(ctx.query, strapi.getModel("api::product.product")); const filtersParams = (params?.filters as Record) ?? {}; const populateParams = (params?.populate as Record) ?? {}; const paginationParams = (params?.pagination as Record) ?? {}; // First get all products const mergedParams = { ...productDefaultParams, filters: filtersParams, populate: { ...productDefaultParams.populate, ...populateParams }, pagination: { pageSize: 999 } // Temporarily get all to filter }; const allProducts = await strapi.service("api::product.product").find(mergedParams); const allProductsSanitized = (await sanitize.contentAPI.output( allProducts.results, strapi.getModel("api::product.product") )) as Product[]; // Group products by cover+pattern combination const productGroups = new Map(); allProductsSanitized.forEach((product) => { const key = `${product.cover.id}-${product.pattern.id}`; if (!productGroups.has(key) || calculateTotalProductPrice(product) < calculateTotalProductPrice(productGroups.get(key))) { productGroups.set(key, product); } }); // Convert map values to array and sort by pattern.id const cheapestProducts = Array.from(productGroups.values()).sort((a, b) => { const idA = a.pattern?.id ?? 0; const idB = b.pattern?.id ?? 0; return idA - idB; }); // Apply pagination manually const page = parseInt(paginationParams.page) || 1; const pageSize = parseInt(paginationParams.pageSize) || 24; const total = cheapestProducts.length; const pageCount = Math.ceil(total / pageSize); const start = (page - 1) * pageSize; const end = start + pageSize; const paginatedProducts = cheapestProducts.slice(start, end); // Add total price to each product const data = paginatedProducts.map((product) => ({ ...product, totalProductPrice: calculateTotalProductPrice(product) })); return { data, meta: { pagination: { page, pageSize, pageCount, total } } }; } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not fetch cheapest products"); } }, /** * Get all published product paths for sitemap generation * @returns {Promise} Array of product paths */ sitemap: async (ctx): Promise => { try { strapi.log.verbose("Generating product sitemap"); const products = await strapi.entityService.findMany("api::product.product", { fields: ["slug"], filters: { publishedAt: { $notNull: true } }, pagination: { page: 1, pageSize: maxProductsSitemap }, sort: { updatedAt: "desc" } }); if (!products || !products.length) { return []; } const productPaths = products.map((product) => `/details/${product.slug}`); strapi.log.verbose(`Generated product sitemap with ${productPaths.length} entries`); strapi.log.silly(`Product sitemap: ${JSON.stringify(productPaths)}`); return productPaths; } catch (error) { strapi.log.error(error); return ctx.badRequest("Could not generate product sitemap"); } } })); /** * Helper function to get product details * @param {ID} id - The product ID * @returns {Promise} The product details * @throws {Error} If the product is not found */ async function getProductDetails(id: ID): Promise { const product = await strapi.entityService.findOne("api::product.product", id, { fields: ["id", "name"], populate: { pattern: { fields: ["id"] }, cover: { fields: ["id"] }, ruling: { fields: ["id"] }, pages: { fields: ["id"] } } }); if (!product) { throw new Error(`Could not find product with ID ${id}`); } return (await sanitize.contentAPI.output(product, strapi.getModel("api::product.product"))) as Promise; }