@strapi/content-type-builder 5.53.0 → 5.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/components/AIChat/hooks/useAIFetch.js +6 -2
- package/dist/admin/components/AIChat/hooks/useAIFetch.js.map +1 -1
- package/dist/admin/components/AIChat/hooks/useAIFetch.mjs +7 -3
- package/dist/admin/components/AIChat/hooks/useAIFetch.mjs.map +1 -1
- package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.js +20 -0
- package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.js.map +1 -0
- package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.mjs +18 -0
- package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.js +83 -59
- package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.js.map +1 -1
- package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.mjs +86 -62
- package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.mjs.map +1 -1
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.js +112 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.mjs +110 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.js +159 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.mjs +157 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.js +311 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.mjs +309 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.js +379 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.mjs +376 -0
- package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.js +172 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.mjs +166 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.js +62 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.mjs +60 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.js +145 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.mjs +143 -0
- package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.js +176 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.mjs +169 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.js +277 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.mjs +273 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.mjs.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.js +62 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.js.map +1 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.mjs +58 -0
- package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.mjs.map +1 -0
- package/dist/admin/components/DataManager/DataManagerContext.js.map +1 -1
- package/dist/admin/components/DataManager/DataManagerContext.mjs.map +1 -1
- package/dist/admin/components/DataManager/DataManagerProvider.js +70 -8
- package/dist/admin/components/DataManager/DataManagerProvider.js.map +1 -1
- package/dist/admin/components/DataManager/DataManagerProvider.mjs +70 -8
- package/dist/admin/components/DataManager/DataManagerProvider.mjs.map +1 -1
- package/dist/admin/components/DataManager/reducer.js +411 -33
- package/dist/admin/components/DataManager/reducer.js.map +1 -1
- package/dist/admin/components/DataManager/reducer.mjs +411 -33
- package/dist/admin/components/DataManager/reducer.mjs.map +1 -1
- package/dist/admin/components/DataManager/utils/cleanData.js +11 -2
- package/dist/admin/components/DataManager/utils/cleanData.js.map +1 -1
- package/dist/admin/components/DataManager/utils/cleanData.mjs +11 -2
- package/dist/admin/components/DataManager/utils/cleanData.mjs.map +1 -1
- package/dist/admin/components/DataManager/utils/contentStructure.js +150 -0
- package/dist/admin/components/DataManager/utils/contentStructure.js.map +1 -0
- package/dist/admin/components/DataManager/utils/contentStructure.mjs +133 -0
- package/dist/admin/components/DataManager/utils/contentStructure.mjs.map +1 -0
- package/dist/admin/components/FolderSelect.js +200 -0
- package/dist/admin/components/FolderSelect.js.map +1 -0
- package/dist/admin/components/FolderSelect.mjs +198 -0
- package/dist/admin/components/FolderSelect.mjs.map +1 -0
- package/dist/admin/components/FormModal/FormModal.js +23 -2
- package/dist/admin/components/FormModal/FormModal.js.map +1 -1
- package/dist/admin/components/FormModal/FormModal.mjs +23 -2
- package/dist/admin/components/FormModal/FormModal.mjs.map +1 -1
- package/dist/admin/components/FormModal/contentType/contentTypeForm.js +15 -18
- package/dist/admin/components/FormModal/contentType/contentTypeForm.js.map +1 -1
- package/dist/admin/components/FormModal/contentType/contentTypeForm.mjs +15 -18
- package/dist/admin/components/FormModal/contentType/contentTypeForm.mjs.map +1 -1
- package/dist/admin/components/FormModal/reducer.js +6 -0
- package/dist/admin/components/FormModal/reducer.js.map +1 -1
- package/dist/admin/components/FormModal/reducer.mjs +6 -0
- package/dist/admin/components/FormModal/reducer.mjs.map +1 -1
- package/dist/admin/pages/App/index.js +2 -5
- package/dist/admin/pages/App/index.js.map +1 -1
- package/dist/admin/pages/App/index.mjs +2 -5
- package/dist/admin/pages/App/index.mjs.map +1 -1
- package/dist/admin/src/components/AIChat/hooks/usePrefetchAIToken.d.ts +1 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.d.ts +16 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.d.ts +12 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.d.ts +14 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.d.ts +38 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/{useContentTypeBuilderMenu.d.ts → hooks/useContentTypeBuilderMenu.d.ts} +17 -7
- package/dist/admin/src/components/ContentTypeBuilderNav/hooks/useFolderActions.d.ts +42 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/hooks/useSortableTree.d.ts +53 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/lib/buildFolderTree.d.ts +42 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/lib/dropProjection.d.ts +58 -0
- package/dist/admin/src/components/ContentTypeBuilderNav/lib/flatModel.d.ts +18 -0
- package/dist/admin/src/components/DataManager/DataManagerContext.d.ts +41 -0
- package/dist/admin/src/components/DataManager/reducer.d.ts +105 -1
- package/dist/admin/src/components/DataManager/utils/cleanData.d.ts +5 -1
- package/dist/admin/src/components/DataManager/utils/contentStructure.d.ts +68 -0
- package/dist/admin/src/components/FolderSelect.d.ts +18 -0
- package/dist/admin/src/components/FormModal/contentType/contentTypeForm.d.ts +0 -21
- package/dist/admin/src/components/FormModal/forms/forms.d.ts +0 -10
- package/dist/admin/src/components/FormModal/reducer.d.ts +7 -0
- package/dist/admin/src/utils/folderTelemetry.d.ts +5 -0
- package/dist/admin/translations/en.json.js +23 -0
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +23 -0
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/admin/utils/folderTelemetry.js +19 -0
- package/dist/admin/utils/folderTelemetry.js.map +1 -0
- package/dist/admin/utils/folderTelemetry.mjs +16 -0
- package/dist/admin/utils/folderTelemetry.mjs.map +1 -0
- package/dist/server/controllers/schema.js +1 -1
- package/dist/server/controllers/schema.js.map +1 -1
- package/dist/server/controllers/schema.mjs +1 -1
- package/dist/server/controllers/schema.mjs.map +1 -1
- package/dist/server/controllers/validation/common.js +3 -0
- package/dist/server/controllers/validation/common.js.map +1 -1
- package/dist/server/controllers/validation/common.mjs +3 -1
- package/dist/server/controllers/validation/common.mjs.map +1 -1
- package/dist/server/controllers/validation/content-structure.js +177 -0
- package/dist/server/controllers/validation/content-structure.js.map +1 -0
- package/dist/server/controllers/validation/content-structure.mjs +173 -0
- package/dist/server/controllers/validation/content-structure.mjs.map +1 -0
- package/dist/server/controllers/validation/schema.js +3 -1
- package/dist/server/controllers/validation/schema.js.map +1 -1
- package/dist/server/controllers/validation/schema.mjs +3 -1
- package/dist/server/controllers/validation/schema.mjs.map +1 -1
- package/dist/server/register.js +1 -1
- package/dist/server/register.js.map +1 -1
- package/dist/server/register.mjs +1 -1
- package/dist/server/register.mjs.map +1 -1
- package/dist/server/routes/content-api.js +4 -4
- package/dist/server/routes/content-api.js.map +1 -1
- package/dist/server/routes/content-api.mjs +4 -4
- package/dist/server/routes/content-api.mjs.map +1 -1
- package/dist/server/services/api-handler.js +37 -4
- package/dist/server/services/api-handler.js.map +1 -1
- package/dist/server/services/api-handler.mjs +36 -5
- package/dist/server/services/api-handler.mjs.map +1 -1
- package/dist/server/services/content-structure.js +174 -0
- package/dist/server/services/content-structure.js.map +1 -0
- package/dist/server/services/content-structure.mjs +172 -0
- package/dist/server/services/content-structure.mjs.map +1 -0
- package/dist/server/services/content-types.js +196 -44
- package/dist/server/services/content-types.js.map +1 -1
- package/dist/server/services/content-types.mjs +195 -45
- package/dist/server/services/content-types.mjs.map +1 -1
- package/dist/server/services/index.js +3 -1
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/index.mjs +3 -1
- package/dist/server/services/index.mjs.map +1 -1
- package/dist/server/services/schema-builder/index.js +4 -2
- package/dist/server/services/schema-builder/index.js.map +1 -1
- package/dist/server/services/schema-builder/index.mjs +4 -2
- package/dist/server/services/schema-builder/index.mjs.map +1 -1
- package/dist/server/services/schema-builder/schema-handler.js +19 -17
- package/dist/server/services/schema-builder/schema-handler.js.map +1 -1
- package/dist/server/services/schema-builder/schema-handler.mjs +19 -17
- package/dist/server/services/schema-builder/schema-handler.mjs.map +1 -1
- package/dist/server/services/schema-mutation.js +46 -0
- package/dist/server/services/schema-mutation.js.map +1 -0
- package/dist/server/services/schema-mutation.mjs +43 -0
- package/dist/server/services/schema-mutation.mjs.map +1 -0
- package/dist/server/services/schema.js +134 -79
- package/dist/server/services/schema.js.map +1 -1
- package/dist/server/services/schema.mjs +135 -80
- package/dist/server/services/schema.mjs.map +1 -1
- package/dist/server/src/controllers/schema.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/common.d.ts +1 -0
- package/dist/server/src/controllers/validation/common.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/content-structure.d.ts +56 -0
- package/dist/server/src/controllers/validation/content-structure.d.ts.map +1 -0
- package/dist/server/src/controllers/validation/schema.d.ts +35 -0
- package/dist/server/src/controllers/validation/schema.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +3 -0
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/routes/content-api.d.ts.map +1 -1
- package/dist/server/src/services/api-handler.d.ts +13 -1
- package/dist/server/src/services/api-handler.d.ts.map +1 -1
- package/dist/server/src/services/content-structure.d.ts +37 -0
- package/dist/server/src/services/content-structure.d.ts.map +1 -0
- package/dist/server/src/services/content-types.d.ts +4 -0
- package/dist/server/src/services/content-types.d.ts.map +1 -1
- package/dist/server/src/services/index.d.ts +4 -0
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/services/schema-builder/index.d.ts +3 -1
- package/dist/server/src/services/schema-builder/index.d.ts.map +1 -1
- package/dist/server/src/services/schema-builder/schema-handler.d.ts.map +1 -1
- package/dist/server/src/services/schema-mutation.d.ts +26 -0
- package/dist/server/src/services/schema-mutation.d.ts.map +1 -0
- package/dist/server/src/services/schema.d.ts +1 -0
- package/dist/server/src/services/schema.d.ts.map +1 -1
- package/dist/server/src/utils/index.d.ts +2 -0
- package/dist/server/src/utils/index.d.ts.map +1 -1
- package/dist/server/utils/index.js.map +1 -1
- package/dist/server/utils/index.mjs.map +1 -1
- package/package.json +8 -8
- package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js +0 -157
- package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js.map +0 -1
- package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.mjs +0 -151
- package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-handler.js","sources":["../../../server/src/services/api-handler.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fse from 'fs-extra';\nimport type { Internal } from '@strapi/types';\n\n/**\n * Deletes the API folder of a contentType\n */\nexport async function clear(uid: Internal.UID.ContentType) {\n // TODO double check if this is the correct way to get the apiName\n const { apiName, modelName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n\n await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));\n await deleteBackup(uid);\n}\n\n/**\n * Backups the API folder of a contentType\n * @param {string} uid content type uid\n */\nexport async function backup(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n const
|
|
1
|
+
{"version":3,"file":"api-handler.js","sources":["../../../server/src/services/api-handler.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fse from 'fs-extra';\nimport type { Internal } from '@strapi/types';\n\n/**\n * Deletes the API folder of a contentType\n */\nexport async function clear(\n uid: Internal.UID.ContentType,\n { preserveBackup = false }: { preserveBackup?: boolean } = {}\n) {\n // TODO double check if this is the correct way to get the apiName\n const { apiName, modelName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n\n await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));\n if (!preserveBackup) {\n await deleteBackup(uid);\n }\n}\n\n/**\n * Deletes an inactive rollback backup after the enclosing schema mutation has committed.\n * A failed cleanup leaves only housekeeping residue; it does not alter committed schema, API,\n * or groups.json artifacts.\n */\nexport async function finalize(uid: Internal.UID.ContentType) {\n await deleteBackup(uid);\n}\n\n/**\n * Backups the API folder of a contentType\n * @param {string} uid content type uid\n */\nexport async function backup(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n const backupRoot = path.join(strapi.dirs.app.api, '.backup');\n const backupFolder = path.join(backupRoot, apiName);\n const stagingFolder = path.join(backupRoot, `.${apiName}.staging`);\n\n // Never copy into the canonical backup directly: a retained or failed prior attempt could\n // otherwise merge stale files into the next rollback source.\n await fse.ensureDir(backupRoot);\n await fse.remove(stagingFolder);\n\n try {\n await fse.copy(apiFolder, stagingFolder);\n } catch (error) {\n await fse.remove(stagingFolder);\n throw error;\n }\n\n try {\n await fse.remove(backupFolder);\n await fse.move(stagingFolder, backupFolder);\n } catch (error) {\n await fse.remove(stagingFolder);\n throw error;\n }\n}\n\n/**\n * Deletes an API backup folder\n */\nasync function deleteBackup(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const backupFolder = path.join(strapi.dirs.app.api, '.backup');\n const apiBackupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);\n\n await fse.remove(apiBackupFolder);\n\n const list = await fse.readdir(backupFolder);\n if (list.length === 0) {\n await fse.remove(backupFolder);\n }\n}\n\n/**\n * Rollbacks the API folder of a contentType\n */\nexport async function rollback(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n const backupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);\n\n try {\n await fse.access(backupFolder);\n } catch {\n throw new Error('Cannot rollback api that was not backed up');\n }\n\n await fse.remove(apiFolder);\n await fse.copy(backupFolder, apiFolder);\n await deleteBackup(uid);\n}\n\n/**\n * Removes an API skeleton created for a new content type before its schema mutation commits.\n */\nexport async function clearGenerated(apiName: string) {\n await fse.remove(path.join(strapi.dirs.app.api, apiName));\n}\n\n/**\n * Creates a delete function to clear an api folder\n */\nconst createDeleteApiFunction = (baseName: string) => {\n /**\n * Delets a file in an api.\n * Will only update routes.json instead of deleting it if other routes are present\n */\n return async (filePath: string) => {\n const fileName = path.basename(filePath, path.extname(filePath));\n\n const isSchemaFile = filePath.endsWith(`${baseName}/schema.json`);\n if (fileName === baseName || isSchemaFile) {\n return fse.remove(filePath);\n }\n };\n};\n\n/**\n * Deletes a folder recursively using a delete function\n * @param {string} folder folder to delete\n */\nconst recursiveRemoveFiles = async (\n folder: string,\n deleteFn: (file: string) => unknown\n): Promise<void> => {\n const filesName = await fse.readdir(folder);\n\n for (const fileName of filesName) {\n const filePath = path.join(folder, fileName);\n\n const stat = await fse.stat(filePath);\n\n if (stat.isDirectory()) {\n await recursiveRemoveFiles(filePath, deleteFn);\n } else {\n await deleteFn(filePath);\n }\n }\n\n const files = await fse.readdir(folder);\n if (files.length === 0) {\n await fse.remove(folder);\n }\n};\n"],"names":["clear","uid","preserveBackup","apiName","modelName","strapi","contentTypes","apiFolder","path","join","dirs","app","api","recursiveRemoveFiles","createDeleteApiFunction","deleteBackup","finalize","backup","backupRoot","backupFolder","stagingFolder","fse","ensureDir","remove","copy","error","move","apiBackupFolder","list","readdir","length","rollback","access","Error","clearGenerated","baseName","filePath","fileName","basename","extname","isSchemaFile","endsWith","folder","deleteFn","filesName","stat","isDirectory","files"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAIA;;IAGO,eAAeA,KAAAA,CACpBC,GAA6B,EAC7B,EAAEC,cAAAA,GAAiB,KAAK,EAAgC,GAAG,EAAE,EAAA;;IAG7D,MAAM,EAAEC,OAAO,EAAEC,SAAS,EAAE,GAAGC,MAAAA,CAAOC,YAAY,CAACL,GAAAA,CAAI;;AAGvD,IAAA,IAAI,CAACE,OAAAA,EAAS;IAEd,MAAMI,SAAAA,GAAYC,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAET,OAAAA,CAAAA;IAEjD,MAAMU,oBAAAA,CAAqBN,WAAWO,uBAAAA,CAAwBV,SAAAA,CAAAA,CAAAA;AAC9D,IAAA,IAAI,CAACF,cAAAA,EAAgB;AACnB,QAAA,MAAMa,YAAAA,CAAad,GAAAA,CAAAA;AACrB,IAAA;AACF;AAEA;;;;IAKO,eAAee,QAAAA,CAASf,GAA6B,EAAA;AAC1D,IAAA,MAAMc,YAAAA,CAAad,GAAAA,CAAAA;AACrB;AAEA;;;IAIO,eAAegB,MAAAA,CAAOhB,GAA6B,EAAA;AACxD,IAAA,MAAM,EAAEE,OAAO,EAAE,GAAGE,MAAAA,CAAOC,YAAY,CAACL,GAAAA,CAAI;;AAG5C,IAAA,IAAI,CAACE,OAAAA,EAAS;IAEd,MAAMI,SAAAA,GAAYC,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAET,OAAAA,CAAAA;IACjD,MAAMe,UAAAA,GAAaV,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAE,SAAA,CAAA;AAClD,IAAA,MAAMO,YAAAA,GAAeX,eAAAA,CAAKC,IAAI,CAACS,UAAAA,EAAYf,OAAAA,CAAAA;IAC3C,MAAMiB,aAAAA,GAAgBZ,eAAAA,CAAKC,IAAI,CAACS,UAAAA,EAAY,CAAC,CAAC,EAAEf,OAAAA,CAAQ,QAAQ,CAAC,CAAA;;;IAIjE,MAAMkB,cAAAA,CAAIC,SAAS,CAACJ,UAAAA,CAAAA;IACpB,MAAMG,cAAAA,CAAIE,MAAM,CAACH,aAAAA,CAAAA;IAEjB,IAAI;QACF,MAAMC,cAAAA,CAAIG,IAAI,CAACjB,SAAAA,EAAWa,aAAAA,CAAAA;AAC5B,IAAA,CAAA,CAAE,OAAOK,KAAAA,EAAO;QACd,MAAMJ,cAAAA,CAAIE,MAAM,CAACH,aAAAA,CAAAA;QACjB,MAAMK,KAAAA;AACR,IAAA;IAEA,IAAI;QACF,MAAMJ,cAAAA,CAAIE,MAAM,CAACJ,YAAAA,CAAAA;QACjB,MAAME,cAAAA,CAAIK,IAAI,CAACN,aAAAA,EAAeD,YAAAA,CAAAA;AAChC,IAAA,CAAA,CAAE,OAAOM,KAAAA,EAAO;QACd,MAAMJ,cAAAA,CAAIE,MAAM,CAACH,aAAAA,CAAAA;QACjB,MAAMK,KAAAA;AACR,IAAA;AACF;AAEA;;IAGA,eAAeV,aAAad,GAA6B,EAAA;AACvD,IAAA,MAAM,EAAEE,OAAO,EAAE,GAAGE,MAAAA,CAAOC,YAAY,CAACL,GAAAA,CAAI;;AAG5C,IAAA,IAAI,CAACE,OAAAA,EAAS;IAEd,MAAMgB,YAAAA,GAAeX,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAE,SAAA,CAAA;IACpD,MAAMe,eAAAA,GAAkBnB,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAE,SAAA,EAAWT,OAAAA,CAAAA;IAElE,MAAMkB,cAAAA,CAAIE,MAAM,CAACI,eAAAA,CAAAA;AAEjB,IAAA,MAAMC,IAAAA,GAAO,MAAMP,cAAAA,CAAIQ,OAAO,CAACV,YAAAA,CAAAA;IAC/B,IAAIS,IAAAA,CAAKE,MAAM,KAAK,CAAA,EAAG;QACrB,MAAMT,cAAAA,CAAIE,MAAM,CAACJ,YAAAA,CAAAA;AACnB,IAAA;AACF;AAEA;;IAGO,eAAeY,QAAAA,CAAS9B,GAA6B,EAAA;AAC1D,IAAA,MAAM,EAAEE,OAAO,EAAE,GAAGE,MAAAA,CAAOC,YAAY,CAACL,GAAAA,CAAI;;AAG5C,IAAA,IAAI,CAACE,OAAAA,EAAS;IAEd,MAAMI,SAAAA,GAAYC,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAET,OAAAA,CAAAA;IACjD,MAAMgB,YAAAA,GAAeX,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAE,SAAA,EAAWT,OAAAA,CAAAA;IAE/D,IAAI;QACF,MAAMkB,cAAAA,CAAIW,MAAM,CAACb,YAAAA,CAAAA;AACnB,IAAA,CAAA,CAAE,OAAM;AACN,QAAA,MAAM,IAAIc,KAAAA,CAAM,4CAAA,CAAA;AAClB,IAAA;IAEA,MAAMZ,cAAAA,CAAIE,MAAM,CAAChB,SAAAA,CAAAA;IACjB,MAAMc,cAAAA,CAAIG,IAAI,CAACL,YAAAA,EAAcZ,SAAAA,CAAAA;AAC7B,IAAA,MAAMQ,YAAAA,CAAad,GAAAA,CAAAA;AACrB;AAEA;;IAGO,eAAeiC,cAAAA,CAAe/B,OAAe,EAAA;AAClD,IAAA,MAAMkB,cAAAA,CAAIE,MAAM,CAACf,eAAAA,CAAKC,IAAI,CAACJ,MAAAA,CAAOK,IAAI,CAACC,GAAG,CAACC,GAAG,EAAET,OAAAA,CAAAA,CAAAA;AAClD;AAEA;;IAGA,MAAMW,0BAA0B,CAACqB,QAAAA,GAAAA;AAC/B;;;AAGC,MACD,OAAO,OAAOC,QAAAA,GAAAA;AACZ,QAAA,MAAMC,WAAW7B,eAAAA,CAAK8B,QAAQ,CAACF,QAAAA,EAAU5B,eAAAA,CAAK+B,OAAO,CAACH,QAAAA,CAAAA,CAAAA;AAEtD,QAAA,MAAMI,eAAeJ,QAAAA,CAASK,QAAQ,CAAC,CAAA,EAAGN,QAAAA,CAAS,YAAY,CAAC,CAAA;QAChE,IAAIE,QAAAA,KAAaF,YAAYK,YAAAA,EAAc;YACzC,OAAOnB,cAAAA,CAAIE,MAAM,CAACa,QAAAA,CAAAA;AACpB,QAAA;AACF,IAAA,CAAA;AACF,CAAA;AAEA;;;IAIA,MAAMvB,oBAAAA,GAAuB,OAC3B6B,MAAAA,EACAC,QAAAA,GAAAA;AAEA,IAAA,MAAMC,SAAAA,GAAY,MAAMvB,cAAAA,CAAIQ,OAAO,CAACa,MAAAA,CAAAA;IAEpC,KAAK,MAAML,YAAYO,SAAAA,CAAW;AAChC,QAAA,MAAMR,QAAAA,GAAW5B,eAAAA,CAAKC,IAAI,CAACiC,MAAAA,EAAQL,QAAAA,CAAAA;AAEnC,QAAA,MAAMQ,IAAAA,GAAO,MAAMxB,cAAAA,CAAIwB,IAAI,CAACT,QAAAA,CAAAA;QAE5B,IAAIS,IAAAA,CAAKC,WAAW,EAAA,EAAI;AACtB,YAAA,MAAMjC,qBAAqBuB,QAAAA,EAAUO,QAAAA,CAAAA;QACvC,CAAA,MAAO;AACL,YAAA,MAAMA,QAAAA,CAASP,QAAAA,CAAAA;AACjB,QAAA;AACF,IAAA;AAEA,IAAA,MAAMW,KAAAA,GAAQ,MAAM1B,cAAAA,CAAIQ,OAAO,CAACa,MAAAA,CAAAA;IAChC,IAAIK,KAAAA,CAAMjB,MAAM,KAAK,CAAA,EAAG;QACtB,MAAMT,cAAAA,CAAIE,MAAM,CAACmB,MAAAA,CAAAA;AACnB,IAAA;AACF,CAAA;;;;;;;;"}
|
|
@@ -3,13 +3,22 @@ import * as fse from 'fs-extra';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Deletes the API folder of a contentType
|
|
6
|
-
*/ async function clear(uid) {
|
|
6
|
+
*/ async function clear(uid, { preserveBackup = false } = {}) {
|
|
7
7
|
// TODO double check if this is the correct way to get the apiName
|
|
8
8
|
const { apiName, modelName } = strapi.contentTypes[uid];
|
|
9
9
|
// Opt out if the content type is not linked to an API (e.g. plugin content type)
|
|
10
10
|
if (!apiName) return;
|
|
11
11
|
const apiFolder = path.join(strapi.dirs.app.api, apiName);
|
|
12
12
|
await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));
|
|
13
|
+
if (!preserveBackup) {
|
|
14
|
+
await deleteBackup(uid);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deletes an inactive rollback backup after the enclosing schema mutation has committed.
|
|
19
|
+
* A failed cleanup leaves only housekeeping residue; it does not alter committed schema, API,
|
|
20
|
+
* or groups.json artifacts.
|
|
21
|
+
*/ async function finalize(uid) {
|
|
13
22
|
await deleteBackup(uid);
|
|
14
23
|
}
|
|
15
24
|
/**
|
|
@@ -20,9 +29,26 @@ import * as fse from 'fs-extra';
|
|
|
20
29
|
// Opt out if the content type is not linked to an API (e.g. plugin content type)
|
|
21
30
|
if (!apiName) return;
|
|
22
31
|
const apiFolder = path.join(strapi.dirs.app.api, apiName);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
const backupRoot = path.join(strapi.dirs.app.api, '.backup');
|
|
33
|
+
const backupFolder = path.join(backupRoot, apiName);
|
|
34
|
+
const stagingFolder = path.join(backupRoot, `.${apiName}.staging`);
|
|
35
|
+
// Never copy into the canonical backup directly: a retained or failed prior attempt could
|
|
36
|
+
// otherwise merge stale files into the next rollback source.
|
|
37
|
+
await fse.ensureDir(backupRoot);
|
|
38
|
+
await fse.remove(stagingFolder);
|
|
39
|
+
try {
|
|
40
|
+
await fse.copy(apiFolder, stagingFolder);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
await fse.remove(stagingFolder);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await fse.remove(backupFolder);
|
|
47
|
+
await fse.move(stagingFolder, backupFolder);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await fse.remove(stagingFolder);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
26
52
|
}
|
|
27
53
|
/**
|
|
28
54
|
* Deletes an API backup folder
|
|
@@ -55,6 +81,11 @@ import * as fse from 'fs-extra';
|
|
|
55
81
|
await fse.copy(backupFolder, apiFolder);
|
|
56
82
|
await deleteBackup(uid);
|
|
57
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Removes an API skeleton created for a new content type before its schema mutation commits.
|
|
86
|
+
*/ async function clearGenerated(apiName) {
|
|
87
|
+
await fse.remove(path.join(strapi.dirs.app.api, apiName));
|
|
88
|
+
}
|
|
58
89
|
/**
|
|
59
90
|
* Creates a delete function to clear an api folder
|
|
60
91
|
*/ const createDeleteApiFunction = (baseName)=>{
|
|
@@ -89,5 +120,5 @@ import * as fse from 'fs-extra';
|
|
|
89
120
|
}
|
|
90
121
|
};
|
|
91
122
|
|
|
92
|
-
export { backup, clear, rollback };
|
|
123
|
+
export { backup, clear, clearGenerated, finalize, rollback };
|
|
93
124
|
//# sourceMappingURL=api-handler.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-handler.mjs","sources":["../../../server/src/services/api-handler.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fse from 'fs-extra';\nimport type { Internal } from '@strapi/types';\n\n/**\n * Deletes the API folder of a contentType\n */\nexport async function clear(uid: Internal.UID.ContentType) {\n // TODO double check if this is the correct way to get the apiName\n const { apiName, modelName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n\n await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));\n await deleteBackup(uid);\n}\n\n/**\n * Backups the API folder of a contentType\n * @param {string} uid content type uid\n */\nexport async function backup(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n const
|
|
1
|
+
{"version":3,"file":"api-handler.mjs","sources":["../../../server/src/services/api-handler.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fse from 'fs-extra';\nimport type { Internal } from '@strapi/types';\n\n/**\n * Deletes the API folder of a contentType\n */\nexport async function clear(\n uid: Internal.UID.ContentType,\n { preserveBackup = false }: { preserveBackup?: boolean } = {}\n) {\n // TODO double check if this is the correct way to get the apiName\n const { apiName, modelName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n\n await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));\n if (!preserveBackup) {\n await deleteBackup(uid);\n }\n}\n\n/**\n * Deletes an inactive rollback backup after the enclosing schema mutation has committed.\n * A failed cleanup leaves only housekeeping residue; it does not alter committed schema, API,\n * or groups.json artifacts.\n */\nexport async function finalize(uid: Internal.UID.ContentType) {\n await deleteBackup(uid);\n}\n\n/**\n * Backups the API folder of a contentType\n * @param {string} uid content type uid\n */\nexport async function backup(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n const backupRoot = path.join(strapi.dirs.app.api, '.backup');\n const backupFolder = path.join(backupRoot, apiName);\n const stagingFolder = path.join(backupRoot, `.${apiName}.staging`);\n\n // Never copy into the canonical backup directly: a retained or failed prior attempt could\n // otherwise merge stale files into the next rollback source.\n await fse.ensureDir(backupRoot);\n await fse.remove(stagingFolder);\n\n try {\n await fse.copy(apiFolder, stagingFolder);\n } catch (error) {\n await fse.remove(stagingFolder);\n throw error;\n }\n\n try {\n await fse.remove(backupFolder);\n await fse.move(stagingFolder, backupFolder);\n } catch (error) {\n await fse.remove(stagingFolder);\n throw error;\n }\n}\n\n/**\n * Deletes an API backup folder\n */\nasync function deleteBackup(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const backupFolder = path.join(strapi.dirs.app.api, '.backup');\n const apiBackupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);\n\n await fse.remove(apiBackupFolder);\n\n const list = await fse.readdir(backupFolder);\n if (list.length === 0) {\n await fse.remove(backupFolder);\n }\n}\n\n/**\n * Rollbacks the API folder of a contentType\n */\nexport async function rollback(uid: Internal.UID.ContentType) {\n const { apiName } = strapi.contentTypes[uid] as any;\n\n // Opt out if the content type is not linked to an API (e.g. plugin content type)\n if (!apiName) return;\n\n const apiFolder = path.join(strapi.dirs.app.api, apiName);\n const backupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);\n\n try {\n await fse.access(backupFolder);\n } catch {\n throw new Error('Cannot rollback api that was not backed up');\n }\n\n await fse.remove(apiFolder);\n await fse.copy(backupFolder, apiFolder);\n await deleteBackup(uid);\n}\n\n/**\n * Removes an API skeleton created for a new content type before its schema mutation commits.\n */\nexport async function clearGenerated(apiName: string) {\n await fse.remove(path.join(strapi.dirs.app.api, apiName));\n}\n\n/**\n * Creates a delete function to clear an api folder\n */\nconst createDeleteApiFunction = (baseName: string) => {\n /**\n * Delets a file in an api.\n * Will only update routes.json instead of deleting it if other routes are present\n */\n return async (filePath: string) => {\n const fileName = path.basename(filePath, path.extname(filePath));\n\n const isSchemaFile = filePath.endsWith(`${baseName}/schema.json`);\n if (fileName === baseName || isSchemaFile) {\n return fse.remove(filePath);\n }\n };\n};\n\n/**\n * Deletes a folder recursively using a delete function\n * @param {string} folder folder to delete\n */\nconst recursiveRemoveFiles = async (\n folder: string,\n deleteFn: (file: string) => unknown\n): Promise<void> => {\n const filesName = await fse.readdir(folder);\n\n for (const fileName of filesName) {\n const filePath = path.join(folder, fileName);\n\n const stat = await fse.stat(filePath);\n\n if (stat.isDirectory()) {\n await recursiveRemoveFiles(filePath, deleteFn);\n } else {\n await deleteFn(filePath);\n }\n }\n\n const files = await fse.readdir(folder);\n if (files.length === 0) {\n await fse.remove(folder);\n }\n};\n"],"names":["clear","uid","preserveBackup","apiName","modelName","strapi","contentTypes","apiFolder","path","join","dirs","app","api","recursiveRemoveFiles","createDeleteApiFunction","deleteBackup","finalize","backup","backupRoot","backupFolder","stagingFolder","fse","ensureDir","remove","copy","error","move","apiBackupFolder","list","readdir","length","rollback","access","Error","clearGenerated","baseName","filePath","fileName","basename","extname","isSchemaFile","endsWith","folder","deleteFn","filesName","stat","isDirectory","files"],"mappings":"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"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils = require('@strapi/utils');
|
|
4
|
+
|
|
5
|
+
const { ApplicationError } = utils.errors;
|
|
6
|
+
const SECTION_KEYS = [
|
|
7
|
+
'collectionTypes',
|
|
8
|
+
'singleTypes'
|
|
9
|
+
];
|
|
10
|
+
const expectedKindFor = (sectionKey)=>{
|
|
11
|
+
return sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';
|
|
12
|
+
};
|
|
13
|
+
const isRecord = (value)=>{
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Formats a validated ContentStructureFile into a complete `groups.json` file.
|
|
18
|
+
*/ const formatContentStructureObjectAsFile = (structure)=>{
|
|
19
|
+
const sections = isRecord(structure) && isRecord(structure.sections) ? structure.sections : {};
|
|
20
|
+
// Supplies all record groups with expected fields, so that the reference validator
|
|
21
|
+
// can access `children`/`id` unconditionally.
|
|
22
|
+
const coerceGroup = (raw)=>{
|
|
23
|
+
const childrenArray = Array.isArray(raw.children) ? raw.children : [];
|
|
24
|
+
return {
|
|
25
|
+
parent: typeof raw.parent === 'string' ? raw.parent : null,
|
|
26
|
+
name: typeof raw.name === 'string' ? raw.name : '',
|
|
27
|
+
id: typeof raw.id === 'string' ? raw.id : '',
|
|
28
|
+
children: childrenArray
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const coerceSection = (key)=>{
|
|
32
|
+
const raw = sections[key];
|
|
33
|
+
const groups = isRecord(raw) && Array.isArray(raw.groups) ? raw.groups : [];
|
|
34
|
+
return {
|
|
35
|
+
groups: groups.filter(isRecord).map(coerceGroup)
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
version: 1,
|
|
40
|
+
sections: {
|
|
41
|
+
collectionTypes: coerceSection('collectionTypes'),
|
|
42
|
+
singleTypes: coerceSection('singleTypes')
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const cloneFile = (file)=>({
|
|
47
|
+
version: 1,
|
|
48
|
+
sections: {
|
|
49
|
+
collectionTypes: {
|
|
50
|
+
groups: file.sections.collectionTypes.groups.map((group)=>({
|
|
51
|
+
...group,
|
|
52
|
+
children: [
|
|
53
|
+
...group.children
|
|
54
|
+
]
|
|
55
|
+
}))
|
|
56
|
+
},
|
|
57
|
+
singleTypes: {
|
|
58
|
+
groups: file.sections.singleTypes.groups.map((group)=>({
|
|
59
|
+
...group,
|
|
60
|
+
children: [
|
|
61
|
+
...group.children
|
|
62
|
+
]
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
function createContentStructureService(strapi) {
|
|
68
|
+
const contentStructureService = strapi.get('content-structure');
|
|
69
|
+
const warn = (message)=>{
|
|
70
|
+
strapi.log.warn(`[content-structure] ${message}`);
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Builds an effective set of content type uids and their kind by resolving the pre-transaction content type registry with the list of upserted and deleted uids.
|
|
74
|
+
*/ const buildEffectiveUidKindSet = (upsertedUids, deletedUids)=>{
|
|
75
|
+
const effective = new Map();
|
|
76
|
+
for (const [uid, contentType] of Object.entries(strapi.contentTypes)){
|
|
77
|
+
effective.set(uid, contentType.kind === 'singleType' ? 'singleType' : 'collectionType');
|
|
78
|
+
}
|
|
79
|
+
for (const uid of deletedUids){
|
|
80
|
+
effective.delete(uid);
|
|
81
|
+
}
|
|
82
|
+
for (const [uid, kind] of upsertedUids){
|
|
83
|
+
effective.set(uid, kind);
|
|
84
|
+
}
|
|
85
|
+
return effective;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Remove references to content types that have been deleted
|
|
89
|
+
*/ const pruneStructure = (file, deletedUids)=>{
|
|
90
|
+
const cloned = cloneFile(file);
|
|
91
|
+
for (const sectionKey of SECTION_KEYS){
|
|
92
|
+
for (const group of cloned.sections[sectionKey].groups){
|
|
93
|
+
group.children = group.children.filter((child)=>{
|
|
94
|
+
if (child.type !== 'contentType') {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const { uid } = child;
|
|
98
|
+
if (deletedUids.has(uid)) {
|
|
99
|
+
warn(`Pruned deleted content type "${uid}" from group "${group.id}"`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return cloned;
|
|
107
|
+
};
|
|
108
|
+
const validateContentTypeUidReferences = (structure, contentTypeKinds)=>{
|
|
109
|
+
const file = formatContentStructureObjectAsFile(structure);
|
|
110
|
+
const violations = [];
|
|
111
|
+
for (const sectionKey of SECTION_KEYS){
|
|
112
|
+
const expectedKind = expectedKindFor(sectionKey);
|
|
113
|
+
for (const group of file.sections[sectionKey].groups){
|
|
114
|
+
for (const child of group.children){
|
|
115
|
+
if (child.type !== 'contentType') {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const { uid } = child;
|
|
119
|
+
// Referenced content type exists (in the effective set).
|
|
120
|
+
if (!contentTypeKinds.has(uid)) {
|
|
121
|
+
violations.push(`Content type "${uid}" in group "${group.id}" does not exist in section "${sectionKey}"`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Kind matches section.
|
|
125
|
+
if (contentTypeKinds.get(uid) !== expectedKind) {
|
|
126
|
+
violations.push(`Content type "${uid}" in group "${group.id}" is a ${contentTypeKinds.get(uid)} and cannot be placed in section "${sectionKey}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (violations.length > 0) {
|
|
132
|
+
throw new ApplicationError(`Invalid content structure:\n- ${violations.join('\n- ')}`, {
|
|
133
|
+
errors: violations.map((message)=>({
|
|
134
|
+
name: 'ApplicationError',
|
|
135
|
+
path: [],
|
|
136
|
+
message
|
|
137
|
+
}))
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const validateFromUpdate = ({ incomingStructure, upsertedUids, deletedUids })=>{
|
|
142
|
+
if (incomingStructure === undefined || incomingStructure === null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const effectiveKinds = buildEffectiveUidKindSet(upsertedUids, deletedUids);
|
|
146
|
+
const pruned = pruneStructure(formatContentStructureObjectAsFile(incomingStructure), deletedUids);
|
|
147
|
+
validateContentTypeUidReferences(pruned, effectiveKinds);
|
|
148
|
+
};
|
|
149
|
+
const commitFromUpdate = async ({ incomingStructure, deletedUids })=>{
|
|
150
|
+
if (incomingStructure !== undefined && incomingStructure !== null) {
|
|
151
|
+
const pruned = pruneStructure(formatContentStructureObjectAsFile(incomingStructure), deletedUids);
|
|
152
|
+
await contentStructureService.write(pruned);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (deletedUids.size > 0) {
|
|
156
|
+
const current = await contentStructureService.getCleanedFile();
|
|
157
|
+
if (!current) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const pruned = pruneStructure(current, deletedUids);
|
|
161
|
+
await contentStructureService.write(pruned);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
validateContentTypeUidReferences,
|
|
168
|
+
validateFromUpdate,
|
|
169
|
+
commitFromUpdate
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
exports.createContentStructureService = createContentStructureService;
|
|
174
|
+
//# sourceMappingURL=content-structure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-structure.js","sources":["../../../server/src/services/content-structure.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\n\nimport type { Core, Modules } from '@strapi/types';\n\nconst { ApplicationError } = errors;\n\ntype ContentStructureGroup = Modules.ContentStructure.ContentStructureGroup;\ntype ContentStructureFile = Modules.ContentStructure.ContentStructureFile;\ntype SectionKey = Modules.ContentStructure.ContentStructureSectionKey;\ntype ContentTypeKind = 'collectionType' | 'singleType';\n\nconst SECTION_KEYS: SectionKey[] = ['collectionTypes', 'singleTypes'];\n\nconst expectedKindFor = (sectionKey: SectionKey): ContentTypeKind => {\n return sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';\n};\n\n/**\n * A mirror of the subset of the core `content-structure` service (strapi.get('content-structure'))\n * that this service consumes.\n */\nexport interface CoreContentStructureService {\n getCleanedFile(): Promise<ContentStructureFile | null>;\n write(structure: ContentStructureFile): Promise<void>;\n}\n\n/**\n * This service handles the CTB orchestration of folder-group WRITES. It handles:\n * - Context-aware validation that requires the content-type registry\n * - Context-aware pruning of references invalidated by CTB transaction.\n * - Persistence to filesystem via the core content-structure service.\n * It assumes that non-context-aware validation has already been performed.\n */\nexport interface ContentTypeBuilderContentStructureService {\n /**\n * Validates all references to content types in the provided content structure.\n * Throws an ApplicationError naming every offending uid + rule.\n */\n validateContentTypeUidReferences(\n structure: unknown,\n contentTypeUids: Map<string, ContentTypeKind>\n ): void;\n\n validateFromUpdate(input: {\n incomingStructure?: unknown;\n upsertedUids: Map<string, ContentTypeKind>;\n deletedUids: Set<string>;\n }): void;\n\n commitFromUpdate(input: {\n incomingStructure?: unknown;\n deletedUids: Set<string>;\n }): Promise<boolean>;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n};\n\n/**\n * Formats a validated ContentStructureFile into a complete `groups.json` file.\n */\nconst formatContentStructureObjectAsFile = (structure: unknown): ContentStructureFile => {\n const sections = isRecord(structure) && isRecord(structure.sections) ? structure.sections : {};\n\n // Supplies all record groups with expected fields, so that the reference validator\n // can access `children`/`id` unconditionally.\n const coerceGroup = (raw: Record<string, unknown>): ContentStructureGroup => {\n const childrenArray = (\n Array.isArray(raw.children) ? raw.children : []\n ) as Modules.ContentStructure.ContentStructureChild[];\n\n return {\n parent: typeof raw.parent === 'string' ? raw.parent : null,\n name: typeof raw.name === 'string' ? raw.name : '',\n id: typeof raw.id === 'string' ? raw.id : '',\n children: childrenArray,\n };\n };\n\n const coerceSection = (key: SectionKey): Modules.ContentStructure.ContentStructureSection => {\n const raw = (sections as Record<string, unknown>)[key];\n const groups = isRecord(raw) && Array.isArray(raw.groups) ? raw.groups : [];\n\n return { groups: groups.filter(isRecord).map(coerceGroup) };\n };\n\n return {\n version: 1,\n sections: {\n collectionTypes: coerceSection('collectionTypes'),\n singleTypes: coerceSection('singleTypes'),\n },\n };\n};\n\nconst cloneFile = (file: ContentStructureFile): ContentStructureFile => ({\n version: 1,\n sections: {\n collectionTypes: {\n groups: file.sections.collectionTypes.groups.map((group) => ({\n ...group,\n children: [...group.children],\n })),\n },\n singleTypes: {\n groups: file.sections.singleTypes.groups.map((group) => ({\n ...group,\n children: [...group.children],\n })),\n },\n },\n});\n\nexport function createContentStructureService(\n strapi: Core.Strapi\n): ContentTypeBuilderContentStructureService {\n const contentStructureService: CoreContentStructureService = strapi.get('content-structure');\n\n const warn = (message: string) => {\n strapi.log.warn(`[content-structure] ${message}`);\n };\n\n /**\n * Builds an effective set of content type uids and their kind by resolving the pre-transaction content type registry with the list of upserted and deleted uids.\n */\n const buildEffectiveUidKindSet = (\n upsertedUids: Map<string, ContentTypeKind>,\n deletedUids: Set<string>\n ): Map<string, ContentTypeKind> => {\n const effective = new Map<string, ContentTypeKind>();\n\n for (const [uid, contentType] of Object.entries(strapi.contentTypes)) {\n effective.set(uid, contentType.kind === 'singleType' ? 'singleType' : 'collectionType');\n }\n\n for (const uid of deletedUids) {\n effective.delete(uid);\n }\n\n for (const [uid, kind] of upsertedUids) {\n effective.set(uid, kind);\n }\n\n return effective;\n };\n\n /**\n * Remove references to content types that have been deleted\n */\n const pruneStructure = (\n file: ContentStructureFile,\n deletedUids: Set<string>\n ): ContentStructureFile => {\n const cloned = cloneFile(file);\n\n for (const sectionKey of SECTION_KEYS) {\n for (const group of cloned.sections[sectionKey].groups) {\n group.children = group.children.filter((child) => {\n if (child.type !== 'contentType') {\n return true;\n }\n\n const { uid } = child;\n\n if (deletedUids.has(uid)) {\n warn(`Pruned deleted content type \"${uid}\" from group \"${group.id}\"`);\n return false;\n }\n\n return true;\n });\n }\n }\n\n return cloned;\n };\n\n const validateContentTypeUidReferences = (\n structure: unknown,\n contentTypeKinds: Map<string, ContentTypeKind>\n ): void => {\n const file = formatContentStructureObjectAsFile(structure);\n const violations: string[] = [];\n\n for (const sectionKey of SECTION_KEYS) {\n const expectedKind = expectedKindFor(sectionKey);\n\n for (const group of file.sections[sectionKey].groups) {\n for (const child of group.children) {\n if (child.type !== 'contentType') {\n continue;\n }\n\n const { uid } = child;\n\n // Referenced content type exists (in the effective set).\n if (!contentTypeKinds.has(uid)) {\n violations.push(\n `Content type \"${uid}\" in group \"${group.id}\" does not exist in section \"${sectionKey}\"`\n );\n continue;\n }\n\n // Kind matches section.\n if (contentTypeKinds.get(uid) !== expectedKind) {\n violations.push(\n `Content type \"${uid}\" in group \"${group.id}\" is a ${contentTypeKinds.get(\n uid\n )} and cannot be placed in section \"${sectionKey}\"`\n );\n }\n }\n }\n }\n\n if (violations.length > 0) {\n throw new ApplicationError(`Invalid content structure:\\n- ${violations.join('\\n- ')}`, {\n errors: violations.map((message) => ({\n name: 'ApplicationError',\n path: [] as string[],\n message,\n })),\n });\n }\n };\n\n const validateFromUpdate = ({\n incomingStructure,\n upsertedUids,\n deletedUids,\n }: {\n incomingStructure?: unknown;\n upsertedUids: Map<string, ContentTypeKind>;\n deletedUids: Set<string>;\n }): void => {\n if (incomingStructure === undefined || incomingStructure === null) {\n return;\n }\n\n const effectiveKinds = buildEffectiveUidKindSet(upsertedUids, deletedUids);\n const pruned = pruneStructure(\n formatContentStructureObjectAsFile(incomingStructure),\n deletedUids\n );\n\n validateContentTypeUidReferences(pruned, effectiveKinds);\n };\n\n const commitFromUpdate = async ({\n incomingStructure,\n deletedUids,\n }: {\n incomingStructure?: unknown;\n deletedUids: Set<string>;\n }): Promise<boolean> => {\n if (incomingStructure !== undefined && incomingStructure !== null) {\n const pruned = pruneStructure(\n formatContentStructureObjectAsFile(incomingStructure),\n deletedUids\n );\n\n await contentStructureService.write(pruned);\n return true;\n }\n\n if (deletedUids.size > 0) {\n const current = await contentStructureService.getCleanedFile();\n\n if (!current) {\n return false;\n }\n\n const pruned = pruneStructure(current, deletedUids);\n\n await contentStructureService.write(pruned);\n return true;\n }\n\n return false;\n };\n\n return {\n validateContentTypeUidReferences,\n validateFromUpdate,\n commitFromUpdate,\n };\n}\n"],"names":["ApplicationError","errors","SECTION_KEYS","expectedKindFor","sectionKey","isRecord","value","Array","isArray","formatContentStructureObjectAsFile","structure","sections","coerceGroup","raw","childrenArray","children","parent","name","id","coerceSection","key","groups","filter","map","version","collectionTypes","singleTypes","cloneFile","file","group","createContentStructureService","strapi","contentStructureService","get","warn","message","log","buildEffectiveUidKindSet","upsertedUids","deletedUids","effective","Map","uid","contentType","Object","entries","contentTypes","set","kind","delete","pruneStructure","cloned","child","type","has","validateContentTypeUidReferences","contentTypeKinds","violations","expectedKind","push","length","join","path","validateFromUpdate","incomingStructure","undefined","effectiveKinds","pruned","commitFromUpdate","write","size","current","getCleanedFile"],"mappings":"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"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { errors } from '@strapi/utils';
|
|
2
|
+
|
|
3
|
+
const { ApplicationError } = errors;
|
|
4
|
+
const SECTION_KEYS = [
|
|
5
|
+
'collectionTypes',
|
|
6
|
+
'singleTypes'
|
|
7
|
+
];
|
|
8
|
+
const expectedKindFor = (sectionKey)=>{
|
|
9
|
+
return sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';
|
|
10
|
+
};
|
|
11
|
+
const isRecord = (value)=>{
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Formats a validated ContentStructureFile into a complete `groups.json` file.
|
|
16
|
+
*/ const formatContentStructureObjectAsFile = (structure)=>{
|
|
17
|
+
const sections = isRecord(structure) && isRecord(structure.sections) ? structure.sections : {};
|
|
18
|
+
// Supplies all record groups with expected fields, so that the reference validator
|
|
19
|
+
// can access `children`/`id` unconditionally.
|
|
20
|
+
const coerceGroup = (raw)=>{
|
|
21
|
+
const childrenArray = Array.isArray(raw.children) ? raw.children : [];
|
|
22
|
+
return {
|
|
23
|
+
parent: typeof raw.parent === 'string' ? raw.parent : null,
|
|
24
|
+
name: typeof raw.name === 'string' ? raw.name : '',
|
|
25
|
+
id: typeof raw.id === 'string' ? raw.id : '',
|
|
26
|
+
children: childrenArray
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const coerceSection = (key)=>{
|
|
30
|
+
const raw = sections[key];
|
|
31
|
+
const groups = isRecord(raw) && Array.isArray(raw.groups) ? raw.groups : [];
|
|
32
|
+
return {
|
|
33
|
+
groups: groups.filter(isRecord).map(coerceGroup)
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
sections: {
|
|
39
|
+
collectionTypes: coerceSection('collectionTypes'),
|
|
40
|
+
singleTypes: coerceSection('singleTypes')
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const cloneFile = (file)=>({
|
|
45
|
+
version: 1,
|
|
46
|
+
sections: {
|
|
47
|
+
collectionTypes: {
|
|
48
|
+
groups: file.sections.collectionTypes.groups.map((group)=>({
|
|
49
|
+
...group,
|
|
50
|
+
children: [
|
|
51
|
+
...group.children
|
|
52
|
+
]
|
|
53
|
+
}))
|
|
54
|
+
},
|
|
55
|
+
singleTypes: {
|
|
56
|
+
groups: file.sections.singleTypes.groups.map((group)=>({
|
|
57
|
+
...group,
|
|
58
|
+
children: [
|
|
59
|
+
...group.children
|
|
60
|
+
]
|
|
61
|
+
}))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
function createContentStructureService(strapi) {
|
|
66
|
+
const contentStructureService = strapi.get('content-structure');
|
|
67
|
+
const warn = (message)=>{
|
|
68
|
+
strapi.log.warn(`[content-structure] ${message}`);
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Builds an effective set of content type uids and their kind by resolving the pre-transaction content type registry with the list of upserted and deleted uids.
|
|
72
|
+
*/ const buildEffectiveUidKindSet = (upsertedUids, deletedUids)=>{
|
|
73
|
+
const effective = new Map();
|
|
74
|
+
for (const [uid, contentType] of Object.entries(strapi.contentTypes)){
|
|
75
|
+
effective.set(uid, contentType.kind === 'singleType' ? 'singleType' : 'collectionType');
|
|
76
|
+
}
|
|
77
|
+
for (const uid of deletedUids){
|
|
78
|
+
effective.delete(uid);
|
|
79
|
+
}
|
|
80
|
+
for (const [uid, kind] of upsertedUids){
|
|
81
|
+
effective.set(uid, kind);
|
|
82
|
+
}
|
|
83
|
+
return effective;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Remove references to content types that have been deleted
|
|
87
|
+
*/ const pruneStructure = (file, deletedUids)=>{
|
|
88
|
+
const cloned = cloneFile(file);
|
|
89
|
+
for (const sectionKey of SECTION_KEYS){
|
|
90
|
+
for (const group of cloned.sections[sectionKey].groups){
|
|
91
|
+
group.children = group.children.filter((child)=>{
|
|
92
|
+
if (child.type !== 'contentType') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const { uid } = child;
|
|
96
|
+
if (deletedUids.has(uid)) {
|
|
97
|
+
warn(`Pruned deleted content type "${uid}" from group "${group.id}"`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return cloned;
|
|
105
|
+
};
|
|
106
|
+
const validateContentTypeUidReferences = (structure, contentTypeKinds)=>{
|
|
107
|
+
const file = formatContentStructureObjectAsFile(structure);
|
|
108
|
+
const violations = [];
|
|
109
|
+
for (const sectionKey of SECTION_KEYS){
|
|
110
|
+
const expectedKind = expectedKindFor(sectionKey);
|
|
111
|
+
for (const group of file.sections[sectionKey].groups){
|
|
112
|
+
for (const child of group.children){
|
|
113
|
+
if (child.type !== 'contentType') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const { uid } = child;
|
|
117
|
+
// Referenced content type exists (in the effective set).
|
|
118
|
+
if (!contentTypeKinds.has(uid)) {
|
|
119
|
+
violations.push(`Content type "${uid}" in group "${group.id}" does not exist in section "${sectionKey}"`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Kind matches section.
|
|
123
|
+
if (contentTypeKinds.get(uid) !== expectedKind) {
|
|
124
|
+
violations.push(`Content type "${uid}" in group "${group.id}" is a ${contentTypeKinds.get(uid)} and cannot be placed in section "${sectionKey}"`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (violations.length > 0) {
|
|
130
|
+
throw new ApplicationError(`Invalid content structure:\n- ${violations.join('\n- ')}`, {
|
|
131
|
+
errors: violations.map((message)=>({
|
|
132
|
+
name: 'ApplicationError',
|
|
133
|
+
path: [],
|
|
134
|
+
message
|
|
135
|
+
}))
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const validateFromUpdate = ({ incomingStructure, upsertedUids, deletedUids })=>{
|
|
140
|
+
if (incomingStructure === undefined || incomingStructure === null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const effectiveKinds = buildEffectiveUidKindSet(upsertedUids, deletedUids);
|
|
144
|
+
const pruned = pruneStructure(formatContentStructureObjectAsFile(incomingStructure), deletedUids);
|
|
145
|
+
validateContentTypeUidReferences(pruned, effectiveKinds);
|
|
146
|
+
};
|
|
147
|
+
const commitFromUpdate = async ({ incomingStructure, deletedUids })=>{
|
|
148
|
+
if (incomingStructure !== undefined && incomingStructure !== null) {
|
|
149
|
+
const pruned = pruneStructure(formatContentStructureObjectAsFile(incomingStructure), deletedUids);
|
|
150
|
+
await contentStructureService.write(pruned);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (deletedUids.size > 0) {
|
|
154
|
+
const current = await contentStructureService.getCleanedFile();
|
|
155
|
+
if (!current) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const pruned = pruneStructure(current, deletedUids);
|
|
159
|
+
await contentStructureService.write(pruned);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
validateContentTypeUidReferences,
|
|
166
|
+
validateFromUpdate,
|
|
167
|
+
commitFromUpdate
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { createContentStructureService };
|
|
172
|
+
//# sourceMappingURL=content-structure.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-structure.mjs","sources":["../../../server/src/services/content-structure.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\n\nimport type { Core, Modules } from '@strapi/types';\n\nconst { ApplicationError } = errors;\n\ntype ContentStructureGroup = Modules.ContentStructure.ContentStructureGroup;\ntype ContentStructureFile = Modules.ContentStructure.ContentStructureFile;\ntype SectionKey = Modules.ContentStructure.ContentStructureSectionKey;\ntype ContentTypeKind = 'collectionType' | 'singleType';\n\nconst SECTION_KEYS: SectionKey[] = ['collectionTypes', 'singleTypes'];\n\nconst expectedKindFor = (sectionKey: SectionKey): ContentTypeKind => {\n return sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';\n};\n\n/**\n * A mirror of the subset of the core `content-structure` service (strapi.get('content-structure'))\n * that this service consumes.\n */\nexport interface CoreContentStructureService {\n getCleanedFile(): Promise<ContentStructureFile | null>;\n write(structure: ContentStructureFile): Promise<void>;\n}\n\n/**\n * This service handles the CTB orchestration of folder-group WRITES. It handles:\n * - Context-aware validation that requires the content-type registry\n * - Context-aware pruning of references invalidated by CTB transaction.\n * - Persistence to filesystem via the core content-structure service.\n * It assumes that non-context-aware validation has already been performed.\n */\nexport interface ContentTypeBuilderContentStructureService {\n /**\n * Validates all references to content types in the provided content structure.\n * Throws an ApplicationError naming every offending uid + rule.\n */\n validateContentTypeUidReferences(\n structure: unknown,\n contentTypeUids: Map<string, ContentTypeKind>\n ): void;\n\n validateFromUpdate(input: {\n incomingStructure?: unknown;\n upsertedUids: Map<string, ContentTypeKind>;\n deletedUids: Set<string>;\n }): void;\n\n commitFromUpdate(input: {\n incomingStructure?: unknown;\n deletedUids: Set<string>;\n }): Promise<boolean>;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n};\n\n/**\n * Formats a validated ContentStructureFile into a complete `groups.json` file.\n */\nconst formatContentStructureObjectAsFile = (structure: unknown): ContentStructureFile => {\n const sections = isRecord(structure) && isRecord(structure.sections) ? structure.sections : {};\n\n // Supplies all record groups with expected fields, so that the reference validator\n // can access `children`/`id` unconditionally.\n const coerceGroup = (raw: Record<string, unknown>): ContentStructureGroup => {\n const childrenArray = (\n Array.isArray(raw.children) ? raw.children : []\n ) as Modules.ContentStructure.ContentStructureChild[];\n\n return {\n parent: typeof raw.parent === 'string' ? raw.parent : null,\n name: typeof raw.name === 'string' ? raw.name : '',\n id: typeof raw.id === 'string' ? raw.id : '',\n children: childrenArray,\n };\n };\n\n const coerceSection = (key: SectionKey): Modules.ContentStructure.ContentStructureSection => {\n const raw = (sections as Record<string, unknown>)[key];\n const groups = isRecord(raw) && Array.isArray(raw.groups) ? raw.groups : [];\n\n return { groups: groups.filter(isRecord).map(coerceGroup) };\n };\n\n return {\n version: 1,\n sections: {\n collectionTypes: coerceSection('collectionTypes'),\n singleTypes: coerceSection('singleTypes'),\n },\n };\n};\n\nconst cloneFile = (file: ContentStructureFile): ContentStructureFile => ({\n version: 1,\n sections: {\n collectionTypes: {\n groups: file.sections.collectionTypes.groups.map((group) => ({\n ...group,\n children: [...group.children],\n })),\n },\n singleTypes: {\n groups: file.sections.singleTypes.groups.map((group) => ({\n ...group,\n children: [...group.children],\n })),\n },\n },\n});\n\nexport function createContentStructureService(\n strapi: Core.Strapi\n): ContentTypeBuilderContentStructureService {\n const contentStructureService: CoreContentStructureService = strapi.get('content-structure');\n\n const warn = (message: string) => {\n strapi.log.warn(`[content-structure] ${message}`);\n };\n\n /**\n * Builds an effective set of content type uids and their kind by resolving the pre-transaction content type registry with the list of upserted and deleted uids.\n */\n const buildEffectiveUidKindSet = (\n upsertedUids: Map<string, ContentTypeKind>,\n deletedUids: Set<string>\n ): Map<string, ContentTypeKind> => {\n const effective = new Map<string, ContentTypeKind>();\n\n for (const [uid, contentType] of Object.entries(strapi.contentTypes)) {\n effective.set(uid, contentType.kind === 'singleType' ? 'singleType' : 'collectionType');\n }\n\n for (const uid of deletedUids) {\n effective.delete(uid);\n }\n\n for (const [uid, kind] of upsertedUids) {\n effective.set(uid, kind);\n }\n\n return effective;\n };\n\n /**\n * Remove references to content types that have been deleted\n */\n const pruneStructure = (\n file: ContentStructureFile,\n deletedUids: Set<string>\n ): ContentStructureFile => {\n const cloned = cloneFile(file);\n\n for (const sectionKey of SECTION_KEYS) {\n for (const group of cloned.sections[sectionKey].groups) {\n group.children = group.children.filter((child) => {\n if (child.type !== 'contentType') {\n return true;\n }\n\n const { uid } = child;\n\n if (deletedUids.has(uid)) {\n warn(`Pruned deleted content type \"${uid}\" from group \"${group.id}\"`);\n return false;\n }\n\n return true;\n });\n }\n }\n\n return cloned;\n };\n\n const validateContentTypeUidReferences = (\n structure: unknown,\n contentTypeKinds: Map<string, ContentTypeKind>\n ): void => {\n const file = formatContentStructureObjectAsFile(structure);\n const violations: string[] = [];\n\n for (const sectionKey of SECTION_KEYS) {\n const expectedKind = expectedKindFor(sectionKey);\n\n for (const group of file.sections[sectionKey].groups) {\n for (const child of group.children) {\n if (child.type !== 'contentType') {\n continue;\n }\n\n const { uid } = child;\n\n // Referenced content type exists (in the effective set).\n if (!contentTypeKinds.has(uid)) {\n violations.push(\n `Content type \"${uid}\" in group \"${group.id}\" does not exist in section \"${sectionKey}\"`\n );\n continue;\n }\n\n // Kind matches section.\n if (contentTypeKinds.get(uid) !== expectedKind) {\n violations.push(\n `Content type \"${uid}\" in group \"${group.id}\" is a ${contentTypeKinds.get(\n uid\n )} and cannot be placed in section \"${sectionKey}\"`\n );\n }\n }\n }\n }\n\n if (violations.length > 0) {\n throw new ApplicationError(`Invalid content structure:\\n- ${violations.join('\\n- ')}`, {\n errors: violations.map((message) => ({\n name: 'ApplicationError',\n path: [] as string[],\n message,\n })),\n });\n }\n };\n\n const validateFromUpdate = ({\n incomingStructure,\n upsertedUids,\n deletedUids,\n }: {\n incomingStructure?: unknown;\n upsertedUids: Map<string, ContentTypeKind>;\n deletedUids: Set<string>;\n }): void => {\n if (incomingStructure === undefined || incomingStructure === null) {\n return;\n }\n\n const effectiveKinds = buildEffectiveUidKindSet(upsertedUids, deletedUids);\n const pruned = pruneStructure(\n formatContentStructureObjectAsFile(incomingStructure),\n deletedUids\n );\n\n validateContentTypeUidReferences(pruned, effectiveKinds);\n };\n\n const commitFromUpdate = async ({\n incomingStructure,\n deletedUids,\n }: {\n incomingStructure?: unknown;\n deletedUids: Set<string>;\n }): Promise<boolean> => {\n if (incomingStructure !== undefined && incomingStructure !== null) {\n const pruned = pruneStructure(\n formatContentStructureObjectAsFile(incomingStructure),\n deletedUids\n );\n\n await contentStructureService.write(pruned);\n return true;\n }\n\n if (deletedUids.size > 0) {\n const current = await contentStructureService.getCleanedFile();\n\n if (!current) {\n return false;\n }\n\n const pruned = pruneStructure(current, deletedUids);\n\n await contentStructureService.write(pruned);\n return true;\n }\n\n return false;\n };\n\n return {\n validateContentTypeUidReferences,\n validateFromUpdate,\n commitFromUpdate,\n };\n}\n"],"names":["ApplicationError","errors","SECTION_KEYS","expectedKindFor","sectionKey","isRecord","value","Array","isArray","formatContentStructureObjectAsFile","structure","sections","coerceGroup","raw","childrenArray","children","parent","name","id","coerceSection","key","groups","filter","map","version","collectionTypes","singleTypes","cloneFile","file","group","createContentStructureService","strapi","contentStructureService","get","warn","message","log","buildEffectiveUidKindSet","upsertedUids","deletedUids","effective","Map","uid","contentType","Object","entries","contentTypes","set","kind","delete","pruneStructure","cloned","child","type","has","validateContentTypeUidReferences","contentTypeKinds","violations","expectedKind","push","length","join","path","validateFromUpdate","incomingStructure","undefined","effectiveKinds","pruned","commitFromUpdate","write","size","current","getCleanedFile"],"mappings":"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"}
|