@dyrected/core 2.5.51 → 2.5.52

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 (165) hide show
  1. package/dist/__tests__/admin-auth.test.d.ts +2 -0
  2. package/dist/__tests__/admin-auth.test.d.ts.map +1 -0
  3. package/dist/__tests__/admin-auth.test.js +440 -0
  4. package/dist/__tests__/admin-auth.test.js.map +1 -0
  5. package/dist/__tests__/config.test.js +57 -0
  6. package/dist/__tests__/config.test.js.map +1 -1
  7. package/dist/__tests__/email.service.test.d.ts +2 -0
  8. package/dist/__tests__/email.service.test.d.ts.map +1 -0
  9. package/dist/__tests__/email.service.test.js +46 -0
  10. package/dist/__tests__/email.service.test.js.map +1 -0
  11. package/dist/__tests__/fixtures/editorial-workflow.d.ts +9 -0
  12. package/dist/__tests__/fixtures/editorial-workflow.d.ts.map +1 -0
  13. package/dist/__tests__/fixtures/editorial-workflow.js +12 -0
  14. package/dist/__tests__/fixtures/editorial-workflow.js.map +1 -0
  15. package/dist/__tests__/mocks.d.ts +6 -0
  16. package/dist/__tests__/mocks.d.ts.map +1 -1
  17. package/dist/__tests__/mocks.js +8 -0
  18. package/dist/__tests__/mocks.js.map +1 -1
  19. package/dist/__tests__/openapi.test.d.ts +2 -0
  20. package/dist/__tests__/openapi.test.d.ts.map +1 -0
  21. package/dist/__tests__/openapi.test.js +95 -0
  22. package/dist/__tests__/openapi.test.js.map +1 -0
  23. package/dist/__tests__/parse-sort.test.d.ts +2 -0
  24. package/dist/__tests__/parse-sort.test.d.ts.map +1 -0
  25. package/dist/__tests__/parse-sort.test.js +24 -0
  26. package/dist/__tests__/parse-sort.test.js.map +1 -0
  27. package/dist/__tests__/preferences.test.d.ts +2 -0
  28. package/dist/__tests__/preferences.test.d.ts.map +1 -0
  29. package/dist/__tests__/preferences.test.js +168 -0
  30. package/dist/__tests__/preferences.test.js.map +1 -0
  31. package/dist/__tests__/router.test.js +4 -0
  32. package/dist/__tests__/router.test.js.map +1 -1
  33. package/dist/__tests__/where.test.js +9 -1
  34. package/dist/__tests__/where.test.js.map +1 -1
  35. package/dist/__tests__/workflow.test.d.ts +2 -0
  36. package/dist/__tests__/workflow.test.d.ts.map +1 -0
  37. package/dist/__tests__/workflow.test.js +497 -0
  38. package/dist/__tests__/workflow.test.js.map +1 -0
  39. package/dist/app-config-DLK6bCaP.d.cts +1390 -0
  40. package/dist/app-config-DLK6bCaP.d.ts +1390 -0
  41. package/dist/app-config-oOarB6Fw.d.cts +1411 -0
  42. package/dist/app-config-oOarB6Fw.d.ts +1411 -0
  43. package/dist/auth/token.d.ts +2 -0
  44. package/dist/auth/token.d.ts.map +1 -1
  45. package/dist/auth/token.js.map +1 -1
  46. package/dist/controllers/admin-auth.controller.d.ts +68 -0
  47. package/dist/controllers/admin-auth.controller.d.ts.map +1 -0
  48. package/dist/controllers/admin-auth.controller.js +414 -0
  49. package/dist/controllers/admin-auth.controller.js.map +1 -0
  50. package/dist/controllers/collection.controller.d.ts +69 -0
  51. package/dist/controllers/collection.controller.d.ts.map +1 -1
  52. package/dist/controllers/collection.controller.js +232 -7
  53. package/dist/controllers/collection.controller.js.map +1 -1
  54. package/dist/controllers/global.controller.js +19 -3
  55. package/dist/controllers/global.controller.js.map +1 -1
  56. package/dist/controllers/media.controller.d.ts +2 -0
  57. package/dist/controllers/media.controller.d.ts.map +1 -1
  58. package/dist/controllers/media.controller.js +13 -6
  59. package/dist/controllers/media.controller.js.map +1 -1
  60. package/dist/index.d.cts +2 -2
  61. package/dist/index.d.ts +2 -2
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/router.d.ts.map +1 -1
  65. package/dist/router.js +168 -6
  66. package/dist/router.js.map +1 -1
  67. package/dist/server.cjs +54 -8
  68. package/dist/server.d.cts +7 -1
  69. package/dist/server.d.ts +7 -1
  70. package/dist/server.js +54 -8
  71. package/dist/services/email-template.d.ts +46 -0
  72. package/dist/services/email-template.d.ts.map +1 -0
  73. package/dist/services/email-template.js +88 -0
  74. package/dist/services/email-template.js.map +1 -0
  75. package/dist/services/email.service.d.ts.map +1 -1
  76. package/dist/services/email.service.js +25 -148
  77. package/dist/services/email.service.js.map +1 -1
  78. package/dist/services/population.service.d.ts.map +1 -1
  79. package/dist/services/population.service.js +27 -1
  80. package/dist/services/population.service.js.map +1 -1
  81. package/dist/types/access.d.ts +38 -0
  82. package/dist/types/access.d.ts.map +1 -0
  83. package/dist/types/access.js +2 -0
  84. package/dist/types/access.js.map +1 -0
  85. package/dist/types/adapters.d.ts +146 -0
  86. package/dist/types/adapters.d.ts.map +1 -0
  87. package/dist/types/adapters.js +2 -0
  88. package/dist/types/adapters.js.map +1 -0
  89. package/dist/types/admin-auth.d.ts +132 -0
  90. package/dist/types/admin-auth.d.ts.map +1 -0
  91. package/dist/types/admin-auth.js +2 -0
  92. package/dist/types/admin-auth.js.map +1 -0
  93. package/dist/types/admin.d.ts +65 -0
  94. package/dist/types/admin.d.ts.map +1 -0
  95. package/dist/types/admin.js +2 -0
  96. package/dist/types/admin.js.map +1 -0
  97. package/dist/types/app-config.d.ts +143 -0
  98. package/dist/types/app-config.d.ts.map +1 -0
  99. package/dist/types/app-config.js +2 -0
  100. package/dist/types/app-config.js.map +1 -0
  101. package/dist/types/documents.d.ts +65 -0
  102. package/dist/types/documents.d.ts.map +1 -0
  103. package/dist/types/documents.js +2 -0
  104. package/dist/types/documents.js.map +1 -0
  105. package/dist/types/hooks.d.ts +122 -0
  106. package/dist/types/hooks.d.ts.map +1 -0
  107. package/dist/types/hooks.js +2 -0
  108. package/dist/types/hooks.js.map +1 -0
  109. package/dist/types/index.d.ts +12 -1748
  110. package/dist/types/index.d.ts.map +1 -1
  111. package/dist/types/index.js +12 -2
  112. package/dist/types/index.js.map +1 -1
  113. package/dist/types/request.d.ts +42 -0
  114. package/dist/types/request.d.ts.map +1 -0
  115. package/dist/types/request.js +2 -0
  116. package/dist/types/request.js.map +1 -0
  117. package/dist/types/schema-config.d.ts +303 -0
  118. package/dist/types/schema-config.d.ts.map +1 -0
  119. package/dist/types/schema-config.js +2 -0
  120. package/dist/types/schema-config.js.map +1 -0
  121. package/dist/types/schema-core.d.ts +294 -0
  122. package/dist/types/schema-core.d.ts.map +1 -0
  123. package/dist/types/schema-core.js +2 -0
  124. package/dist/types/schema-core.js.map +1 -0
  125. package/dist/types/schema-inference.d.ts +73 -0
  126. package/dist/types/schema-inference.d.ts.map +1 -0
  127. package/dist/types/schema-inference.js +2 -0
  128. package/dist/types/schema-inference.js.map +1 -0
  129. package/dist/types/workflows.d.ts +84 -0
  130. package/dist/types/workflows.d.ts.map +1 -0
  131. package/dist/types/workflows.js +7 -0
  132. package/dist/types/workflows.js.map +1 -0
  133. package/dist/utils/admin-auth.d.ts +5 -0
  134. package/dist/utils/admin-auth.d.ts.map +1 -0
  135. package/dist/utils/admin-auth.js +39 -0
  136. package/dist/utils/admin-auth.js.map +1 -0
  137. package/dist/utils/config.d.ts.map +1 -1
  138. package/dist/utils/config.js +86 -1
  139. package/dist/utils/config.js.map +1 -1
  140. package/dist/utils/hooks.d.ts.map +1 -1
  141. package/dist/utils/hooks.js +12 -8
  142. package/dist/utils/hooks.js.map +1 -1
  143. package/dist/utils/openapi.d.ts.map +1 -1
  144. package/dist/utils/openapi.js +559 -48
  145. package/dist/utils/openapi.js.map +1 -1
  146. package/dist/utils/parse-sort.d.ts +7 -0
  147. package/dist/utils/parse-sort.d.ts.map +1 -0
  148. package/dist/utils/parse-sort.js +23 -0
  149. package/dist/utils/parse-sort.js.map +1 -0
  150. package/dist/utils/swagger.d.ts +6 -0
  151. package/dist/utils/swagger.d.ts.map +1 -1
  152. package/dist/utils/swagger.js +13 -2
  153. package/dist/utils/swagger.js.map +1 -1
  154. package/dist/utils/upload-validation.d.ts +23 -0
  155. package/dist/utils/upload-validation.d.ts.map +1 -0
  156. package/dist/utils/upload-validation.js +44 -0
  157. package/dist/utils/upload-validation.js.map +1 -0
  158. package/dist/utils/where-sanitizer.d.ts.map +1 -1
  159. package/dist/utils/where-sanitizer.js +4 -0
  160. package/dist/utils/where-sanitizer.js.map +1 -1
  161. package/dist/workflows.d.ts +54 -0
  162. package/dist/workflows.d.ts.map +1 -0
  163. package/dist/workflows.js +282 -0
  164. package/dist/workflows.js.map +1 -0
  165. package/package.json +1 -1
@@ -10,7 +10,76 @@ export function generateOpenApi(config) {
10
10
  description: "Automatically generated OpenAPI specification for the Dyrected project.",
11
11
  },
12
12
  components: {
13
- schemas: {},
13
+ schemas: {
14
+ WorkflowMetadata: {
15
+ type: "object",
16
+ required: ["state", "revision"],
17
+ properties: {
18
+ state: { type: "string" },
19
+ revision: { type: "integer", minimum: 1 },
20
+ publishedRevision: { type: "integer", minimum: 1 },
21
+ publishedAt: { type: "string", format: "date-time" },
22
+ publishedBy: { type: "string" },
23
+ availableTransitions: { type: "array", items: { type: "string" } },
24
+ },
25
+ },
26
+ WorkflowTransitionRequest: {
27
+ type: "object",
28
+ properties: {
29
+ expectedRevision: { type: "integer", minimum: 1 },
30
+ comment: { type: "string" },
31
+ },
32
+ },
33
+ WorkflowHistoryEntry: {
34
+ type: "object",
35
+ required: [
36
+ "collection",
37
+ "documentId",
38
+ "transition",
39
+ "from",
40
+ "to",
41
+ "revision",
42
+ "createdAt",
43
+ ],
44
+ properties: {
45
+ id: { type: "string" },
46
+ collection: { type: "string" },
47
+ documentId: { type: "string" },
48
+ transition: { type: "string" },
49
+ from: { type: "string" },
50
+ to: { type: "string" },
51
+ revision: { type: "integer" },
52
+ comment: { type: "string", nullable: true },
53
+ actorId: { type: "string", nullable: true },
54
+ createdAt: { type: "string", format: "date-time" },
55
+ },
56
+ },
57
+ Error: {
58
+ type: "object",
59
+ properties: {
60
+ message: { type: "string" },
61
+ errors: {
62
+ type: "array",
63
+ items: { type: "object", additionalProperties: true },
64
+ },
65
+ },
66
+ },
67
+ AuthCredentials: {
68
+ type: "object",
69
+ required: ["email", "password"],
70
+ properties: {
71
+ email: { type: "string", format: "email" },
72
+ password: { type: "string" },
73
+ },
74
+ },
75
+ TokenResponse: {
76
+ type: "object",
77
+ properties: {
78
+ token: { type: "string" },
79
+ user: { type: "object", additionalProperties: true },
80
+ },
81
+ },
82
+ },
14
83
  securitySchemes: {
15
84
  ApiKeyAuth: {
16
85
  type: "apiKey",
@@ -22,6 +91,98 @@ export function generateOpenApi(config) {
22
91
  paths: {},
23
92
  security: [{ ApiKeyAuth: [] }],
24
93
  };
94
+ spec.paths["/api/schemas"] = {
95
+ get: {
96
+ tags: ["System"],
97
+ summary: "Get the serialized Dyrected schema",
98
+ security: [],
99
+ responses: {
100
+ 200: { description: "Collection and global schema definitions" },
101
+ },
102
+ },
103
+ };
104
+ spec.paths["/api/openapi.json"] = {
105
+ get: {
106
+ tags: ["System"],
107
+ summary: "Get the OpenAPI specification",
108
+ security: [],
109
+ responses: { 200: { description: "OpenAPI 3.0 document" } },
110
+ },
111
+ };
112
+ spec.paths["/api/docs"] = {
113
+ get: {
114
+ tags: ["System"],
115
+ summary: "Open interactive API documentation",
116
+ security: [],
117
+ responses: { 200: { description: "Swagger UI HTML" } },
118
+ },
119
+ };
120
+ spec.paths["/api/dyrected/options/{collection}/{field}"] = {
121
+ get: {
122
+ tags: ["System"],
123
+ summary: "Resolve dynamic field options",
124
+ parameters: [
125
+ {
126
+ name: "collection",
127
+ in: "path",
128
+ required: true,
129
+ schema: { type: "string" },
130
+ },
131
+ {
132
+ name: "field",
133
+ in: "path",
134
+ required: true,
135
+ schema: { type: "string" },
136
+ },
137
+ ],
138
+ responses: { 200: { description: "Resolved option items" } },
139
+ },
140
+ };
141
+ spec.paths["/api/preferences/{key}"] = {
142
+ get: {
143
+ tags: ["Preferences"],
144
+ summary: "Get an authenticated user preference",
145
+ parameters: [
146
+ { name: "key", in: "path", required: true, schema: { type: "string" } },
147
+ ],
148
+ responses: { 200: { description: "Preference value" } },
149
+ },
150
+ put: {
151
+ tags: ["Preferences"],
152
+ summary: "Set an authenticated user preference",
153
+ parameters: [
154
+ { name: "key", in: "path", required: true, schema: { type: "string" } },
155
+ ],
156
+ requestBody: {
157
+ required: true,
158
+ content: { "application/json": { schema: { type: "object" } } },
159
+ },
160
+ responses: { 200: { description: "Updated preference" } },
161
+ },
162
+ };
163
+ spec.paths["/api/preview-token"] = {
164
+ post: {
165
+ tags: ["Preview"],
166
+ summary: "Create a preview token",
167
+ responses: { 200: { description: "Short-lived preview token" } },
168
+ },
169
+ };
170
+ spec.paths["/api/preview-data"] = {
171
+ get: {
172
+ tags: ["Preview"],
173
+ summary: "Resolve preview data from a token",
174
+ security: [],
175
+ parameters: [
176
+ {
177
+ name: "token",
178
+ in: "query",
179
+ required: true,
180
+ schema: { type: "string" },
181
+ },
182
+ ],
183
+ responses: { 200: { description: "Preview document data" } },
184
+ },
185
+ };
25
186
  // 1. Generate Schemas for Collections
26
187
  for (const collection of config.collections) {
27
188
  spec.components.schemas[collection.slug] = collectionToSchema(collection);
@@ -40,10 +201,28 @@ export function generateOpenApi(config) {
40
201
  tags: ["Collections"],
41
202
  summary: `Find ${labels.plural}`,
42
203
  parameters: [
43
- { name: "limit", in: "query", schema: { type: "integer", default: 10 } },
44
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
45
- { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
46
- { name: "sort", in: "query", schema: { type: "string" }, description: "Sort field (e.g. -createdAt)" },
204
+ {
205
+ name: "limit",
206
+ in: "query",
207
+ schema: { type: "integer", default: 10 },
208
+ },
209
+ {
210
+ name: "page",
211
+ in: "query",
212
+ schema: { type: "integer", default: 1 },
213
+ },
214
+ {
215
+ name: "where",
216
+ in: "query",
217
+ schema: { type: "string" },
218
+ description: "JSON filter",
219
+ },
220
+ {
221
+ name: "sort",
222
+ in: "query",
223
+ schema: { type: "string" },
224
+ description: "Sort field (e.g. -createdAt)",
225
+ },
47
226
  ],
48
227
  responses: {
49
228
  200: {
@@ -53,7 +232,10 @@ export function generateOpenApi(config) {
53
232
  schema: {
54
233
  type: "object",
55
234
  properties: {
56
- docs: { type: "array", items: { $ref: `#/components/schemas/${slug}` } },
235
+ docs: {
236
+ type: "array",
237
+ items: { $ref: `#/components/schemas/${slug}` },
238
+ },
57
239
  total: { type: "integer" },
58
240
  limit: { type: "integer" },
59
241
  page: { type: "integer" },
@@ -91,7 +273,14 @@ export function generateOpenApi(config) {
91
273
  get: {
92
274
  tags: ["Collections"],
93
275
  summary: `Get a single ${labels.singular}`,
94
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
276
+ parameters: [
277
+ {
278
+ name: "id",
279
+ in: "path",
280
+ required: true,
281
+ schema: { type: "string" },
282
+ },
283
+ ],
95
284
  responses: {
96
285
  200: {
97
286
  description: "Success",
@@ -106,7 +295,14 @@ export function generateOpenApi(config) {
106
295
  patch: {
107
296
  tags: ["Collections"],
108
297
  summary: `Update ${labels.singular}`,
109
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
298
+ parameters: [
299
+ {
300
+ name: "id",
301
+ in: "path",
302
+ required: true,
303
+ schema: { type: "string" },
304
+ },
305
+ ],
110
306
  requestBody: {
111
307
  required: true,
112
308
  content: {
@@ -129,12 +325,288 @@ export function generateOpenApi(config) {
129
325
  delete: {
130
326
  tags: ["Collections"],
131
327
  summary: `Delete ${labels.singular}`,
132
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
328
+ parameters: [
329
+ {
330
+ name: "id",
331
+ in: "path",
332
+ required: true,
333
+ schema: { type: "string" },
334
+ },
335
+ ],
133
336
  responses: {
134
337
  204: { description: "Deleted" },
135
338
  },
136
339
  },
137
340
  };
341
+ spec.paths[`${path}/delete-many`] = {
342
+ delete: {
343
+ tags: ["Collections"],
344
+ summary: `Delete multiple ${labels.plural}`,
345
+ requestBody: {
346
+ required: true,
347
+ content: {
348
+ "application/json": {
349
+ schema: {
350
+ type: "object",
351
+ required: ["ids"],
352
+ properties: {
353
+ ids: { type: "array", items: { type: "string" } },
354
+ },
355
+ },
356
+ },
357
+ },
358
+ },
359
+ responses: { 200: { description: "Deleted and failed document IDs" } },
360
+ },
361
+ };
362
+ spec.paths[`${path}/seed`] = {
363
+ post: {
364
+ tags: ["Collections"],
365
+ summary: `Seed initial ${labels.plural}`,
366
+ responses: { 200: { description: "Seeded documents" } },
367
+ },
368
+ };
369
+ if (collection.upload) {
370
+ spec.paths[`${path}/media`] = {
371
+ get: {
372
+ tags: ["Media"],
373
+ summary: `List ${labels.plural}`,
374
+ responses: { 200: { description: "Paginated media documents" } },
375
+ },
376
+ post: {
377
+ tags: ["Media"],
378
+ summary: `Upload ${labels.singular}`,
379
+ requestBody: {
380
+ required: true,
381
+ content: {
382
+ "multipart/form-data": {
383
+ schema: {
384
+ type: "object",
385
+ required: ["file"],
386
+ properties: { file: { type: "string", format: "binary" } },
387
+ },
388
+ },
389
+ },
390
+ },
391
+ responses: { 201: { description: "Uploaded media document" } },
392
+ },
393
+ };
394
+ spec.paths[`${path}/media/{filename}`] = {
395
+ get: {
396
+ tags: ["Media"],
397
+ summary: `Serve ${labels.singular} bytes`,
398
+ parameters: [
399
+ {
400
+ name: "filename",
401
+ in: "path",
402
+ required: true,
403
+ schema: { type: "string" },
404
+ },
405
+ ],
406
+ security: [],
407
+ responses: {
408
+ 200: { description: "Stored file" },
409
+ 404: { description: "File not found" },
410
+ },
411
+ },
412
+ };
413
+ }
414
+ if (collection.auth) {
415
+ const publicAuthPost = (summary) => ({
416
+ tags: ["Authentication"],
417
+ summary,
418
+ security: [],
419
+ requestBody: {
420
+ required: true,
421
+ content: {
422
+ "application/json": {
423
+ schema: { type: "object", additionalProperties: true },
424
+ },
425
+ },
426
+ },
427
+ responses: {
428
+ 200: { description: "Success" },
429
+ 400: { description: "Invalid request" },
430
+ },
431
+ });
432
+ spec.paths[`${path}/login`] = {
433
+ post: {
434
+ ...publicAuthPost(`Log in to ${labels.plural}`),
435
+ requestBody: {
436
+ required: true,
437
+ content: {
438
+ "application/json": {
439
+ schema: { $ref: "#/components/schemas/AuthCredentials" },
440
+ },
441
+ },
442
+ },
443
+ responses: {
444
+ 200: {
445
+ description: "Authenticated",
446
+ content: {
447
+ "application/json": {
448
+ schema: { $ref: "#/components/schemas/TokenResponse" },
449
+ },
450
+ },
451
+ },
452
+ 401: { description: "Invalid credentials" },
453
+ },
454
+ },
455
+ };
456
+ spec.paths[`${path}/logout`] = {
457
+ post: {
458
+ tags: ["Authentication"],
459
+ summary: `Log out of ${labels.plural}`,
460
+ responses: { 200: { description: "Logged out" } },
461
+ },
462
+ };
463
+ spec.paths[`${path}/init`] = {
464
+ get: {
465
+ tags: ["Authentication"],
466
+ summary: `Get ${labels.plural} initialization state`,
467
+ security: [],
468
+ responses: { 200: { description: "Initialization state" } },
469
+ },
470
+ };
471
+ spec.paths[`${path}/first-user`] = {
472
+ post: publicAuthPost(`Register the first ${labels.singular}`),
473
+ };
474
+ spec.paths[`${path}/me`] = {
475
+ get: {
476
+ tags: ["Authentication"],
477
+ summary: `Get the current ${labels.singular}`,
478
+ responses: { 200: { description: "Authenticated user" } },
479
+ },
480
+ };
481
+ spec.paths[`${path}/refresh-token`] = {
482
+ post: {
483
+ tags: ["Authentication"],
484
+ summary: "Refresh an authentication token",
485
+ responses: { 200: { description: "Refreshed token" } },
486
+ },
487
+ };
488
+ spec.paths[`${path}/forgot-password`] = {
489
+ post: publicAuthPost("Request a password reset"),
490
+ };
491
+ spec.paths[`${path}/reset-password`] = {
492
+ post: publicAuthPost("Reset a password"),
493
+ };
494
+ spec.paths[`${path}/invite`] = {
495
+ post: {
496
+ tags: ["Authentication"],
497
+ summary: `Invite a ${labels.singular}`,
498
+ responses: { 200: { description: "Invitation sent" } },
499
+ },
500
+ };
501
+ spec.paths[`${path}/accept-invite`] = {
502
+ post: publicAuthPost("Accept an invitation"),
503
+ };
504
+ spec.paths[`${path}/{id}/change-password`] = {
505
+ post: {
506
+ tags: ["Authentication"],
507
+ summary: `Change a ${labels.singular} password`,
508
+ parameters: [
509
+ {
510
+ name: "id",
511
+ in: "path",
512
+ required: true,
513
+ schema: { type: "string" },
514
+ },
515
+ ],
516
+ responses: { 200: { description: "Password changed" } },
517
+ },
518
+ };
519
+ }
520
+ if (collection.workflow) {
521
+ spec.paths[`${path}/{id}/transitions/{transition}`] = {
522
+ post: {
523
+ tags: ["Workflows"],
524
+ summary: `Transition ${labels.singular} workflow`,
525
+ parameters: [
526
+ {
527
+ name: "id",
528
+ in: "path",
529
+ required: true,
530
+ schema: { type: "string" },
531
+ },
532
+ {
533
+ name: "transition",
534
+ in: "path",
535
+ required: true,
536
+ schema: {
537
+ type: "string",
538
+ enum: collection.workflow.transitions.map((item) => item.name),
539
+ },
540
+ },
541
+ ],
542
+ requestBody: {
543
+ content: {
544
+ "application/json": {
545
+ schema: {
546
+ $ref: "#/components/schemas/WorkflowTransitionRequest",
547
+ },
548
+ },
549
+ },
550
+ },
551
+ responses: {
552
+ 200: {
553
+ description: "Transition applied",
554
+ content: {
555
+ "application/json": {
556
+ schema: { $ref: `#/components/schemas/${slug}` },
557
+ },
558
+ },
559
+ },
560
+ 400: { description: "Required transition input is missing" },
561
+ 403: { description: "Transition capability denied" },
562
+ 409: { description: "Invalid state or stale revision" },
563
+ },
564
+ },
565
+ };
566
+ spec.paths[`${path}/{id}/workflow-history`] = {
567
+ get: {
568
+ tags: ["Workflows"],
569
+ summary: `Get ${labels.singular} workflow history`,
570
+ parameters: [
571
+ {
572
+ name: "id",
573
+ in: "path",
574
+ required: true,
575
+ schema: { type: "string" },
576
+ },
577
+ {
578
+ name: "limit",
579
+ in: "query",
580
+ schema: { type: "integer", default: 50, maximum: 100 },
581
+ },
582
+ ],
583
+ responses: {
584
+ 200: {
585
+ description: "Workflow transition history",
586
+ content: {
587
+ "application/json": {
588
+ schema: {
589
+ type: "object",
590
+ properties: {
591
+ docs: {
592
+ type: "array",
593
+ items: {
594
+ $ref: "#/components/schemas/WorkflowHistoryEntry",
595
+ },
596
+ },
597
+ total: { type: "integer" },
598
+ limit: { type: "integer" },
599
+ page: { type: "integer" },
600
+ },
601
+ },
602
+ },
603
+ },
604
+ },
605
+ 403: { description: "Collection read access denied" },
606
+ },
607
+ },
608
+ };
609
+ }
138
610
  }
139
611
  // 4. Generate Paths for Globals
140
612
  for (const global of config.globals) {
@@ -178,46 +650,32 @@ export function generateOpenApi(config) {
178
650
  },
179
651
  },
180
652
  };
653
+ spec.paths[`${path}/seed`] = {
654
+ post: {
655
+ tags: ["Globals"],
656
+ summary: `Seed ${global.label || slug}`,
657
+ responses: { 200: { description: "Seeded global" } },
658
+ },
659
+ };
181
660
  }
182
- // 5. Generate Paths for Media (if storage is configured)
661
+ // 5. Generate the storage-backed file serving path.
183
662
  if (config.storage) {
184
- spec.paths["/api/media"] = {
663
+ spec.paths["/api/media/{filename}"] = {
185
664
  get: {
186
665
  tags: ["Media"],
187
- summary: "List Media",
188
- responses: {
189
- 200: {
190
- description: "Success",
191
- content: {
192
- "application/json": {
193
- schema: {
194
- type: "object",
195
- properties: {
196
- docs: { type: "array", items: { type: "object", additionalProperties: true } },
197
- },
198
- },
199
- },
200
- },
201
- },
202
- },
203
- },
204
- post: {
205
- tags: ["Media"],
206
- summary: "Upload Media",
207
- requestBody: {
208
- content: {
209
- "multipart/form-data": {
210
- schema: {
211
- type: "object",
212
- properties: {
213
- file: { type: "string", format: "binary" },
214
- },
215
- },
216
- },
666
+ summary: "Serve a stored file",
667
+ security: [],
668
+ parameters: [
669
+ {
670
+ name: "filename",
671
+ in: "path",
672
+ required: true,
673
+ schema: { type: "string" },
217
674
  },
218
- },
675
+ ],
219
676
  responses: {
220
- 201: { description: "Uploaded" },
677
+ 200: { description: "Stored file bytes" },
678
+ 404: { description: "File not found" },
221
679
  },
222
680
  },
223
681
  };
@@ -232,6 +690,9 @@ function collectionToSchema(collection) {
232
690
  id: { type: "string" },
233
691
  createdAt: { type: "string", format: "date-time" },
234
692
  updatedAt: { type: "string", format: "date-time" },
693
+ ...(collection.workflow
694
+ ? { _workflow: { $ref: "#/components/schemas/WorkflowMetadata" } }
695
+ : {}),
235
696
  ...properties,
236
697
  },
237
698
  required: ["id", ...required],
@@ -249,7 +710,13 @@ function fieldsToProperties(fields) {
249
710
  const props = {};
250
711
  const required = [];
251
712
  for (const field of fields) {
252
- if (!field.name || field.type === 'join' || field.type === 'row')
713
+ if (field.type === "row") {
714
+ const nested = fieldsToProperties(field.fields || []);
715
+ Object.assign(props, nested.properties);
716
+ required.push(...nested.required);
717
+ continue;
718
+ }
719
+ if (!field.name)
253
720
  continue;
254
721
  props[field.name] = fieldToSchema(field);
255
722
  if (field.required) {
@@ -264,7 +731,17 @@ function fieldToSchema(field) {
264
731
  case "text":
265
732
  case "textarea":
266
733
  case "email":
734
+ schema = { type: "string" };
735
+ break;
267
736
  case "url":
737
+ schema = {
738
+ oneOf: [
739
+ { type: "string" },
740
+ { type: "object", additionalProperties: true },
741
+ ],
742
+ };
743
+ break;
744
+ case "icon":
268
745
  schema = { type: "string" };
269
746
  break;
270
747
  case "number":
@@ -274,20 +751,51 @@ function fieldToSchema(field) {
274
751
  schema = { type: "boolean" };
275
752
  break;
276
753
  case "date":
754
+ schema = { type: "string", format: "date" };
755
+ break;
756
+ case "datetime":
277
757
  schema = { type: "string", format: "date-time" };
278
758
  break;
759
+ case "time":
760
+ schema = { type: "string", format: "time" };
761
+ break;
279
762
  case "select":
280
763
  case "radio":
281
- schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => (typeof o === "string" ? o : o.value)) : undefined };
764
+ schema = {
765
+ type: "string",
766
+ enum: Array.isArray(field.options)
767
+ ? field.options.map((o) => (typeof o === "string" ? o : o.value))
768
+ : undefined,
769
+ };
282
770
  break;
283
771
  case "multiSelect":
284
772
  schema = {
285
773
  type: "array",
286
- items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => (typeof o === "string" ? o : o.value)) : undefined },
774
+ items: {
775
+ type: "string",
776
+ enum: Array.isArray(field.options)
777
+ ? field.options.map((o) => (typeof o === "string" ? o : o.value))
778
+ : undefined,
779
+ },
287
780
  };
288
781
  break;
289
782
  case "relationship":
290
- schema = { type: 'string', description: `ID of a ${field.relationTo} record` };
783
+ case "image": {
784
+ const valueSchema = {
785
+ type: "string",
786
+ description: `ID of a ${field.relationTo} record`,
787
+ };
788
+ schema = field.hasMany
789
+ ? { type: "array", items: valueSchema }
790
+ : valueSchema;
791
+ break;
792
+ }
793
+ case "join":
794
+ schema = {
795
+ type: "array",
796
+ readOnly: true,
797
+ items: { type: "object", additionalProperties: true },
798
+ };
291
799
  break;
292
800
  case "object": {
293
801
  const { properties, required } = fieldsToProperties(field.fields || []);
@@ -296,7 +804,10 @@ function fieldToSchema(field) {
296
804
  }
297
805
  case "array": {
298
806
  const { properties, required } = fieldsToProperties(field.fields || []);
299
- schema = { type: "array", items: { type: "object", properties, required } };
807
+ schema = {
808
+ type: "array",
809
+ items: { type: "object", properties, required },
810
+ };
300
811
  break;
301
812
  }
302
813
  case "json":