@strapi/core 5.54.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.
Files changed (47) hide show
  1. package/dist/configuration/get-dirs.d.ts.map +1 -1
  2. package/dist/configuration/get-dirs.js +4 -2
  3. package/dist/configuration/get-dirs.js.map +1 -1
  4. package/dist/configuration/get-dirs.mjs +4 -2
  5. package/dist/configuration/get-dirs.mjs.map +1 -1
  6. package/dist/core-api/routes/validation/component.d.ts +1 -1
  7. package/dist/core-api/routes/validation/component.d.ts.map +1 -1
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/package.json.js +13 -13
  13. package/dist/package.json.mjs +13 -13
  14. package/dist/providers/content-structure.d.ts +3 -0
  15. package/dist/providers/content-structure.d.ts.map +1 -0
  16. package/dist/providers/content-structure.js +22 -0
  17. package/dist/providers/content-structure.js.map +1 -0
  18. package/dist/providers/content-structure.mjs +20 -0
  19. package/dist/providers/content-structure.mjs.map +1 -0
  20. package/dist/providers/index.d.ts.map +1 -1
  21. package/dist/providers/index.js +2 -0
  22. package/dist/providers/index.js.map +1 -1
  23. package/dist/providers/index.mjs +2 -0
  24. package/dist/providers/index.mjs.map +1 -1
  25. package/dist/services/content-structure/index.d.ts +37 -0
  26. package/dist/services/content-structure/index.d.ts.map +1 -0
  27. package/dist/services/content-structure/index.js +304 -0
  28. package/dist/services/content-structure/index.js.map +1 -0
  29. package/dist/services/content-structure/index.mjs +296 -0
  30. package/dist/services/content-structure/index.mjs.map +1 -0
  31. package/dist/services/content-structure/utils/isGroupExpressionValid.d.ts +7 -0
  32. package/dist/services/content-structure/utils/isGroupExpressionValid.d.ts.map +1 -0
  33. package/dist/services/content-structure/utils/isGroupExpressionValid.js +19 -0
  34. package/dist/services/content-structure/utils/isGroupExpressionValid.js.map +1 -0
  35. package/dist/services/content-structure/utils/isGroupExpressionValid.mjs +17 -0
  36. package/dist/services/content-structure/utils/isGroupExpressionValid.mjs.map +1 -0
  37. package/dist/services/content-structure/validation.d.ts +35 -0
  38. package/dist/services/content-structure/validation.d.ts.map +1 -0
  39. package/dist/services/content-structure/validation.js +163 -0
  40. package/dist/services/content-structure/validation.js.map +1 -0
  41. package/dist/services/content-structure/validation.mjs +161 -0
  42. package/dist/services/content-structure/validation.mjs.map +1 -0
  43. package/dist/services/server/koa-methods.d.ts +0 -6
  44. package/dist/services/server/koa-methods.d.ts.map +1 -1
  45. package/dist/services/server/koa-methods.js.map +1 -1
  46. package/dist/services/server/koa-methods.mjs.map +1 -1
  47. package/package.json +13 -13
@@ -0,0 +1,304 @@
1
+ 'use strict';
2
+
3
+ var path = require('path');
4
+ var fse = require('fs-extra');
5
+ var isGroupExpressionValid = require('./utils/isGroupExpressionValid.js');
6
+ var validation = require('./validation.js');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var fse__default = /*#__PURE__*/_interopDefault(fse);
11
+
12
+ const CONTENT_STRUCTURE_FILE_NAME = 'groups.json';
13
+ const MAX_FOLDER_DEPTH = 3;
14
+ const isRecord = (value)=>{
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ };
17
+ function createContentStructureService(strapi) {
18
+ /**
19
+ * This cache exists to ensure all consumers recieve the same cleaned+resolved data, regardless of call order / invocation relative to the strapi reload.
20
+ */ let cache;
21
+ const warn = (message)=>{
22
+ strapi.log.warn(`[content-structure] ${message}`);
23
+ };
24
+ const cleanSection = (rawSection, sectionKey)=>{
25
+ const rawGroups = (()=>{
26
+ if (rawSection === undefined || rawSection === null) {
27
+ return [];
28
+ }
29
+ if (!isRecord(rawSection) || !Array.isArray(rawSection.groups)) {
30
+ warn(`Section "${sectionKey}" is malformed; ignoring it`);
31
+ return [];
32
+ }
33
+ return rawSection.groups;
34
+ })();
35
+ const seenGroupIds = new Map();
36
+ const groups = [];
37
+ for (const raw of rawGroups){
38
+ if (!isGroupExpressionValid.isGroupExpressionValid(raw)) {
39
+ warn(`Dropping a malformed group entry in section "${sectionKey}"`);
40
+ continue;
41
+ }
42
+ if (raw.name.trim() === '') {
43
+ warn(`Dropping group "${raw.id}" in section "${sectionKey}": empty name`);
44
+ continue;
45
+ }
46
+ if (seenGroupIds.has(raw.id)) {
47
+ warn(`Dropping duplicate group id "${raw.id}" in section "${sectionKey}"`);
48
+ continue;
49
+ }
50
+ const group = {
51
+ children: raw.children,
52
+ parent: raw.parent,
53
+ name: raw.name,
54
+ id: raw.id
55
+ };
56
+ seenGroupIds.set(group.id, group);
57
+ groups.push(group);
58
+ }
59
+ const reparentToRoot = (group, reason)=>{
60
+ group.parent = null;
61
+ groups.splice(groups.indexOf(group), 1);
62
+ groups.push(group);
63
+ warn(`Reparenting group "${group.id}" in section "${sectionKey}" to root: ${reason}`);
64
+ };
65
+ // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.
66
+ for (const group of [
67
+ ...groups
68
+ ]){
69
+ if (group.parent !== null && !seenGroupIds.has(group.parent)) {
70
+ reparentToRoot(group, `parent "${group.parent}" does not exist`);
71
+ }
72
+ }
73
+ // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.
74
+ for (const group of [
75
+ ...groups
76
+ ]){
77
+ const chain = new Set([
78
+ group.id
79
+ ]);
80
+ let current = group;
81
+ while(current.parent !== null){
82
+ if (chain.has(current.parent)) {
83
+ reparentToRoot(group, 'its parent chain contains a cycle');
84
+ break;
85
+ }
86
+ chain.add(current.parent);
87
+ current = seenGroupIds.get(current.parent);
88
+ }
89
+ }
90
+ // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.
91
+ for (const group of [
92
+ ...groups
93
+ ]){
94
+ let current = group;
95
+ let depth = 1;
96
+ while(current.parent !== null){
97
+ current = seenGroupIds.get(current.parent);
98
+ depth += 1;
99
+ }
100
+ if (depth > MAX_FOLDER_DEPTH) {
101
+ reparentToRoot(group, `it exceeds the maximum nesting depth of ${MAX_FOLDER_DEPTH}`);
102
+ }
103
+ }
104
+ const expectedKind = sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';
105
+ const seenGroupChildren = new Set();
106
+ const seenUids = new Set();
107
+ for (const group of groups){
108
+ const cleanedChildren = [];
109
+ for (const entry of group.children){
110
+ if (!isRecord(entry)) {
111
+ warn(`Dropping a malformed child entry of group "${group.id}"`);
112
+ continue;
113
+ }
114
+ if (entry.type === 'contentType') {
115
+ const { uid } = entry;
116
+ if (typeof uid !== 'string') {
117
+ warn(`Dropping a contentType child of group "${group.id}": invalid uid`);
118
+ continue;
119
+ }
120
+ const schema = strapi.contentTypes[uid];
121
+ if (!schema) {
122
+ warn(`Dropping unknown content type "${uid}" from group "${group.id}"`);
123
+ continue;
124
+ }
125
+ // kind is optional on schemas; absent means collectionType.
126
+ const kind = schema.kind ?? 'collectionType';
127
+ if (kind !== expectedKind) {
128
+ warn(`Dropping content type "${uid}" from group "${group.id}": its kind "${kind}" does not belong in section "${sectionKey}"`);
129
+ continue;
130
+ }
131
+ if (seenUids.has(uid)) {
132
+ warn(`Dropping duplicate reference to content type "${uid}" from group "${group.id}"`);
133
+ continue;
134
+ }
135
+ cleanedChildren.push({
136
+ type: 'contentType',
137
+ uid: uid
138
+ });
139
+ seenUids.add(uid);
140
+ continue;
141
+ }
142
+ if (entry.type === 'group') {
143
+ const { id } = entry;
144
+ if (typeof id !== 'string') {
145
+ warn(`Dropping a group child of group "${group.id}": invalid id`);
146
+ continue;
147
+ }
148
+ const target = seenGroupIds.get(id);
149
+ if (!target || target.parent !== group.id) {
150
+ warn(`Dropping group child "${id}" from group "${group.id}": inconsistent reference`);
151
+ continue;
152
+ }
153
+ if (seenGroupChildren.has(id)) {
154
+ warn(`Dropping duplicate group child "${id}" from group "${group.id}"`);
155
+ continue;
156
+ }
157
+ cleanedChildren.push({
158
+ type: 'group',
159
+ id
160
+ });
161
+ seenGroupChildren.add(id);
162
+ continue;
163
+ }
164
+ warn(`Dropping a child entry of group "${group.id}" with an unknown type`);
165
+ }
166
+ group.children = cleanedChildren;
167
+ }
168
+ for (const group of groups){
169
+ if (group.parent !== null && !seenGroupChildren.has(group.id)) {
170
+ const parent = seenGroupIds.get(group.parent);
171
+ parent.children.push({
172
+ type: 'group',
173
+ id: group.id
174
+ });
175
+ seenGroupChildren.add(group.id);
176
+ warn(`Group "${group.id}" was missing from the children of its parent "${parent.id}"; appended it`);
177
+ }
178
+ }
179
+ return {
180
+ groups
181
+ };
182
+ };
183
+ const resolveSection = (section)=>{
184
+ const byId = new Map(section.groups.map((group)=>[
185
+ group.id,
186
+ group
187
+ ]));
188
+ const toNode = (group)=>{
189
+ const cleanedChildren = group.children.map((entry)=>{
190
+ if (entry.type === 'contentType') {
191
+ return {
192
+ type: 'contentType',
193
+ uid: entry.uid
194
+ };
195
+ }
196
+ return toNode(byId.get(entry.id));
197
+ });
198
+ return {
199
+ children: cleanedChildren,
200
+ name: group.name,
201
+ type: 'group',
202
+ id: group.id
203
+ };
204
+ };
205
+ return section.groups.filter((group)=>group.parent === null).map(toNode);
206
+ };
207
+ async function read() {
208
+ const filePath = path.join(strapi.dirs.dist.contentStructure, CONTENT_STRUCTURE_FILE_NAME);
209
+ if (!await fse__default.default.pathExists(filePath)) {
210
+ return null;
211
+ }
212
+ let parsed;
213
+ try {
214
+ parsed = await fse__default.default.readJSON(filePath);
215
+ } catch (error) {
216
+ const message = error instanceof Error ? error.message : String(error);
217
+ strapi.log.error(`[content-structure] Could not parse ${filePath} (${message}); continuing without content-type folders`);
218
+ return null;
219
+ }
220
+ if (!isRecord(parsed) || !isRecord(parsed.sections)) {
221
+ strapi.log.error(`[content-structure] ${filePath} is not an object with a "sections" property; continuing without content-type folders`);
222
+ return null;
223
+ }
224
+ if (parsed.version !== 1) {
225
+ strapi.log.error(`[content-structure] Unknown version "${parsed.version}" in ${filePath} (expected 1); continuing without content-type folders`);
226
+ return null;
227
+ }
228
+ return parsed;
229
+ }
230
+ const load = ()=>{
231
+ if (!cache) {
232
+ cache = (async ()=>{
233
+ const file = await read();
234
+ if (!file) {
235
+ return {
236
+ cleaned: null,
237
+ resolved: {
238
+ collectionTypes: [],
239
+ singleTypes: []
240
+ }
241
+ };
242
+ }
243
+ const cleaned = {
244
+ version: 1,
245
+ sections: {
246
+ collectionTypes: cleanSection(file.sections.collectionTypes, 'collectionTypes'),
247
+ singleTypes: cleanSection(file.sections.singleTypes, 'singleTypes')
248
+ }
249
+ };
250
+ return {
251
+ cleaned,
252
+ resolved: {
253
+ collectionTypes: resolveSection(cleaned.sections.collectionTypes),
254
+ singleTypes: resolveSection(cleaned.sections.singleTypes)
255
+ }
256
+ };
257
+ })();
258
+ }
259
+ return cache;
260
+ };
261
+ function invalidate() {
262
+ cache = undefined;
263
+ }
264
+ async function getCleanedFile() {
265
+ return (await load()).cleaned;
266
+ }
267
+ async function resolve() {
268
+ return (await load()).resolved;
269
+ }
270
+ async function write(structure) {
271
+ const dir = strapi.dirs.app.contentStructure;
272
+ await fse__default.default.ensureDir(dir);
273
+ await fse__default.default.writeJSON(path.join(dir, CONTENT_STRUCTURE_FILE_NAME), structure, {
274
+ spaces: 2
275
+ });
276
+ invalidate();
277
+ }
278
+ function validate(value) {
279
+ return validation.contentStructureFileSchema.parse(value);
280
+ }
281
+ async function countGroups() {
282
+ const { cleaned } = await load();
283
+ if (!cleaned) {
284
+ return 0;
285
+ }
286
+ const collectionTypesCount = cleaned.sections.collectionTypes.groups.length;
287
+ const singleTypesCount = cleaned.sections.singleTypes.groups.length;
288
+ return collectionTypesCount + singleTypesCount;
289
+ }
290
+ return {
291
+ getCleanedFile,
292
+ countGroups,
293
+ invalidate,
294
+ validate,
295
+ resolve,
296
+ write,
297
+ read
298
+ };
299
+ }
300
+
301
+ exports.CONTENT_STRUCTURE_FILE_NAME = CONTENT_STRUCTURE_FILE_NAME;
302
+ exports.MAX_FOLDER_DEPTH = MAX_FOLDER_DEPTH;
303
+ exports.createContentStructureService = createContentStructureService;
304
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../src/services/content-structure/index.ts"],"sourcesContent":["import { join } from 'path';\nimport fse from 'fs-extra';\n\nimport type { Core, Modules, UID } from '@strapi/types';\nimport { isGroupExpressionValid } from './utils/isGroupExpressionValid';\nimport { contentStructureFileSchema } from './validation';\n\nexport const CONTENT_STRUCTURE_FILE_NAME = 'groups.json';\n\nexport const MAX_FOLDER_DEPTH = 3;\n\nexport interface ContentStructureService {\n /**\n * Reads the contents of groups.json directly with minimal sanitization.\n */\n read(): Promise<Modules.ContentStructure.ContentStructureFile | null>;\n\n /**\n * Yields an extremely tolerant version of the groups configuration file. Invalid references are repaired or ignored. Warnings fire for each repair/omission.\n */\n getCleanedFile(): Promise<Modules.ContentStructure.ContentStructureFile | null>;\n\n /**\n * Returns the groups configuration transformed into nested trees.\n */\n resolve(): Promise<Modules.ContentStructure.ResolvedContentStructure>;\n\n /**\n * Writes a new config and invalidates the cache. Does NOT perform validation - this is\n * left to the caller. The Save that triggers the write restarts the server via the\n * standard update-schema flow, which reloads the file; this method only persists it.\n */\n write(structure: Modules.ContentStructure.ContentStructureFile): Promise<void>;\n\n /**\n * Strictly validates an unknown value against the canonical groups.json schema; throws if invalid.\n */\n validate(value: unknown): Modules.ContentStructure.ContentStructureFile;\n\n /**\n * Invalidates the cached parsed groups.json file.\n */\n invalidate(): void;\n\n /**\n * Total number of groups across both sections of the cleaned file.\n */\n countGroups(): Promise<number>;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n};\n\ntype ContentStructureCache = Promise<{\n cleaned: Modules.ContentStructure.ContentStructureFile | null;\n resolved: Modules.ContentStructure.ResolvedContentStructure;\n}>;\n\nexport function createContentStructureService(strapi: Core.Strapi): ContentStructureService {\n /**\n * This cache exists to ensure all consumers recieve the same cleaned+resolved data, regardless of call order / invocation relative to the strapi reload.\n */\n let cache: ContentStructureCache | undefined;\n\n const warn = (message: string) => {\n strapi.log.warn(`[content-structure] ${message}`);\n };\n\n const cleanSection = (\n rawSection: unknown,\n sectionKey: Modules.ContentStructure.ContentStructureSectionKey\n ): Modules.ContentStructure.ContentStructureSection => {\n const rawGroups = (() => {\n if (rawSection === undefined || rawSection === null) {\n return [];\n }\n\n if (!isRecord(rawSection) || !Array.isArray(rawSection.groups)) {\n warn(`Section \"${sectionKey}\" is malformed; ignoring it`);\n return [];\n }\n\n return rawSection.groups;\n })();\n\n const seenGroupIds = new Map<string, Modules.ContentStructure.ContentStructureGroup>();\n const groups: Modules.ContentStructure.ContentStructureGroup[] = [];\n\n for (const raw of rawGroups) {\n if (!isGroupExpressionValid(raw)) {\n warn(`Dropping a malformed group entry in section \"${sectionKey}\"`);\n continue;\n }\n\n if (raw.name.trim() === '') {\n warn(`Dropping group \"${raw.id}\" in section \"${sectionKey}\": empty name`);\n continue;\n }\n\n if (seenGroupIds.has(raw.id)) {\n warn(`Dropping duplicate group id \"${raw.id}\" in section \"${sectionKey}\"`);\n continue;\n }\n\n const group: Modules.ContentStructure.ContentStructureGroup = {\n children: raw.children as Modules.ContentStructure.ContentStructureChild[],\n parent: raw.parent,\n name: raw.name,\n id: raw.id,\n };\n\n seenGroupIds.set(group.id, group);\n groups.push(group);\n }\n\n const reparentToRoot = (\n group: Modules.ContentStructure.ContentStructureGroup,\n reason: string\n ) => {\n group.parent = null;\n\n groups.splice(groups.indexOf(group), 1);\n groups.push(group);\n\n warn(`Reparenting group \"${group.id}\" in section \"${sectionKey}\" to root: ${reason}`);\n };\n\n // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.\n for (const group of [...groups]) {\n if (group.parent !== null && !seenGroupIds.has(group.parent)) {\n reparentToRoot(group, `parent \"${group.parent}\" does not exist`);\n }\n }\n\n // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.\n for (const group of [...groups]) {\n const chain = new Set([group.id]);\n let current = group;\n\n while (current.parent !== null) {\n if (chain.has(current.parent)) {\n reparentToRoot(group, 'its parent chain contains a cycle');\n break;\n }\n\n chain.add(current.parent);\n current = seenGroupIds.get(current.parent)!;\n }\n }\n\n // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.\n for (const group of [...groups]) {\n let current = group;\n let depth = 1;\n\n while (current.parent !== null) {\n current = seenGroupIds.get(current.parent)!;\n depth += 1;\n }\n\n if (depth > MAX_FOLDER_DEPTH) {\n reparentToRoot(group, `it exceeds the maximum nesting depth of ${MAX_FOLDER_DEPTH}`);\n }\n }\n\n const expectedKind = sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';\n const seenGroupChildren = new Set<string>();\n const seenUids = new Set<string>();\n\n for (const group of groups) {\n const cleanedChildren: Modules.ContentStructure.ContentStructureChild[] = [];\n\n for (const entry of group.children) {\n if (!isRecord(entry)) {\n warn(`Dropping a malformed child entry of group \"${group.id}\"`);\n continue;\n }\n\n if (entry.type === 'contentType') {\n const { uid } = entry;\n\n if (typeof uid !== 'string') {\n warn(`Dropping a contentType child of group \"${group.id}\": invalid uid`);\n continue;\n }\n\n const schema: { kind?: string } | undefined = strapi.contentTypes[uid as UID.ContentType];\n\n if (!schema) {\n warn(`Dropping unknown content type \"${uid}\" from group \"${group.id}\"`);\n continue;\n }\n\n // kind is optional on schemas; absent means collectionType.\n const kind = schema.kind ?? 'collectionType';\n\n if (kind !== expectedKind) {\n warn(\n `Dropping content type \"${uid}\" from group \"${group.id}\": its kind \"${kind}\" does not belong in section \"${sectionKey}\"`\n );\n continue;\n }\n\n if (seenUids.has(uid)) {\n warn(`Dropping duplicate reference to content type \"${uid}\" from group \"${group.id}\"`);\n continue;\n }\n\n cleanedChildren.push({ type: 'contentType', uid: uid as UID.ContentType });\n seenUids.add(uid);\n continue;\n }\n\n if (entry.type === 'group') {\n const { id } = entry;\n\n if (typeof id !== 'string') {\n warn(`Dropping a group child of group \"${group.id}\": invalid id`);\n continue;\n }\n\n const target = seenGroupIds.get(id);\n\n if (!target || target.parent !== group.id) {\n warn(`Dropping group child \"${id}\" from group \"${group.id}\": inconsistent reference`);\n continue;\n }\n\n if (seenGroupChildren.has(id)) {\n warn(`Dropping duplicate group child \"${id}\" from group \"${group.id}\"`);\n continue;\n }\n\n cleanedChildren.push({ type: 'group', id });\n seenGroupChildren.add(id);\n continue;\n }\n\n warn(`Dropping a child entry of group \"${group.id}\" with an unknown type`);\n }\n\n group.children = cleanedChildren;\n }\n\n for (const group of groups) {\n if (group.parent !== null && !seenGroupChildren.has(group.id)) {\n const parent = seenGroupIds.get(group.parent)!;\n\n parent.children.push({ type: 'group', id: group.id });\n seenGroupChildren.add(group.id);\n\n warn(\n `Group \"${group.id}\" was missing from the children of its parent \"${parent.id}\"; appended it`\n );\n }\n }\n\n return { groups };\n };\n\n const resolveSection = (\n section: Modules.ContentStructure.ContentStructureSection\n ): Modules.ContentStructure.ResolvedGroupNode[] => {\n const byId = new Map(section.groups.map((group) => [group.id, group]));\n\n const toNode = (\n group: Modules.ContentStructure.ContentStructureGroup\n ): Modules.ContentStructure.ResolvedGroupNode => {\n const cleanedChildren = group.children.map(\n (entry): Modules.ContentStructure.ResolvedStructureChild => {\n if (entry.type === 'contentType') {\n return { type: 'contentType', uid: entry.uid };\n }\n\n return toNode(byId.get(entry.id)!);\n }\n );\n\n return {\n children: cleanedChildren,\n name: group.name,\n type: 'group',\n id: group.id,\n };\n };\n\n return section.groups.filter((group) => group.parent === null).map(toNode);\n };\n\n async function read(): Promise<Modules.ContentStructure.ContentStructureFile | null> {\n const filePath = join(strapi.dirs.dist.contentStructure, CONTENT_STRUCTURE_FILE_NAME);\n\n if (!(await fse.pathExists(filePath))) {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = await fse.readJSON(filePath);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n\n strapi.log.error(\n `[content-structure] Could not parse ${filePath} (${message}); continuing without content-type folders`\n );\n\n return null;\n }\n\n if (!isRecord(parsed) || !isRecord(parsed.sections)) {\n strapi.log.error(\n `[content-structure] ${filePath} is not an object with a \"sections\" property; continuing without content-type folders`\n );\n return null;\n }\n\n if (parsed.version !== 1) {\n strapi.log.error(\n `[content-structure] Unknown version \"${parsed.version}\" in ${filePath} (expected 1); continuing without content-type folders`\n );\n return null;\n }\n\n return parsed as unknown as Modules.ContentStructure.ContentStructureFile;\n }\n\n const load = () => {\n if (!cache) {\n cache = (async () => {\n const file = await read();\n\n if (!file) {\n return { cleaned: null, resolved: { collectionTypes: [], singleTypes: [] } };\n }\n\n const cleaned: Modules.ContentStructure.ContentStructureFile = {\n version: 1,\n sections: {\n collectionTypes: cleanSection(file.sections.collectionTypes, 'collectionTypes'),\n singleTypes: cleanSection(file.sections.singleTypes, 'singleTypes'),\n },\n };\n\n return {\n cleaned,\n resolved: {\n collectionTypes: resolveSection(cleaned.sections.collectionTypes),\n singleTypes: resolveSection(cleaned.sections.singleTypes),\n },\n };\n })();\n }\n\n return cache;\n };\n\n function invalidate() {\n cache = undefined;\n }\n\n async function getCleanedFile() {\n return (await load()).cleaned;\n }\n\n async function resolve() {\n return (await load()).resolved;\n }\n\n async function write(structure: Modules.ContentStructure.ContentStructureFile) {\n const dir = strapi.dirs.app.contentStructure;\n\n await fse.ensureDir(dir);\n await fse.writeJSON(join(dir, CONTENT_STRUCTURE_FILE_NAME), structure, { spaces: 2 });\n\n invalidate();\n }\n\n function validate(value: unknown): Modules.ContentStructure.ContentStructureFile {\n return contentStructureFileSchema.parse(value) as Modules.ContentStructure.ContentStructureFile;\n }\n\n async function countGroups() {\n const { cleaned } = await load();\n\n if (!cleaned) {\n return 0;\n }\n\n const collectionTypesCount = cleaned.sections.collectionTypes.groups.length;\n const singleTypesCount = cleaned.sections.singleTypes.groups.length;\n\n return collectionTypesCount + singleTypesCount;\n }\n\n return {\n getCleanedFile,\n countGroups,\n invalidate,\n validate,\n resolve,\n write,\n read,\n };\n}\n"],"names":["CONTENT_STRUCTURE_FILE_NAME","MAX_FOLDER_DEPTH","isRecord","value","Array","isArray","createContentStructureService","strapi","cache","warn","message","log","cleanSection","rawSection","sectionKey","rawGroups","undefined","groups","seenGroupIds","Map","raw","isGroupExpressionValid","name","trim","id","has","group","children","parent","set","push","reparentToRoot","reason","splice","indexOf","chain","Set","current","add","get","depth","expectedKind","seenGroupChildren","seenUids","cleanedChildren","entry","type","uid","schema","contentTypes","kind","target","resolveSection","section","byId","map","toNode","filter","read","filePath","join","dirs","dist","contentStructure","fse","pathExists","parsed","readJSON","error","Error","String","sections","version","load","file","cleaned","resolved","collectionTypes","singleTypes","invalidate","getCleanedFile","resolve","write","structure","dir","app","ensureDir","writeJSON","spaces","validate","contentStructureFileSchema","parse","countGroups","collectionTypesCount","length","singleTypesCount"],"mappings":"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"}
@@ -0,0 +1,296 @@
1
+ import { join } from 'path';
2
+ import fse from 'fs-extra';
3
+ import { isGroupExpressionValid } from './utils/isGroupExpressionValid.mjs';
4
+ import { contentStructureFileSchema } from './validation.mjs';
5
+
6
+ const CONTENT_STRUCTURE_FILE_NAME = 'groups.json';
7
+ const MAX_FOLDER_DEPTH = 3;
8
+ const isRecord = (value)=>{
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ };
11
+ function createContentStructureService(strapi) {
12
+ /**
13
+ * This cache exists to ensure all consumers recieve the same cleaned+resolved data, regardless of call order / invocation relative to the strapi reload.
14
+ */ let cache;
15
+ const warn = (message)=>{
16
+ strapi.log.warn(`[content-structure] ${message}`);
17
+ };
18
+ const cleanSection = (rawSection, sectionKey)=>{
19
+ const rawGroups = (()=>{
20
+ if (rawSection === undefined || rawSection === null) {
21
+ return [];
22
+ }
23
+ if (!isRecord(rawSection) || !Array.isArray(rawSection.groups)) {
24
+ warn(`Section "${sectionKey}" is malformed; ignoring it`);
25
+ return [];
26
+ }
27
+ return rawSection.groups;
28
+ })();
29
+ const seenGroupIds = new Map();
30
+ const groups = [];
31
+ for (const raw of rawGroups){
32
+ if (!isGroupExpressionValid(raw)) {
33
+ warn(`Dropping a malformed group entry in section "${sectionKey}"`);
34
+ continue;
35
+ }
36
+ if (raw.name.trim() === '') {
37
+ warn(`Dropping group "${raw.id}" in section "${sectionKey}": empty name`);
38
+ continue;
39
+ }
40
+ if (seenGroupIds.has(raw.id)) {
41
+ warn(`Dropping duplicate group id "${raw.id}" in section "${sectionKey}"`);
42
+ continue;
43
+ }
44
+ const group = {
45
+ children: raw.children,
46
+ parent: raw.parent,
47
+ name: raw.name,
48
+ id: raw.id
49
+ };
50
+ seenGroupIds.set(group.id, group);
51
+ groups.push(group);
52
+ }
53
+ const reparentToRoot = (group, reason)=>{
54
+ group.parent = null;
55
+ groups.splice(groups.indexOf(group), 1);
56
+ groups.push(group);
57
+ warn(`Reparenting group "${group.id}" in section "${sectionKey}" to root: ${reason}`);
58
+ };
59
+ // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.
60
+ for (const group of [
61
+ ...groups
62
+ ]){
63
+ if (group.parent !== null && !seenGroupIds.has(group.parent)) {
64
+ reparentToRoot(group, `parent "${group.parent}" does not exist`);
65
+ }
66
+ }
67
+ // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.
68
+ for (const group of [
69
+ ...groups
70
+ ]){
71
+ const chain = new Set([
72
+ group.id
73
+ ]);
74
+ let current = group;
75
+ while(current.parent !== null){
76
+ if (chain.has(current.parent)) {
77
+ reparentToRoot(group, 'its parent chain contains a cycle');
78
+ break;
79
+ }
80
+ chain.add(current.parent);
81
+ current = seenGroupIds.get(current.parent);
82
+ }
83
+ }
84
+ // oxlint-disable-next-line - Duplicating is necessary because the original array is mutated inside the iterator.
85
+ for (const group of [
86
+ ...groups
87
+ ]){
88
+ let current = group;
89
+ let depth = 1;
90
+ while(current.parent !== null){
91
+ current = seenGroupIds.get(current.parent);
92
+ depth += 1;
93
+ }
94
+ if (depth > MAX_FOLDER_DEPTH) {
95
+ reparentToRoot(group, `it exceeds the maximum nesting depth of ${MAX_FOLDER_DEPTH}`);
96
+ }
97
+ }
98
+ const expectedKind = sectionKey === 'collectionTypes' ? 'collectionType' : 'singleType';
99
+ const seenGroupChildren = new Set();
100
+ const seenUids = new Set();
101
+ for (const group of groups){
102
+ const cleanedChildren = [];
103
+ for (const entry of group.children){
104
+ if (!isRecord(entry)) {
105
+ warn(`Dropping a malformed child entry of group "${group.id}"`);
106
+ continue;
107
+ }
108
+ if (entry.type === 'contentType') {
109
+ const { uid } = entry;
110
+ if (typeof uid !== 'string') {
111
+ warn(`Dropping a contentType child of group "${group.id}": invalid uid`);
112
+ continue;
113
+ }
114
+ const schema = strapi.contentTypes[uid];
115
+ if (!schema) {
116
+ warn(`Dropping unknown content type "${uid}" from group "${group.id}"`);
117
+ continue;
118
+ }
119
+ // kind is optional on schemas; absent means collectionType.
120
+ const kind = schema.kind ?? 'collectionType';
121
+ if (kind !== expectedKind) {
122
+ warn(`Dropping content type "${uid}" from group "${group.id}": its kind "${kind}" does not belong in section "${sectionKey}"`);
123
+ continue;
124
+ }
125
+ if (seenUids.has(uid)) {
126
+ warn(`Dropping duplicate reference to content type "${uid}" from group "${group.id}"`);
127
+ continue;
128
+ }
129
+ cleanedChildren.push({
130
+ type: 'contentType',
131
+ uid: uid
132
+ });
133
+ seenUids.add(uid);
134
+ continue;
135
+ }
136
+ if (entry.type === 'group') {
137
+ const { id } = entry;
138
+ if (typeof id !== 'string') {
139
+ warn(`Dropping a group child of group "${group.id}": invalid id`);
140
+ continue;
141
+ }
142
+ const target = seenGroupIds.get(id);
143
+ if (!target || target.parent !== group.id) {
144
+ warn(`Dropping group child "${id}" from group "${group.id}": inconsistent reference`);
145
+ continue;
146
+ }
147
+ if (seenGroupChildren.has(id)) {
148
+ warn(`Dropping duplicate group child "${id}" from group "${group.id}"`);
149
+ continue;
150
+ }
151
+ cleanedChildren.push({
152
+ type: 'group',
153
+ id
154
+ });
155
+ seenGroupChildren.add(id);
156
+ continue;
157
+ }
158
+ warn(`Dropping a child entry of group "${group.id}" with an unknown type`);
159
+ }
160
+ group.children = cleanedChildren;
161
+ }
162
+ for (const group of groups){
163
+ if (group.parent !== null && !seenGroupChildren.has(group.id)) {
164
+ const parent = seenGroupIds.get(group.parent);
165
+ parent.children.push({
166
+ type: 'group',
167
+ id: group.id
168
+ });
169
+ seenGroupChildren.add(group.id);
170
+ warn(`Group "${group.id}" was missing from the children of its parent "${parent.id}"; appended it`);
171
+ }
172
+ }
173
+ return {
174
+ groups
175
+ };
176
+ };
177
+ const resolveSection = (section)=>{
178
+ const byId = new Map(section.groups.map((group)=>[
179
+ group.id,
180
+ group
181
+ ]));
182
+ const toNode = (group)=>{
183
+ const cleanedChildren = group.children.map((entry)=>{
184
+ if (entry.type === 'contentType') {
185
+ return {
186
+ type: 'contentType',
187
+ uid: entry.uid
188
+ };
189
+ }
190
+ return toNode(byId.get(entry.id));
191
+ });
192
+ return {
193
+ children: cleanedChildren,
194
+ name: group.name,
195
+ type: 'group',
196
+ id: group.id
197
+ };
198
+ };
199
+ return section.groups.filter((group)=>group.parent === null).map(toNode);
200
+ };
201
+ async function read() {
202
+ const filePath = join(strapi.dirs.dist.contentStructure, CONTENT_STRUCTURE_FILE_NAME);
203
+ if (!await fse.pathExists(filePath)) {
204
+ return null;
205
+ }
206
+ let parsed;
207
+ try {
208
+ parsed = await fse.readJSON(filePath);
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ strapi.log.error(`[content-structure] Could not parse ${filePath} (${message}); continuing without content-type folders`);
212
+ return null;
213
+ }
214
+ if (!isRecord(parsed) || !isRecord(parsed.sections)) {
215
+ strapi.log.error(`[content-structure] ${filePath} is not an object with a "sections" property; continuing without content-type folders`);
216
+ return null;
217
+ }
218
+ if (parsed.version !== 1) {
219
+ strapi.log.error(`[content-structure] Unknown version "${parsed.version}" in ${filePath} (expected 1); continuing without content-type folders`);
220
+ return null;
221
+ }
222
+ return parsed;
223
+ }
224
+ const load = ()=>{
225
+ if (!cache) {
226
+ cache = (async ()=>{
227
+ const file = await read();
228
+ if (!file) {
229
+ return {
230
+ cleaned: null,
231
+ resolved: {
232
+ collectionTypes: [],
233
+ singleTypes: []
234
+ }
235
+ };
236
+ }
237
+ const cleaned = {
238
+ version: 1,
239
+ sections: {
240
+ collectionTypes: cleanSection(file.sections.collectionTypes, 'collectionTypes'),
241
+ singleTypes: cleanSection(file.sections.singleTypes, 'singleTypes')
242
+ }
243
+ };
244
+ return {
245
+ cleaned,
246
+ resolved: {
247
+ collectionTypes: resolveSection(cleaned.sections.collectionTypes),
248
+ singleTypes: resolveSection(cleaned.sections.singleTypes)
249
+ }
250
+ };
251
+ })();
252
+ }
253
+ return cache;
254
+ };
255
+ function invalidate() {
256
+ cache = undefined;
257
+ }
258
+ async function getCleanedFile() {
259
+ return (await load()).cleaned;
260
+ }
261
+ async function resolve() {
262
+ return (await load()).resolved;
263
+ }
264
+ async function write(structure) {
265
+ const dir = strapi.dirs.app.contentStructure;
266
+ await fse.ensureDir(dir);
267
+ await fse.writeJSON(join(dir, CONTENT_STRUCTURE_FILE_NAME), structure, {
268
+ spaces: 2
269
+ });
270
+ invalidate();
271
+ }
272
+ function validate(value) {
273
+ return contentStructureFileSchema.parse(value);
274
+ }
275
+ async function countGroups() {
276
+ const { cleaned } = await load();
277
+ if (!cleaned) {
278
+ return 0;
279
+ }
280
+ const collectionTypesCount = cleaned.sections.collectionTypes.groups.length;
281
+ const singleTypesCount = cleaned.sections.singleTypes.groups.length;
282
+ return collectionTypesCount + singleTypesCount;
283
+ }
284
+ return {
285
+ getCleanedFile,
286
+ countGroups,
287
+ invalidate,
288
+ validate,
289
+ resolve,
290
+ write,
291
+ read
292
+ };
293
+ }
294
+
295
+ export { CONTENT_STRUCTURE_FILE_NAME, MAX_FOLDER_DEPTH, createContentStructureService };
296
+ //# sourceMappingURL=index.mjs.map