import { contentTypes } from '@strapi/utils'; import { resolveReadableMainField, buildHomepageQueryFields, compactSanitizedFields, resolveTitleField } from './homepage-query-utils.mjs'; const createHomepageService = ({ strapi })=>{ const MAX_DOCUMENTS = 4; const metadataService = strapi.plugin('content-manager').service('document-metadata'); const permissionService = strapi.admin.services.permission; const getRegisteredContentType = (uid)=>{ const contentType = strapi.contentTypes[uid]; if (contentType === undefined) { strapi.log.warn(`Skipping homepage content type "${uid}" because it is no longer registered.`); return undefined; } return contentType; }; const getConfiguration = async (contentTypeUids)=>{ /** * Don't use the strapi.store util because we need to make * more precise queries than exact key matches, in order to make as few queries as possible. */ const coreStore = strapi.db.query('strapi::core-store'); const rawConfigurations = await coreStore.findMany({ where: { key: { $in: contentTypeUids.map((contentType)=>`plugin_content_manager_configuration_content_types::${contentType}`) } } }); return rawConfigurations.map((rawConfiguration)=>{ return JSON.parse(rawConfiguration.value); }); }; const getPermittedContentTypes = async ()=>{ const readPermissions = await permissionService.findMany({ where: { role: { users: { id: strapi.requestContext.get()?.state?.user.id } }, action: 'plugin::content-manager.explorer.read' } }); // Deduplicate subjects using a Set: the JOIN across permission -> role -> users produces one row // per role the user belongs to, so a multi-role user gets duplicate subjects. // Using a Set collapses them to unique content type UID. return [ ...new Set(readPermissions.map((permission)=>permission.subject).filter((subject)=>{ if (!subject) { return false; } const contentType = strapi.contentTypes[subject]; const contentTypeOptions = contentType?.pluginOptions?.['content-manager']; return contentTypeOptions?.visible !== false; })) ]; }; const permissionCheckerService = strapi.plugin('content-manager').service('permission-checker'); const getPermissionChecker = (uid)=>permissionCheckerService.create({ userAbility: strapi.requestContext.get()?.state.userAbility, model: uid }); const getContentTypesMeta = (allowedContentTypeUids, configurations)=>{ return allowedContentTypeUids.reduce((acc, uid)=>{ const configuration = configurations.find((config)=>config.uid === uid); const contentType = getRegisteredContentType(uid); if (contentType === undefined) { return acc; } const mainField = resolveReadableMainField(contentType, configuration, getPermissionChecker(uid)); const fields = buildHomepageQueryFields(contentType, mainField); const hasDraftAndPublish = contentTypes.hasDraftAndPublish(contentType); acc.push({ fields, mainField, contentType, hasDraftAndPublish, uid }); return acc; }, []); }; const formatDocuments = (documents, meta, populate)=>{ return documents.map((document)=>{ const additionalFields = populate?.reduce((acc, key)=>{ acc[key] = document[key]; return acc; }, {}) || {}; return { documentId: document.documentId, locale: document.locale ?? null, updatedAt: new Date(document.updatedAt), title: document[meta.mainField ?? 'documentId'], publishedAt: meta.hasDraftAndPublish && document.publishedAt ? new Date(document.publishedAt) : null, contentTypeUid: meta.uid, contentTypeDisplayName: meta.contentType.info.displayName, kind: meta.contentType.kind, ...additionalFields }; }); }; const sanitizeHomepageQuery = async (meta, additionalQueryParams)=>{ const permissionQuery = await getPermissionChecker(meta.uid).sanitizedQuery.read({ limit: MAX_DOCUMENTS, fields: meta.fields, ...additionalQueryParams, locale: '*' }); const sanitizedFields = compactSanitizedFields(permissionQuery.fields); if (sanitizedFields !== undefined) { permissionQuery.fields = sanitizedFields; } return { permissionQuery, titleField: resolveTitleField(meta.mainField, sanitizedFields) }; }; return { async addStatusToDocuments (documents) { return Promise.all(documents.map(async (recentDocument)=>{ const hasDraftAndPublish = contentTypes.hasDraftAndPublish(strapi.contentType(recentDocument.contentTypeUid)); /** * Tries to query the other version of the document if draft and publish is enabled, * so that we know when to give the "modified" status. */ const { availableStatus } = await metadataService.getMetadata(recentDocument.contentTypeUid, recentDocument, { availableStatus: hasDraftAndPublish, availableLocales: false }); const status = metadataService.getStatus(recentDocument, availableStatus); return { ...recentDocument, status: hasDraftAndPublish ? status : undefined }; })); }, async queryLastDocuments (additionalQueryParams, draftAndPublishOnly) { const permittedContentTypes = await getPermittedContentTypes(); const allowedContentTypeUids = draftAndPublishOnly ? permittedContentTypes.filter((uid)=>{ const contentType = getRegisteredContentType(uid); return contentType !== undefined && contentTypes.hasDraftAndPublish(contentType); }) : permittedContentTypes; // Fetch the configuration for each content type in a single query const configurations = await getConfiguration(allowedContentTypeUids); // Get the necessary metadata for the documents const contentTypesMeta = getContentTypesMeta(allowedContentTypeUids, configurations); const recentDocuments = await Promise.all(contentTypesMeta.map(async (meta)=>{ const { permissionQuery, titleField } = await sanitizeHomepageQuery(meta, additionalQueryParams); const docs = await strapi.documents(meta.uid).findMany(permissionQuery); const populate = additionalQueryParams?.populate; return formatDocuments(docs, { ...meta, mainField: titleField }, populate); })); return recentDocuments.flat().sort((a, b)=>{ switch(additionalQueryParams?.sort){ case 'publishedAt:desc': if (!a.publishedAt || !b.publishedAt) return 0; return b.publishedAt.valueOf() - a.publishedAt.valueOf(); case 'publishedAt:asc': if (!a.publishedAt || !b.publishedAt) return 0; return a.publishedAt.valueOf() - b.publishedAt.valueOf(); case 'updatedAt:desc': if (!a.updatedAt || !b.updatedAt) return 0; return b.updatedAt.valueOf() - a.updatedAt.valueOf(); case 'updatedAt:asc': if (!a.updatedAt || !b.updatedAt) return 0; return a.updatedAt.valueOf() - b.updatedAt.valueOf(); default: return 0; } }).slice(0, MAX_DOCUMENTS); }, async getRecentlyPublishedDocuments () { const recentlyPublishedDocuments = await this.queryLastDocuments({ sort: 'publishedAt:desc', status: 'published' }, true); return this.addStatusToDocuments(recentlyPublishedDocuments); }, async getRecentlyUpdatedDocuments () { const recentlyUpdatedDocuments = await this.queryLastDocuments({ sort: 'updatedAt:desc' }); return this.addStatusToDocuments(recentlyUpdatedDocuments); }, async getCountDocuments () { const permittedContentTypes = await getPermittedContentTypes(); // Fetch the configuration for each content type in a single query const configurations = await getConfiguration(permittedContentTypes); // Get the necessary metadata for the documents const contentTypesMeta = getContentTypesMeta(permittedContentTypes, configurations); const countDocuments = { draft: 0, published: 0, modified: 0 }; await Promise.all(contentTypesMeta.map(async (meta)=>{ const strapiDBConnection = strapi.db.connection; const tableName = strapi.contentType(meta.uid).collectionName; if (!tableName) return; if (!meta.hasDraftAndPublish) { const publishedDocuments = await strapiDBConnection(tableName).countDistinct('document_id as count').first(); countDocuments.published += Number(publishedDocuments?.count) || 0; return; } // Classify each document_id into a single bucket (draft / published / modified) // in one pass. Replaces three separate self-join queries that scaled poorly on // large tables — see https://github.com/strapi/strapi/issues/25200. const classified = strapiDBConnection(tableName).select('document_id').select(strapiDBConnection.raw(`CASE WHEN MAX(CASE WHEN published_at IS NOT NULL THEN 1 ELSE 0 END) = 0 THEN 'draft' WHEN MAX(CASE WHEN published_at IS NULL THEN updated_at END) = MAX(CASE WHEN published_at IS NOT NULL THEN updated_at END) THEN 'published' ELSE 'modified' END AS bucket`)).groupBy('document_id'); const counts = await strapiDBConnection.from(classified.as('classified')).select(strapiDBConnection.raw(`COUNT(CASE WHEN bucket = 'draft' THEN 1 END) AS draft, COUNT(CASE WHEN bucket = 'published' THEN 1 END) AS published, COUNT(CASE WHEN bucket = 'modified' THEN 1 END) AS modified`)).first(); countDocuments.draft += Number(counts?.draft) || 0; countDocuments.published += Number(counts?.published) || 0; countDocuments.modified += Number(counts?.modified) || 0; })); return countDocuments; } }; }; export { createHomepageService }; //# sourceMappingURL=homepage.mjs.map