import { isNil, omit } from 'lodash/fp'; import { async, errors, setCreatorFields, parseHasPublishedVersionQueryParam, hasPublishedVersionBooleanToPublicationFilterMode, contentTypes } from '@strapi/utils'; import { getService } from '../utils/index.mjs'; import { validateBulkActionInput } from './validation/index.mjs'; import { getProhibitedCloningFields, excludeNotCreatableFields } from './utils/clone.mjs'; import { getDocumentLocaleAndStatus } from './validation/dimensions.mjs'; import { formatDocumentWithMetadata } from './utils/metadata.mjs'; import { indexByDocumentId } from './utils/document-status.mjs'; import { buildDeepPopulate, getPopulateForLocalizations } from '../services/utils/populate.mjs'; /** * Returns documentIds for (documentId, locale) that have both draft and published, * optionally filtered by whether the draft is newer than the published row. * Uses strapi.documents only. */ const getDocumentIdsByDraftPublishRelation = async (uid, opts)=>{ const schema = strapi.getModel(uid); if (!contentTypes.hasDraftAndPublish(schema)) { return []; } const baseParams = { fields: [ 'documentId', 'locale', 'updatedAt' ], page: 1, pageSize: 10000, ...opts.locale != null && opts.locale !== '*' && { locale: opts.locale } }; const [drafts, published] = await Promise.all([ strapi.documents(uid).findMany({ ...baseParams, status: 'draft' }), strapi.documents(uid).findMany({ ...baseParams, status: 'published' }) ]); const publishedByKey = new Map(); for (const p of published){ const key = `${p.documentId}\t${String(p?.locale ?? '')}`; publishedByKey.set(key, { updatedAt: p.updatedAt }); } const ids = []; const wantModified = opts.type === 'modified'; for (const d of drafts){ const key = `${d.documentId}\t${String(d?.locale ?? '')}`; const pub = publishedByKey.get(key); if (pub) { const dUpdated = d?.updatedAt ? new Date(d.updatedAt).getTime() : 0; const pUpdated = pub?.updatedAt ? new Date(pub.updatedAt).getTime() : 0; const isModified = dUpdated > pUpdated; if (isModified === wantModified) { ids.push(d.documentId); } } } return [ ...new Set(ids) ]; }; /** Map from __status filter value to top-level query fields (mirrors client STATUS_PARAMS). */ const STATUS_QUERY_FROM_FILTER = { draft: { status: 'draft', publicationFilter: 'never-published-document' }, published: { status: 'published' }, 'published-modified': { publicationStatusFilter: 'published-modified' }, 'published-unmodified': { publicationStatusFilter: 'published-unmodified' } }; /** * Extracts __status from query.filters.$and into top-level status, publicationFilter, * and publicationStatusFilter so list works with either transformed params or raw filter params. */ const normalizeStatusFromFilters = (query)=>{ const filters = query.filters; if (!filters?.$and || !Array.isArray(filters.$and)) return; const remainingFilters = []; const statusValues = []; for (const filter of filters.$and){ const eq = filter?.__status?.$eq; if (eq != null) { statusValues.push(String(eq)); } else { remainingFilters.push(filter); } } if (statusValues.length === 0) return; const q = query; for (const value of statusValues){ const toApply = STATUS_QUERY_FROM_FILTER[value]; if (toApply) Object.assign(q, toApply); } if (remainingFilters.length > 0) { filters.$and = remainingFilters; } else { delete q.filters; } }; /** Returns filters object that merges existing $and with a documentId $in filter. */ const mergeDocumentIdFilter = (existingFilters, documentIds)=>{ const documentIdFilter = { documentId: documentIds.length > 0 ? { $in: documentIds } : { $in: [] } }; let existingAnd; if (existingFilters?.$and && Array.isArray(existingFilters.$and)) { existingAnd = existingFilters.$and; } else if (existingFilters && Object.keys(existingFilters).length > 0) { existingAnd = [ existingFilters ]; } else { existingAnd = []; } return { $and: [ ...existingAnd, documentIdFilter ] }; }; /** * Extracts the sort direction for the 'status' field from a sort parameter. * Returns 'ASC', 'DESC', or null if status is not being sorted. * * The sort param can be a string ('status:ASC'), an array (['status:ASC']), * or an object ({ status: 'ASC' }). */ const extractStatusSortOrder = (sort)=>{ if (!sort) return null; if (typeof sort === 'string') { const match = sort.match(/(?:^|,)\s*status:(ASC|DESC)\s*(?:,|$)/i); return match ? match[1].toUpperCase() : null; } if (Array.isArray(sort)) { for (const item of sort){ const result = extractStatusSortOrder(item); if (result) return result; } return null; } if (typeof sort === 'object' && sort !== null && 'status' in sort) { const dir = String(sort.status).toUpperCase(); return dir === 'ASC' || dir === 'DESC' ? dir : null; } return null; }; /** * Removes the 'status' field from a sort parameter, returning the remainder * (or undefined if status was the only sort field). */ const removeStatusFromSort = (sort)=>{ if (!sort) return sort; if (typeof sort === 'string') { const cleaned = sort.split(',').map((s)=>s.trim()).filter((s)=>!/^status:(ASC|DESC)$/i.test(s)).join(','); return cleaned || undefined; } if (Array.isArray(sort)) { const cleaned = sort.filter((item)=>extractStatusSortOrder(item) === null); return cleaned.length ? cleaned : undefined; } if (typeof sort === 'object' && sort !== null) { const rest = { ...sort }; delete rest.status; return Object.keys(rest).length ? rest : undefined; } return sort; }; /** * Create a new document. * * @param ctx - Koa context * @param opts - Options * @param opts.populate - Populate options of the returned document. * By default documentManager will populate all relations. */ const createDocument = async (ctx, opts)=>{ const { userAbility, user } = ctx.state; const { model } = ctx.params; const { body } = ctx.request; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.create()) { throw new errors.ForbiddenError(); } const pickPermittedFields = permissionChecker.sanitizeCreateInput; const setCreator = setCreatorFields({ user }); const sanitizeFn = async.pipe(pickPermittedFields, setCreator); const sanitizedBody = await sanitizeFn(body); const { locale, status } = await getDocumentLocaleAndStatus(body, model); return documentManager.create(model, { data: sanitizedBody, locale, status, populate: opts?.populate }); // TODO: Revert the creation if create permission conditions are not met // if (permissionChecker.cannot.create(document)) { // throw new errors.ForbiddenError(); // } }; /** * Update a document version. * - If the document version exists, it will be updated. * - If the document version does not exist, a new document locale will be created. * By default documentManager will populate all relations. * * @param ctx - Koa context * @param opts - Options * @param opts.populate - Populate options of the returned document */ const updateDocument = async (ctx, opts)=>{ const { userAbility, user } = ctx.state; const { id, model } = ctx.params; const { body } = ctx.request; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.update()) { throw new errors.ForbiddenError(); } // Populate necessary fields to check permissions const permissionQuery = await permissionChecker.sanitizedQuery.update(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); const { locale } = await getDocumentLocaleAndStatus(body, model); // Load document version to update const [documentVersion, documentExists] = await Promise.all([ documentManager.findOne(id, model, { populate, locale, status: 'draft' }), documentManager.exists(model, id) ]); if (!documentExists) { throw new errors.NotFoundError(); } // If version is not found, but document exists, // the intent is to create a new document locale if (documentVersion) { if (permissionChecker.cannot.update(documentVersion)) { throw new errors.ForbiddenError(); } } else if (permissionChecker.cannot.create()) { throw new errors.ForbiddenError(); } const pickPermittedFields = documentVersion ? permissionChecker.sanitizeUpdateInput(documentVersion) : permissionChecker.sanitizeCreateInput; const setCreator = documentVersion ? setCreatorFields({ user, isEdition: true }) : setCreatorFields({ user }); const sanitizeFn = async.pipe(pickPermittedFields, setCreator); const sanitizedBody = await sanitizeFn(body); const updatedDocument = await documentManager.update(documentVersion?.documentId || id, model, { data: sanitizedBody, populate: opts?.populate, locale }); return updatedDocument; }; var collectionTypes = { async find (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const { query } = ctx.request; // Normalize so status/publicationStatusFilter are set from filters.$and.__status when present normalizeStatusFromFilters(query); const documentMetadata = getService('document-metadata'); const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.read()) { return ctx.forbidden(); } // Extract and remove 'status' sort before sanitization/validation, since status // is not a real schema attribute and would be rejected by the permission checker. // It is re-added after sanitization so the DB layer can handle it via a CASE expression. const hasPublicationStatusFilter = query.status !== undefined || query.hasPublishedVersion !== undefined || query.publicationStatusFilter !== undefined; const rawStatusSortOrder = extractStatusSortOrder(query.sort); // Disable status sort when a publication status filter is active — all results share the // same status, making the sort a no-op. const statusSortOrder = hasPublicationStatusFilter ? null : rawStatusSortOrder; // Always strip 'status' from the sort before passing to the document service / permission // checker — it is not a real schema attribute and would be rejected by validation. const queryWithoutStatusSort = rawStatusSortOrder ? { ...query, sort: removeStatusFromSort(query.sort) } : query; const permissionQuery = await permissionChecker.sanitizedQuery.read(queryWithoutStatusSort); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).populateDeep(1).countRelations({ toOne: false, toMany: true }).withPopulateOverride(getPopulateForLocalizations(model)).build(); // "Modified" is a UI-only filter; not a real document status. Read and strip it // so we never pass it to validation or the document service. const publicationStatusFilter = query.publicationStatusFilter; const queryForValidation = { ...query }; delete queryForValidation.publicationStatusFilter; const { locale, status } = await getDocumentLocaleAndStatus(queryForValidation, model); const paramsForDocumentService = omit([ 'publicationStatusFilter' ], permissionQuery); let findPageParams = { ...paramsForDocumentService, populate, locale, status }; if (query.publicationFilter !== undefined) { findPageParams.publicationFilter = query.publicationFilter; } else { const legacy = parseHasPublishedVersionQueryParam(query.hasPublishedVersion); if (legacy !== undefined) { findPageParams.publicationFilter = hasPublishedVersionBooleanToPublicationFilterMode(legacy); } } if (publicationStatusFilter === 'published-modified' || publicationStatusFilter === 'published-unmodified') { const type = publicationStatusFilter === 'published-modified' ? 'modified' : 'unmodified'; const documentIds = await getDocumentIdsByDraftPublishRelation(model, { locale, type }); findPageParams = { ...findPageParams, status: 'published', filters: mergeDocumentIdFilter(paramsForDocumentService.filters, documentIds) }; } if (statusSortOrder) { const s = findPageParams.sort; const statusSort = `status:${statusSortOrder}`; if (!s) { findPageParams.sort = statusSort; } else if (Array.isArray(s)) { findPageParams.sort = [ ...s, statusSort ]; } else if (typeof s === 'string') { findPageParams.sort = `${s},${statusSort}`; } else { findPageParams.sort = { ...s, status: statusSortOrder }; } } const { results: documents, pagination } = await documentManager.findPage(findPageParams, model); const hasDraftAndPublish = contentTypes.hasDraftAndPublish(strapi.getModel(model)); const statusByDocumentId = hasDraftAndPublish ? indexByDocumentId(await documentMetadata.getManyAvailableStatus(model, documents)) : new Map(); const setStatus = (document)=>{ // Available status of document const availableStatuses = statusByDocumentId.get(document.documentId) || []; // Compute document version status document.status = documentMetadata.getStatus(document, availableStatuses); return document; }; const results = await async.map(documents, async.pipe(permissionChecker.sanitizeOutput, setStatus)); ctx.body = { results, pagination }; }, async findOne (ctx) { const { userAbility } = ctx.state; const { model, id } = ctx.params; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.read()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.read(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).populateDeep(Infinity).countRelations().withPopulateOverride(getPopulateForLocalizations(model)).build(); const { locale, status } = await getDocumentLocaleAndStatus(ctx.query, model); const version = await documentManager.findOne(id, model, { populate, locale, status }); if (!version) { // Check if document exists const exists = await documentManager.exists(model, id); if (!exists) { return ctx.notFound(); } // If the requested locale doesn't exist, return an empty response const { meta } = await formatDocumentWithMetadata(permissionChecker, model, // @ts-expect-error TODO: fix { documentId: id, locale, publishedAt: null }, { availableLocales: true, availableStatus: false }); ctx.body = { data: {}, meta }; return; } // if the user has condition that needs populated content, it's not applied because entity don't have relations populated if (permissionChecker.cannot.read(version)) { return ctx.forbidden(); } // TODO: Count populated relations by permissions const sanitizedDocument = await permissionChecker.sanitizeOutput(version); ctx.body = await formatDocumentWithMetadata(permissionChecker, model, sanitizedDocument); }, async create (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const permissionChecker = getService('permission-checker').create({ userAbility, model }); const [totalEntries, document] = await Promise.all([ strapi.db.query(model).count(), createDocument(ctx) ]); const sanitizedDocument = await permissionChecker.sanitizeOutput(document); ctx.status = 201; ctx.body = await formatDocumentWithMetadata(permissionChecker, model, sanitizedDocument, { // Empty metadata as it's not relevant for a new document availableLocales: false, availableStatus: false }); if (totalEntries === 0) { strapi.telemetry.send('didCreateFirstContentTypeEntry', { eventProperties: { model } }); } }, async update (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const permissionChecker = getService('permission-checker').create({ userAbility, model }); const updatedVersion = await updateDocument(ctx); const sanitizedVersion = await permissionChecker.sanitizeOutput(updatedVersion); ctx.body = await formatDocumentWithMetadata(permissionChecker, model, sanitizedVersion); }, async clone (ctx) { const { userAbility, user } = ctx.state; const { model, sourceId: id } = ctx.params; const { body } = ctx.request; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.create()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.create(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); const { locale } = await getDocumentLocaleAndStatus(body, model); const document = await documentManager.findOne(id, model, { populate, locale, status: 'draft' }); if (!document) { return ctx.notFound(); } const pickPermittedFields = permissionChecker.sanitizeCreateInput; const setCreator = setCreatorFields({ user }); const excludeNotCreatable = excludeNotCreatableFields(model, permissionChecker); const sanitizeFn = async.pipe(pickPermittedFields, setCreator, excludeNotCreatable); const sanitizedBody = await sanitizeFn(body); const clonedDocument = await documentManager.clone(document.documentId, sanitizedBody, model); const sanitizedDocument = await permissionChecker.sanitizeOutput(clonedDocument); ctx.body = await formatDocumentWithMetadata(permissionChecker, model, sanitizedDocument, { // Empty metadata as it's not relevant for a new document availableLocales: false, availableStatus: false }); }, async autoClone (ctx) { const { model } = ctx.params; // Check if the model has fields that prevent auto cloning const prohibitedFields = getProhibitedCloningFields(model); if (prohibitedFields.length > 0) { return ctx.badRequest('Entity could not be cloned as it has unique and/or relational fields. ' + 'Please edit those fields manually and save to complete the cloning.', { prohibitedFields }); } await this.clone(ctx); }, async delete (ctx) { const { userAbility } = ctx.state; const { id, model } = ctx.params; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.delete()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.delete(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); const { locale } = await getDocumentLocaleAndStatus(ctx.query, model); // Find locales to delete const documentLocales = await documentManager.findLocales(id, model, { populate, locale }); if (documentLocales.length === 0) { return ctx.notFound(); } for (const document of documentLocales){ if (permissionChecker.cannot.delete(document)) { return ctx.forbidden(); } } const result = await documentManager.delete(id, model, { locale }); ctx.body = await permissionChecker.sanitizeOutput(result); }, /** * Publish a document version. * Supports creating/saving a document and publishing it in one request. */ async publish (ctx) { const { userAbility } = ctx.state; // If id does not exist, the document has to be created const { id, model } = ctx.params; const { body } = ctx.request; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.publish()) { return ctx.forbidden(); } const publishedDocument = await strapi.db.transaction(async ()=>{ // Create or update document const permissionQuery = await permissionChecker.sanitizedQuery.publish(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).populateDeep(Infinity).countRelations().withPopulateOverride(getPopulateForLocalizations(model)).build(); let document; const { locale } = await getDocumentLocaleAndStatus(body, model); /** * Publish can be called on two scenarios: * 1. Create a new document and publish it in one request * 2. Update an existing document and publish it in one request * * Based on user permissions: * 1. User cannot create a document, but can publish * Action will be forbidden as user cannot create a document * 2. User can update and publish a document * Action will be allowed, but document will not be updated, only published with the latest draft */ const isCreate = isNil(id); if (isCreate) { if (permissionChecker.cannot.create()) { throw new errors.ForbiddenError(); } document = await createDocument(ctx, { populate }); } const isUpdate = !isCreate; if (isUpdate) { // check if the document exists const documentExists = await documentManager.exists(model, id); if (!documentExists) { throw new errors.NotFoundError('Document not found'); } // check the document version document = await documentManager.findOne(id, model, { populate, locale }); if (!document) { // update and publish the new version if (permissionChecker.cannot.create({ locale }) || permissionChecker.cannot.publish({ locale })) { throw new errors.ForbiddenError(); } document = await updateDocument(ctx); } else if (permissionChecker.can.update(document)) { await updateDocument(ctx); } } if (permissionChecker.cannot.publish(document)) { throw new errors.ForbiddenError(); } const publishResult = await documentManager.publish(document.documentId, model, { locale }); if (!publishResult || publishResult.length === 0) { throw new errors.NotFoundError('Document not found or already published.'); } return publishResult[0]; }); const sanitizedDocument = await permissionChecker.sanitizeOutput(publishedDocument); ctx.body = await formatDocumentWithMetadata(permissionChecker, model, sanitizedDocument); }, async bulkFindForValidation (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const { documentIds, locale, sort } = ctx.request.body; await validateBulkActionInput(ctx.request.body); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.read()) { return ctx.forbidden(); } const populate = await buildDeepPopulate(model); const documents = await strapi.documents(model).findMany({ populate, filters: { documentId: { $in: documentIds } }, locale, sort, status: 'draft' }); const results = await Promise.all(documents.map(permissionChecker.sanitizeOutput)); ctx.body = { results }; }, async bulkPublish (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const { body } = ctx.request; const { documentIds } = body; await validateBulkActionInput(body); const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.publish()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.publish(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).populateDeep(Infinity).countRelations().build(); const { locale } = await getDocumentLocaleAndStatus(body, model, { allowMultipleLocales: true }); const entities = await documentManager.findLocales(documentIds, model, { populate, locale, isPublished: false }); for (const entity of entities){ if (!entity) { return ctx.notFound(); } if (permissionChecker.cannot.publish(entity)) { return ctx.forbidden(); } } const count = await documentManager.publishMany(model, documentIds, locale); ctx.body = { count }; }, async bulkUnpublish (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const { body } = ctx.request; const { documentIds } = body; await validateBulkActionInput(body); const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.unpublish()) { return ctx.forbidden(); } const { locale } = await getDocumentLocaleAndStatus(body, model, { allowMultipleLocales: true }); const entities = await documentManager.findLocales(documentIds, model, { locale, isPublished: true }); for (const entity of entities){ if (!entity) { return ctx.notFound(); } if (permissionChecker.cannot.publish(entity)) { return ctx.forbidden(); } } const entitiesIds = entities.map((document)=>document.documentId); const { count } = await documentManager.unpublishMany(entitiesIds, model, { locale }); ctx.body = { count }; }, async unpublish (ctx) { const { userAbility } = ctx.state; const { id, model } = ctx.params; const { body: { discardDraft, ...body } } = ctx.request; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.unpublish()) { return ctx.forbidden(); } if (discardDraft && permissionChecker.cannot.discard()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.unpublish(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); // TODO allow multiple locales for bulk locale unpublish const { locale } = await getDocumentLocaleAndStatus(body, model); const document = await documentManager.findOne(id, model, { populate, locale, status: 'published' }); if (!document) { throw new errors.NotFoundError(); } if (permissionChecker.cannot.unpublish(document)) { throw new errors.ForbiddenError(); } if (discardDraft && permissionChecker.cannot.discard(document)) { throw new errors.ForbiddenError(); } await strapi.db.transaction(async ()=>{ if (discardDraft) { await documentManager.discardDraft(document.documentId, model, { locale }); } ctx.body = await async.pipe((document)=>documentManager.unpublish(document.documentId, model, { locale }), permissionChecker.sanitizeOutput, (document)=>formatDocumentWithMetadata(permissionChecker, model, document))(document); }); }, async discard (ctx) { const { userAbility } = ctx.state; const { id, model } = ctx.params; const { body } = ctx.request; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.discard()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.discard(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); const { locale } = await getDocumentLocaleAndStatus(body, model); const document = await documentManager.findOne(id, model, { populate, locale, status: 'published' }); // Can not discard a document that is not published if (!document) { return ctx.notFound(); } if (permissionChecker.cannot.discard(document)) { return ctx.forbidden(); } ctx.body = await async.pipe((document)=>documentManager.discardDraft(document.documentId, model, { locale }), permissionChecker.sanitizeOutput, (document)=>formatDocumentWithMetadata(permissionChecker, model, document))(document); }, async bulkDelete (ctx) { const { userAbility } = ctx.state; const { model } = ctx.params; const { query, body } = ctx.request; const { documentIds } = body; await validateBulkActionInput(body); const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.delete()) { return ctx.forbidden(); } const permissionQuery = await permissionChecker.sanitizedQuery.delete(query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); const { locale } = await getDocumentLocaleAndStatus(body, model); const documentLocales = await documentManager.findLocales(documentIds, model, { populate, locale }); if (documentLocales.length === 0) { return ctx.notFound(); } for (const document of documentLocales){ if (permissionChecker.cannot.delete(document)) { return ctx.forbidden(); } } // We filter out documentsIds that maybe doesn't exist in a specific locale. // With draft & publish, findLocales returns a row per publication state, so the // same documentId can appear twice (draft + published). Deduplicate to avoid // deleting (and running document service middleware for) the same document twice. const localeDocumentsIds = [ ...new Set(documentLocales.map((document)=>document.documentId)) ]; const { count } = await documentManager.deleteMany(localeDocumentsIds, model, { locale }); ctx.body = { count }; }, async countDraftRelations (ctx) { const { userAbility } = ctx.state; const { model, id } = ctx.params; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.read()) { return ctx.forbidden(); } const { locale, status } = await getDocumentLocaleAndStatus(ctx.query, model); if (permissionChecker.requiresEntity.read()) { // Only load what we need for access checks const permissionQuery = await permissionChecker.sanitizedQuery.read(ctx.query); const populate = await getService('populate-builder')(model).populateFromQuery(permissionQuery).build(); const entity = await documentManager.findOne(id, model, { locale, status, populate }); if (!entity) { return ctx.notFound(); } if (permissionChecker.cannot.read(entity)) { return ctx.forbidden(); } } const counts = await documentManager.countDraftRelations(id, model, locale); return { data: counts }; }, async countManyEntriesDraftRelations (ctx) { const { userAbility } = ctx.state; const ids = ctx.request.query.documentIds; const locale = ctx.request.query.locale; const { model } = ctx.params; const documentManager = getService('document-manager'); const permissionChecker = getService('permission-checker').create({ userAbility, model }); if (permissionChecker.cannot.read()) { return ctx.forbidden(); } const count = await strapi.db.query(model).count({ where: { documentId: ids } }); if (count === 0) { return ctx.notFound(); } const number = await documentManager.countManyEntriesDraftRelations(ids, model, locale); return { data: number }; } }; export { collectionTypes as default }; //# sourceMappingURL=collection-types.mjs.map