@btst/stack 2.1.0 → 2.3.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 (229) hide show
  1. package/dist/api/index.cjs +9 -1
  2. package/dist/api/index.d.cts +4 -4
  3. package/dist/api/index.d.mts +4 -4
  4. package/dist/api/index.d.ts +4 -4
  5. package/dist/api/index.mjs +9 -1
  6. package/dist/client/index.d.cts +2 -2
  7. package/dist/client/index.d.mts +2 -2
  8. package/dist/client/index.d.ts +2 -2
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +42 -0
  13. package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +39 -0
  14. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
  15. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
  16. package/dist/packages/stack/src/plugins/blog/api/getters.cjs +131 -0
  17. package/dist/packages/stack/src/plugins/blog/api/getters.mjs +127 -0
  18. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +60 -107
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +60 -107
  20. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
  21. package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
  22. package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
  23. package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
  24. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +16 -1
  25. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +17 -2
  26. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +156 -0
  27. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +147 -0
  28. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +624 -617
  29. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +623 -616
  30. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
  31. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
  32. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
  33. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
  34. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
  35. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
  36. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
  37. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
  38. package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +120 -0
  39. package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +112 -0
  40. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +75 -86
  41. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +71 -82
  42. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
  43. package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
  44. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
  45. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
  46. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
  47. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
  48. package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
  49. package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
  50. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +37 -123
  51. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +37 -123
  52. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
  53. package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
  54. package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
  55. package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
  56. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +11 -1
  57. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +12 -2
  58. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  59. package/dist/packages/stack/src/plugins/utils.mjs +6 -1
  60. package/dist/plugins/ai-chat/api/index.cjs +3 -0
  61. package/dist/plugins/ai-chat/api/index.d.cts +27 -4
  62. package/dist/plugins/ai-chat/api/index.d.mts +27 -4
  63. package/dist/plugins/ai-chat/api/index.d.ts +27 -4
  64. package/dist/plugins/ai-chat/api/index.mjs +1 -0
  65. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  66. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  67. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  68. package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
  69. package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
  70. package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
  71. package/dist/plugins/api/index.d.cts +4 -3
  72. package/dist/plugins/api/index.d.mts +4 -3
  73. package/dist/plugins/api/index.d.ts +4 -3
  74. package/dist/plugins/blog/api/index.cjs +9 -0
  75. package/dist/plugins/blog/api/index.d.cts +20 -4
  76. package/dist/plugins/blog/api/index.d.mts +20 -4
  77. package/dist/plugins/blog/api/index.d.ts +20 -4
  78. package/dist/plugins/blog/api/index.mjs +3 -0
  79. package/dist/plugins/blog/client/hooks/index.d.cts +5 -5
  80. package/dist/plugins/blog/client/hooks/index.d.mts +5 -5
  81. package/dist/plugins/blog/client/hooks/index.d.ts +5 -5
  82. package/dist/plugins/blog/client/index.d.cts +1 -1
  83. package/dist/plugins/blog/client/index.d.mts +1 -1
  84. package/dist/plugins/blog/client/index.d.ts +1 -1
  85. package/dist/plugins/blog/query-keys.cjs +13 -9
  86. package/dist/plugins/blog/query-keys.d.cts +8 -333
  87. package/dist/plugins/blog/query-keys.d.mts +8 -333
  88. package/dist/plugins/blog/query-keys.d.ts +8 -333
  89. package/dist/plugins/blog/query-keys.mjs +13 -9
  90. package/dist/plugins/client/index.cjs +1 -0
  91. package/dist/plugins/client/index.d.cts +10 -3
  92. package/dist/plugins/client/index.d.mts +10 -3
  93. package/dist/plugins/client/index.d.ts +10 -3
  94. package/dist/plugins/client/index.mjs +1 -1
  95. package/dist/plugins/cms/api/index.cjs +10 -0
  96. package/dist/plugins/cms/api/index.d.cts +7 -163
  97. package/dist/plugins/cms/api/index.d.mts +7 -163
  98. package/dist/plugins/cms/api/index.d.ts +7 -163
  99. package/dist/plugins/cms/api/index.mjs +2 -0
  100. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  101. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  102. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  103. package/dist/plugins/cms/query-keys.cjs +2 -1
  104. package/dist/plugins/cms/query-keys.d.cts +6 -9
  105. package/dist/plugins/cms/query-keys.d.mts +6 -9
  106. package/dist/plugins/cms/query-keys.d.ts +6 -9
  107. package/dist/plugins/cms/query-keys.mjs +2 -1
  108. package/dist/plugins/form-builder/api/index.cjs +10 -0
  109. package/dist/plugins/form-builder/api/index.d.cts +7 -141
  110. package/dist/plugins/form-builder/api/index.d.mts +7 -141
  111. package/dist/plugins/form-builder/api/index.d.ts +7 -141
  112. package/dist/plugins/form-builder/api/index.mjs +2 -0
  113. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  114. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  115. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  116. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  117. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  118. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  119. package/dist/plugins/form-builder/query-keys.cjs +3 -2
  120. package/dist/plugins/form-builder/query-keys.d.cts +7 -6
  121. package/dist/plugins/form-builder/query-keys.d.mts +7 -6
  122. package/dist/plugins/form-builder/query-keys.d.ts +7 -6
  123. package/dist/plugins/form-builder/query-keys.mjs +3 -2
  124. package/dist/plugins/kanban/api/index.cjs +9 -0
  125. package/dist/plugins/kanban/api/index.d.cts +17 -395
  126. package/dist/plugins/kanban/api/index.d.mts +17 -395
  127. package/dist/plugins/kanban/api/index.d.ts +17 -395
  128. package/dist/plugins/kanban/api/index.mjs +3 -0
  129. package/dist/plugins/kanban/client/components/index.d.cts +1 -1
  130. package/dist/plugins/kanban/client/components/index.d.mts +1 -1
  131. package/dist/plugins/kanban/client/components/index.d.ts +1 -1
  132. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  133. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  134. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  135. package/dist/plugins/kanban/client/index.d.cts +1 -1
  136. package/dist/plugins/kanban/client/index.d.mts +1 -1
  137. package/dist/plugins/kanban/client/index.d.ts +1 -1
  138. package/dist/plugins/kanban/query-keys.cjs +6 -12
  139. package/dist/plugins/kanban/query-keys.d.cts +5 -16
  140. package/dist/plugins/kanban/query-keys.d.mts +5 -16
  141. package/dist/plugins/kanban/query-keys.d.ts +5 -16
  142. package/dist/plugins/kanban/query-keys.mjs +6 -12
  143. package/dist/plugins/open-api/api/index.d.cts +2 -2
  144. package/dist/plugins/open-api/api/index.d.mts +2 -2
  145. package/dist/plugins/open-api/api/index.d.ts +2 -2
  146. package/dist/plugins/route-docs/client/index.d.cts +1 -1
  147. package/dist/plugins/route-docs/client/index.d.mts +1 -1
  148. package/dist/plugins/route-docs/client/index.d.ts +1 -1
  149. package/dist/plugins/ui-builder/index.d.cts +1 -1
  150. package/dist/plugins/ui-builder/index.d.mts +1 -1
  151. package/dist/plugins/ui-builder/index.d.ts +1 -1
  152. package/dist/shared/{stack.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
  153. package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
  154. package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
  155. package/dist/shared/stack.B1EeBt1b.d.ts +297 -0
  156. package/dist/shared/stack.BIXEI6v_.d.mts +419 -0
  157. package/dist/shared/stack.BKfolAyK.d.ts +419 -0
  158. package/dist/shared/stack.BeSm90va.d.ts +289 -0
  159. package/dist/shared/stack.BpolpQpf.d.cts +445 -0
  160. package/dist/shared/stack.C5dtIncc.d.mts +293 -0
  161. package/dist/shared/stack.CIP6QS9l.d.ts +293 -0
  162. package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
  163. package/dist/shared/stack.CP68pFEH.d.mts +297 -0
  164. package/dist/shared/{stack.BsXokfNh.d.mts → stack.CVDTkMoO.d.cts} +8 -2
  165. package/dist/shared/{stack.BsXokfNh.d.ts → stack.CVDTkMoO.d.mts} +8 -2
  166. package/dist/shared/{stack.BsXokfNh.d.cts → stack.CVDTkMoO.d.ts} +8 -2
  167. package/dist/shared/{stack.DKDMI-QO.d.mts → stack.DJaKVY7v.d.cts} +7 -1
  168. package/dist/shared/{stack.DKDMI-QO.d.ts → stack.DJaKVY7v.d.mts} +7 -1
  169. package/dist/shared/{stack.DKDMI-QO.d.cts → stack.DJaKVY7v.d.ts} +7 -1
  170. package/dist/shared/{stack.DzH_wcvr.d.mts → stack.DdI5W6MB.d.cts} +9 -3
  171. package/dist/shared/{stack.DzH_wcvr.d.ts → stack.DdI5W6MB.d.mts} +9 -3
  172. package/dist/shared/{stack.DzH_wcvr.d.cts → stack.DdI5W6MB.d.ts} +9 -3
  173. package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
  174. package/dist/shared/stack.Dw0Ly2TM.d.cts +293 -0
  175. package/dist/shared/stack.IdtKDRka.d.cts +297 -0
  176. package/dist/shared/stack.TIBF2AOx.d.ts +445 -0
  177. package/dist/shared/stack.rTy7-wQU.d.mts +445 -0
  178. package/dist/shared/stack.snB1EDP7.d.cts +419 -0
  179. package/package.json +3 -3
  180. package/src/__tests__/stack-api.test.ts +118 -0
  181. package/src/api/index.ts +15 -1
  182. package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
  183. package/src/plugins/ai-chat/api/getters.ts +71 -0
  184. package/src/plugins/ai-chat/api/index.ts +1 -0
  185. package/src/plugins/ai-chat/api/plugin.ts +8 -0
  186. package/src/plugins/api/index.ts +3 -1
  187. package/src/plugins/blog/__tests__/getters.test.ts +540 -0
  188. package/src/plugins/blog/api/getters.ts +243 -0
  189. package/src/plugins/blog/api/index.ts +9 -0
  190. package/src/plugins/blog/api/plugin.ts +98 -141
  191. package/src/plugins/blog/api/query-key-defs.ts +46 -0
  192. package/src/plugins/blog/api/serializers.ts +27 -0
  193. package/src/plugins/blog/client/plugin.tsx +21 -1
  194. package/src/plugins/blog/query-keys.ts +21 -20
  195. package/src/plugins/client/index.ts +1 -1
  196. package/src/plugins/cms/__tests__/getters.test.ts +206 -0
  197. package/src/plugins/cms/api/getters.ts +268 -0
  198. package/src/plugins/cms/api/index.ts +15 -1
  199. package/src/plugins/cms/api/plugin.ts +151 -150
  200. package/src/plugins/cms/api/query-key-defs.ts +53 -0
  201. package/src/plugins/cms/api/serializers.ts +12 -0
  202. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
  203. package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
  204. package/src/plugins/cms/client/plugin.tsx +19 -0
  205. package/src/plugins/cms/query-keys.ts +2 -1
  206. package/src/plugins/cms/types.ts +1 -1
  207. package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
  208. package/src/plugins/form-builder/api/getters.ts +226 -0
  209. package/src/plugins/form-builder/api/index.ts +15 -1
  210. package/src/plugins/form-builder/api/plugin.ts +107 -109
  211. package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
  212. package/src/plugins/form-builder/api/serializers.ts +12 -0
  213. package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
  214. package/src/plugins/form-builder/client/plugin.tsx +19 -0
  215. package/src/plugins/form-builder/query-keys.ts +6 -2
  216. package/src/plugins/form-builder/types.ts +2 -2
  217. package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
  218. package/src/plugins/kanban/api/getters.ts +149 -0
  219. package/src/plugins/kanban/api/index.ts +4 -0
  220. package/src/plugins/kanban/api/plugin.ts +65 -146
  221. package/src/plugins/kanban/api/query-key-defs.ts +54 -0
  222. package/src/plugins/kanban/api/serializers.ts +49 -0
  223. package/src/plugins/kanban/client/plugin.tsx +15 -1
  224. package/src/plugins/kanban/query-keys.ts +10 -14
  225. package/src/plugins/utils.ts +19 -0
  226. package/src/types.ts +44 -5
  227. package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CBON0dWL.d.cts} +7 -7
  228. package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CBON0dWL.d.mts} +7 -7
  229. package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CBON0dWL.d.ts} +7 -7
@@ -4,54 +4,9 @@ import { formSchemaToZod, zodToFormSchema } from '../../../../../ui/src/lib/sche
4
4
  import { cmsSchema } from '../db.mjs';
5
5
  import { listContentQuerySchema } from '../schemas.mjs';
6
6
  import { slugify } from '../utils.mjs';
7
+ import { serializeContentType, getAllContentItems, serializeContentItemWithType, serializeContentItem, getContentItemById, getContentItemBySlug, getAllContentTypes } from './getters.mjs';
8
+ import { CMS_QUERY_KEYS } from './query-key-defs.mjs';
7
9
 
8
- function migrateToUnifiedSchema(jsonSchemaStr, fieldConfigStr) {
9
- if (!fieldConfigStr) {
10
- return jsonSchemaStr;
11
- }
12
- try {
13
- const jsonSchema = JSON.parse(jsonSchemaStr);
14
- const fieldConfig = JSON.parse(fieldConfigStr);
15
- if (!jsonSchema.properties || typeof fieldConfig !== "object") {
16
- return jsonSchemaStr;
17
- }
18
- for (const [key, config] of Object.entries(fieldConfig)) {
19
- if (jsonSchema.properties[key] && typeof config === "object" && config !== null && "fieldType" in config) {
20
- jsonSchema.properties[key].fieldType = config.fieldType;
21
- }
22
- }
23
- return JSON.stringify(jsonSchema);
24
- } catch {
25
- return jsonSchemaStr;
26
- }
27
- }
28
- function serializeContentType(ct) {
29
- const needsMigration = !ct.autoFormVersion || ct.autoFormVersion < 2;
30
- const migratedJsonSchema = needsMigration ? migrateToUnifiedSchema(ct.jsonSchema, ct.fieldConfig) : ct.jsonSchema;
31
- return {
32
- id: ct.id,
33
- name: ct.name,
34
- slug: ct.slug,
35
- description: ct.description,
36
- jsonSchema: migratedJsonSchema,
37
- createdAt: ct.createdAt.toISOString(),
38
- updatedAt: ct.updatedAt.toISOString()
39
- };
40
- }
41
- function serializeContentItem(item) {
42
- return {
43
- ...item,
44
- createdAt: item.createdAt.toISOString(),
45
- updatedAt: item.updatedAt.toISOString()
46
- };
47
- }
48
- function serializeContentItemWithType(item) {
49
- return {
50
- ...serializeContentItem(item),
51
- parsedData: JSON.parse(item.data),
52
- contentType: item.contentType ? serializeContentType(item.contentType) : void 0
53
- };
54
- }
55
10
  async function syncContentTypes(adapter, config) {
56
11
  for (const ct of config.contentTypes) {
57
12
  const jsonSchema = JSON.stringify(zodToFormSchema(ct.schema));
@@ -276,268 +231,239 @@ async function populateRelations(adapter, item) {
276
231
  }
277
232
  return relations;
278
233
  }
279
- const cmsBackendPlugin = (config) => defineBackendPlugin({
280
- name: "cms",
281
- dbPlugin: cmsSchema,
282
- routes: (adapter) => {
283
- let syncPromise = null;
284
- const ensureSynced = async () => {
285
- if (!syncPromise) {
286
- syncPromise = syncContentTypes(adapter, config).catch((err) => {
287
- syncPromise = null;
288
- throw err;
289
- });
290
- }
291
- await syncPromise;
292
- };
293
- const getContentType = async (slug) => {
294
- await ensureSynced();
295
- return adapter.findOne({
296
- model: "contentType",
297
- where: [{ field: "slug", value: slug, operator: "eq" }]
234
+ const cmsBackendPlugin = (config) => {
235
+ let syncPromise = null;
236
+ const ensureSynced = (adapter) => {
237
+ if (!syncPromise) {
238
+ syncPromise = syncContentTypes(adapter, config).catch((err) => {
239
+ syncPromise = null;
240
+ throw err;
298
241
  });
299
- };
300
- const createContext = (typeSlug, headers) => ({
301
- typeSlug,
302
- headers
303
- });
304
- const listContentTypes = createEndpoint(
305
- "/content-types",
306
- { method: "GET" },
307
- async (ctx) => {
308
- await ensureSynced();
309
- const contentTypes = await adapter.findMany({
310
- model: "contentType",
311
- sortBy: { field: "name", direction: "asc" }
242
+ }
243
+ return syncPromise;
244
+ };
245
+ const getContentTypesWithCounts = async (adapter) => {
246
+ const contentTypes = await getAllContentTypes(adapter);
247
+ return Promise.all(
248
+ contentTypes.map(async (ct) => {
249
+ const count = await adapter.count({
250
+ model: "contentItem",
251
+ where: [
252
+ { field: "contentTypeId", value: ct.id, operator: "eq" }
253
+ ]
312
254
  });
313
- const typesWithCounts = await Promise.all(
314
- contentTypes.map(async (ct) => {
315
- const items = await adapter.findMany({
316
- model: "contentItem",
317
- where: [
255
+ return { ...ct, itemCount: count };
256
+ })
257
+ );
258
+ };
259
+ const createCMSPrefetchForRoute = (adapter) => {
260
+ return async function prefetchForRoute(key, qc, params) {
261
+ await ensureSynced(adapter);
262
+ switch (key) {
263
+ case "dashboard":
264
+ case "newContent": {
265
+ const typesWithCounts = await getContentTypesWithCounts(adapter);
266
+ qc.setQueryData(CMS_QUERY_KEYS.typesList(), typesWithCounts);
267
+ break;
268
+ }
269
+ case "contentList": {
270
+ const typeSlug = params?.typeSlug ?? "";
271
+ const [contentTypes, contentItems] = await Promise.all([
272
+ getContentTypesWithCounts(adapter),
273
+ getAllContentItems(adapter, typeSlug, { limit: 20, offset: 0 })
274
+ ]);
275
+ qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
276
+ qc.setQueryData(
277
+ CMS_QUERY_KEYS.contentList({ typeSlug, limit: 20, offset: 0 }),
278
+ {
279
+ pages: [
318
280
  {
319
- field: "contentTypeId",
320
- value: ct.id,
321
- operator: "eq"
281
+ items: contentItems.items,
282
+ total: contentItems.total,
283
+ limit: contentItems.limit ?? 20,
284
+ offset: contentItems.offset ?? 0
322
285
  }
323
- ]
324
- });
325
- return {
326
- ...serializeContentType(ct),
327
- itemCount: items.length
328
- };
329
- })
330
- );
331
- return typesWithCounts;
332
- }
333
- );
334
- const getContentTypeBySlug = createEndpoint(
335
- "/content-types/:slug",
336
- {
337
- method: "GET",
338
- params: z.object({ slug: z.string() })
339
- },
340
- async (ctx) => {
341
- const { slug } = ctx.params;
342
- const contentType = await getContentType(slug);
343
- if (!contentType) {
344
- throw ctx.error(404, { message: "Content type not found" });
286
+ ],
287
+ pageParams: [0]
288
+ }
289
+ );
290
+ break;
291
+ }
292
+ case "editContent": {
293
+ const typeSlug = params?.typeSlug ?? "";
294
+ const id = params?.id ?? "";
295
+ const [contentTypes, item] = await Promise.all([
296
+ getContentTypesWithCounts(adapter),
297
+ id ? getContentItemById(adapter, id) : Promise.resolve(null)
298
+ ]);
299
+ qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
300
+ if (id) {
301
+ qc.setQueryData(CMS_QUERY_KEYS.contentDetail(typeSlug, id), item);
302
+ }
303
+ break;
345
304
  }
346
- return serializeContentType(contentType);
347
305
  }
348
- );
349
- const listContentItems = createEndpoint(
350
- "/content/:typeSlug",
351
- {
352
- method: "GET",
353
- params: z.object({ typeSlug: z.string() }),
354
- query: listContentQuerySchema
306
+ };
307
+ };
308
+ return defineBackendPlugin({
309
+ name: "cms",
310
+ dbPlugin: cmsSchema,
311
+ api: (adapter) => ({
312
+ getAllContentTypes: async () => {
313
+ await ensureSynced(adapter);
314
+ return getAllContentTypes(adapter);
355
315
  },
356
- async (ctx) => {
357
- const { typeSlug } = ctx.params;
358
- const { slug, limit, offset } = ctx.query;
359
- const contentType = await getContentType(typeSlug);
360
- if (!contentType) {
361
- throw ctx.error(404, { message: "Content type not found" });
362
- }
363
- const whereConditions = [
364
- {
365
- field: "contentTypeId",
366
- value: contentType.id,
367
- operator: "eq"
368
- }
369
- ];
370
- if (slug) {
371
- whereConditions.push({
372
- field: "slug",
373
- value: slug,
374
- operator: "eq"
375
- });
376
- }
377
- const allItems = await adapter.findMany({
378
- model: "contentItem",
379
- where: whereConditions
380
- });
381
- const total = allItems.length;
382
- const items = await adapter.findMany({
383
- model: "contentItem",
384
- where: whereConditions,
385
- limit,
386
- offset,
387
- sortBy: { field: "createdAt", direction: "desc" },
388
- join: { contentType: true }
389
- });
390
- return {
391
- items: items.map(serializeContentItemWithType),
392
- total,
393
- limit,
394
- offset
395
- };
396
- }
397
- );
398
- const getContentItem = createEndpoint(
399
- "/content/:typeSlug/:id",
400
- {
401
- method: "GET",
402
- params: z.object({ typeSlug: z.string(), id: z.string() })
316
+ getAllContentItems: async (contentTypeSlug, params) => {
317
+ await ensureSynced(adapter);
318
+ return getAllContentItems(adapter, contentTypeSlug, params);
403
319
  },
404
- async (ctx) => {
405
- const { typeSlug, id } = ctx.params;
406
- const contentType = await getContentType(typeSlug);
407
- if (!contentType) {
408
- throw ctx.error(404, { message: "Content type not found" });
409
- }
410
- const item = await adapter.findOne({
411
- model: "contentItem",
412
- where: [{ field: "id", value: id, operator: "eq" }],
413
- join: { contentType: true }
414
- });
415
- if (!item || item.contentTypeId !== contentType.id) {
416
- throw ctx.error(404, { message: "Content item not found" });
417
- }
418
- return serializeContentItemWithType(item);
419
- }
420
- );
421
- const createContentItem = createEndpoint(
422
- "/content/:typeSlug",
423
- {
424
- method: "POST",
425
- params: z.object({ typeSlug: z.string() }),
426
- body: z.object({
427
- slug: z.string().min(1),
428
- // Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
429
- data: z.object({}).passthrough()
430
- })
320
+ getContentItemBySlug: async (contentTypeSlug, slug) => {
321
+ await ensureSynced(adapter);
322
+ return getContentItemBySlug(adapter, contentTypeSlug, slug);
431
323
  },
432
- async (ctx) => {
433
- const { typeSlug } = ctx.params;
434
- const { slug: rawSlug, data } = ctx.body;
435
- const context = createContext(typeSlug, ctx.headers);
436
- const slug = slugify(rawSlug);
437
- if (!slug) {
438
- throw ctx.error(400, {
439
- message: "Invalid slug: must contain at least one alphanumeric character"
440
- });
441
- }
442
- const contentType = await getContentType(typeSlug);
443
- if (!contentType) {
444
- throw ctx.error(404, { message: "Content type not found" });
445
- }
446
- const { processedData: dataWithResolvedRelations, relationIds } = await processRelationsInData(
447
- adapter,
448
- contentType,
449
- data,
450
- getContentType
451
- );
452
- const zodSchema = getContentTypeZodSchema(contentType);
453
- const validation = zodSchema.safeParse(dataWithResolvedRelations);
454
- if (!validation.success) {
455
- throw ctx.error(400, {
456
- message: "Validation failed",
457
- errors: validation.error.issues
458
- });
459
- }
460
- const existing = await adapter.findOne({
461
- model: "contentItem",
462
- where: [
463
- {
464
- field: "contentTypeId",
465
- value: contentType.id,
466
- operator: "eq"
467
- },
468
- { field: "slug", value: slug, operator: "eq" }
469
- ]
324
+ getContentItemById: async (id) => {
325
+ await ensureSynced(adapter);
326
+ return getContentItemById(adapter, id);
327
+ },
328
+ prefetchForRoute: createCMSPrefetchForRoute(adapter)
329
+ }),
330
+ routes: (adapter) => {
331
+ const getContentType = async (slug) => {
332
+ await ensureSynced(adapter);
333
+ return adapter.findOne({
334
+ model: "contentType",
335
+ where: [{ field: "slug", value: slug, operator: "eq" }]
470
336
  });
471
- if (existing) {
472
- throw ctx.error(409, {
473
- message: "Content item with this slug already exists"
337
+ };
338
+ const createContext = (typeSlug, headers) => ({
339
+ typeSlug,
340
+ headers
341
+ });
342
+ const listContentTypes = createEndpoint(
343
+ "/content-types",
344
+ { method: "GET" },
345
+ async (ctx) => {
346
+ await ensureSynced(adapter);
347
+ const contentTypes = await adapter.findMany({
348
+ model: "contentType",
349
+ sortBy: { field: "name", direction: "asc" }
474
350
  });
475
- }
476
- const processedData = validation.data;
477
- if (config.hooks?.onBeforeCreate) {
478
- const result = await config.hooks.onBeforeCreate(
479
- processedData,
480
- context
351
+ const typesWithCounts = await Promise.all(
352
+ contentTypes.map(async (ct) => {
353
+ const items = await adapter.findMany({
354
+ model: "contentItem",
355
+ where: [
356
+ {
357
+ field: "contentTypeId",
358
+ value: ct.id,
359
+ operator: "eq"
360
+ }
361
+ ]
362
+ });
363
+ return {
364
+ ...serializeContentType(ct),
365
+ itemCount: items.length
366
+ };
367
+ })
481
368
  );
482
- if (result === false) {
483
- throw ctx.error(403, { message: "Create operation denied" });
369
+ return typesWithCounts;
370
+ }
371
+ );
372
+ const getContentTypeBySlug = createEndpoint(
373
+ "/content-types/:slug",
374
+ {
375
+ method: "GET",
376
+ params: z.object({ slug: z.string() })
377
+ },
378
+ async (ctx) => {
379
+ const { slug } = ctx.params;
380
+ const contentType = await getContentType(slug);
381
+ if (!contentType) {
382
+ throw ctx.error(404, { message: "Content type not found" });
484
383
  }
384
+ return serializeContentType(contentType);
485
385
  }
486
- const item = await adapter.create({
487
- model: "contentItem",
488
- data: {
489
- contentTypeId: contentType.id,
490
- slug,
491
- data: JSON.stringify(processedData),
492
- createdAt: /* @__PURE__ */ new Date(),
493
- updatedAt: /* @__PURE__ */ new Date()
386
+ );
387
+ const listContentItems = createEndpoint(
388
+ "/content/:typeSlug",
389
+ {
390
+ method: "GET",
391
+ params: z.object({ typeSlug: z.string() }),
392
+ query: listContentQuerySchema
393
+ },
394
+ async (ctx) => {
395
+ const { typeSlug } = ctx.params;
396
+ const { slug, limit, offset } = ctx.query;
397
+ const contentType = await getContentType(typeSlug);
398
+ if (!contentType) {
399
+ throw ctx.error(404, { message: "Content type not found" });
494
400
  }
495
- });
496
- await syncRelations(adapter, item.id, relationIds);
497
- const serialized = serializeContentItem(item);
498
- if (config.hooks?.onAfterCreate) {
499
- await config.hooks.onAfterCreate(serialized, context);
401
+ return getAllContentItems(adapter, typeSlug, { slug, limit, offset });
500
402
  }
501
- return {
502
- ...serialized,
503
- parsedData: processedData
504
- };
505
- }
506
- );
507
- const updateContentItem = createEndpoint(
508
- "/content/:typeSlug/:id",
509
- {
510
- method: "PUT",
511
- params: z.object({ typeSlug: z.string(), id: z.string() }),
512
- body: z.object({
513
- slug: z.string().min(1).optional(),
514
- // Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
515
- data: z.object({}).passthrough().optional()
516
- })
517
- },
518
- async (ctx) => {
519
- const { typeSlug, id } = ctx.params;
520
- const { slug: rawSlug, data } = ctx.body;
521
- const context = createContext(typeSlug, ctx.headers);
522
- const slug = rawSlug ? slugify(rawSlug) : void 0;
523
- if (rawSlug && !slug) {
524
- throw ctx.error(400, {
525
- message: "Invalid slug: must contain at least one alphanumeric character"
403
+ );
404
+ const getContentItem = createEndpoint(
405
+ "/content/:typeSlug/:id",
406
+ {
407
+ method: "GET",
408
+ params: z.object({ typeSlug: z.string(), id: z.string() })
409
+ },
410
+ async (ctx) => {
411
+ const { typeSlug, id } = ctx.params;
412
+ const contentType = await getContentType(typeSlug);
413
+ if (!contentType) {
414
+ throw ctx.error(404, { message: "Content type not found" });
415
+ }
416
+ const item = await adapter.findOne({
417
+ model: "contentItem",
418
+ where: [{ field: "id", value: id, operator: "eq" }],
419
+ join: { contentType: true }
526
420
  });
421
+ if (!item || item.contentTypeId !== contentType.id) {
422
+ throw ctx.error(404, { message: "Content item not found" });
423
+ }
424
+ return serializeContentItemWithType(item);
527
425
  }
528
- const contentType = await getContentType(typeSlug);
529
- if (!contentType) {
530
- throw ctx.error(404, { message: "Content type not found" });
531
- }
532
- const existing = await adapter.findOne({
533
- model: "contentItem",
534
- where: [{ field: "id", value: id, operator: "eq" }]
535
- });
536
- if (!existing || existing.contentTypeId !== contentType.id) {
537
- throw ctx.error(404, { message: "Content item not found" });
538
- }
539
- if (slug && slug !== existing.slug) {
540
- const duplicate = await adapter.findOne({
426
+ );
427
+ const createContentItem = createEndpoint(
428
+ "/content/:typeSlug",
429
+ {
430
+ method: "POST",
431
+ params: z.object({ typeSlug: z.string() }),
432
+ body: z.object({
433
+ slug: z.string().min(1),
434
+ // Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
435
+ data: z.object({}).passthrough()
436
+ })
437
+ },
438
+ async (ctx) => {
439
+ const { typeSlug } = ctx.params;
440
+ const { slug: rawSlug, data } = ctx.body;
441
+ const context = createContext(typeSlug, ctx.headers);
442
+ const slug = slugify(rawSlug);
443
+ if (!slug) {
444
+ throw ctx.error(400, {
445
+ message: "Invalid slug: must contain at least one alphanumeric character"
446
+ });
447
+ }
448
+ const contentType = await getContentType(typeSlug);
449
+ if (!contentType) {
450
+ throw ctx.error(404, { message: "Content type not found" });
451
+ }
452
+ const { processedData: dataWithResolvedRelations, relationIds } = await processRelationsInData(
453
+ adapter,
454
+ contentType,
455
+ data,
456
+ getContentType
457
+ );
458
+ const zodSchema = getContentTypeZodSchema(contentType);
459
+ const validation = zodSchema.safeParse(dataWithResolvedRelations);
460
+ if (!validation.success) {
461
+ throw ctx.error(400, {
462
+ message: "Validation failed",
463
+ errors: validation.error.issues
464
+ });
465
+ }
466
+ const existing = await adapter.findOne({
541
467
  model: "contentItem",
542
468
  where: [
543
469
  {
@@ -548,367 +474,448 @@ const cmsBackendPlugin = (config) => defineBackendPlugin({
548
474
  { field: "slug", value: slug, operator: "eq" }
549
475
  ]
550
476
  });
551
- if (duplicate) {
477
+ if (existing) {
552
478
  throw ctx.error(409, {
553
479
  message: "Content item with this slug already exists"
554
480
  });
555
481
  }
556
- }
557
- let dataWithResolvedRelations;
558
- let relationIds;
559
- if (data) {
560
- const result = await processRelationsInData(
561
- adapter,
562
- contentType,
563
- data,
564
- getContentType
565
- );
566
- dataWithResolvedRelations = result.processedData;
567
- relationIds = result.relationIds;
568
- }
569
- let validatedData = dataWithResolvedRelations;
570
- if (dataWithResolvedRelations) {
571
- const existingData = existing.data ? JSON.parse(existing.data) : {};
572
- const mergedData = {
573
- ...existingData,
574
- ...dataWithResolvedRelations
482
+ const processedData = validation.data;
483
+ if (config.hooks?.onBeforeCreate) {
484
+ const result = await config.hooks.onBeforeCreate(
485
+ processedData,
486
+ context
487
+ );
488
+ if (result === false) {
489
+ throw ctx.error(403, { message: "Create operation denied" });
490
+ }
491
+ }
492
+ const item = await adapter.create({
493
+ model: "contentItem",
494
+ data: {
495
+ contentTypeId: contentType.id,
496
+ slug,
497
+ data: JSON.stringify(processedData),
498
+ createdAt: /* @__PURE__ */ new Date(),
499
+ updatedAt: /* @__PURE__ */ new Date()
500
+ }
501
+ });
502
+ await syncRelations(adapter, item.id, relationIds);
503
+ const serialized = serializeContentItem(item);
504
+ if (config.hooks?.onAfterCreate) {
505
+ await config.hooks.onAfterCreate(serialized, context);
506
+ }
507
+ return {
508
+ ...serialized,
509
+ parsedData: processedData
575
510
  };
576
- const zodSchema = getContentTypeZodSchema(contentType);
577
- const validation = zodSchema.safeParse(mergedData);
578
- if (!validation.success) {
511
+ }
512
+ );
513
+ const updateContentItem = createEndpoint(
514
+ "/content/:typeSlug/:id",
515
+ {
516
+ method: "PUT",
517
+ params: z.object({ typeSlug: z.string(), id: z.string() }),
518
+ body: z.object({
519
+ slug: z.string().min(1).optional(),
520
+ // Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
521
+ data: z.object({}).passthrough().optional()
522
+ })
523
+ },
524
+ async (ctx) => {
525
+ const { typeSlug, id } = ctx.params;
526
+ const { slug: rawSlug, data } = ctx.body;
527
+ const context = createContext(typeSlug, ctx.headers);
528
+ const slug = rawSlug ? slugify(rawSlug) : void 0;
529
+ if (rawSlug && !slug) {
579
530
  throw ctx.error(400, {
580
- message: "Validation failed",
581
- errors: validation.error.issues
531
+ message: "Invalid slug: must contain at least one alphanumeric character"
582
532
  });
583
533
  }
584
- validatedData = validation.data;
585
- }
586
- const processedData = validatedData;
587
- if (config.hooks?.onBeforeUpdate && validatedData) {
588
- const result = await config.hooks.onBeforeUpdate(
589
- id,
590
- validatedData,
591
- context
592
- );
593
- if (result === false) {
594
- throw ctx.error(403, { message: "Update operation denied" });
534
+ const contentType = await getContentType(typeSlug);
535
+ if (!contentType) {
536
+ throw ctx.error(404, { message: "Content type not found" });
595
537
  }
596
- }
597
- if (relationIds) {
598
- await syncRelations(adapter, id, relationIds);
599
- }
600
- const updateData = {
601
- updatedAt: /* @__PURE__ */ new Date()
602
- };
603
- if (slug) updateData.slug = slug;
604
- if (processedData) updateData.data = JSON.stringify(processedData);
605
- await adapter.update({
606
- model: "contentItem",
607
- where: [{ field: "id", value: id, operator: "eq" }],
608
- update: updateData
609
- });
610
- const updated = await adapter.findOne({
611
- model: "contentItem",
612
- where: [{ field: "id", value: id, operator: "eq" }],
613
- join: { contentType: true }
614
- });
615
- if (!updated) {
616
- throw ctx.error(500, { message: "Failed to fetch updated item" });
617
- }
618
- const serialized = serializeContentItem(updated);
619
- if (config.hooks?.onAfterUpdate) {
620
- await config.hooks.onAfterUpdate(serialized, context);
621
- }
622
- return serializeContentItemWithType(updated);
623
- }
624
- );
625
- const deleteContentItem = createEndpoint(
626
- "/content/:typeSlug/:id",
627
- {
628
- method: "DELETE",
629
- params: z.object({ typeSlug: z.string(), id: z.string() })
630
- },
631
- async (ctx) => {
632
- const { typeSlug, id } = ctx.params;
633
- const context = createContext(typeSlug, ctx.headers);
634
- const contentType = await getContentType(typeSlug);
635
- if (!contentType) {
636
- throw ctx.error(404, { message: "Content type not found" });
637
- }
638
- const existing = await adapter.findOne({
639
- model: "contentItem",
640
- where: [{ field: "id", value: id, operator: "eq" }]
641
- });
642
- if (!existing || existing.contentTypeId !== contentType.id) {
643
- throw ctx.error(404, { message: "Content item not found" });
644
- }
645
- if (config.hooks?.onBeforeDelete) {
646
- const canDelete = await config.hooks.onBeforeDelete(id, context);
647
- if (!canDelete) {
648
- throw ctx.error(403, { message: "Delete operation denied" });
538
+ const existing = await adapter.findOne({
539
+ model: "contentItem",
540
+ where: [{ field: "id", value: id, operator: "eq" }]
541
+ });
542
+ if (!existing || existing.contentTypeId !== contentType.id) {
543
+ throw ctx.error(404, { message: "Content item not found" });
649
544
  }
650
- }
651
- await adapter.delete({
652
- model: "contentItem",
653
- where: [{ field: "id", value: id, operator: "eq" }]
654
- });
655
- if (config.hooks?.onAfterDelete) {
656
- await config.hooks.onAfterDelete(id, context);
657
- }
658
- return { success: true };
659
- }
660
- );
661
- const getContentItemPopulated = createEndpoint(
662
- "/content/:typeSlug/:id/populated",
663
- {
664
- method: "GET",
665
- params: z.object({ typeSlug: z.string(), id: z.string() })
666
- },
667
- async (ctx) => {
668
- const { typeSlug, id } = ctx.params;
669
- const contentType = await getContentType(typeSlug);
670
- if (!contentType) {
671
- throw ctx.error(404, { message: "Content type not found" });
672
- }
673
- const item = await adapter.findOne({
674
- model: "contentItem",
675
- where: [{ field: "id", value: id, operator: "eq" }],
676
- join: { contentType: true }
677
- });
678
- if (!item || item.contentTypeId !== contentType.id) {
679
- throw ctx.error(404, { message: "Content item not found" });
680
- }
681
- const _relations = await populateRelations(adapter, item);
682
- return {
683
- ...serializeContentItemWithType(item),
684
- _relations
685
- };
686
- }
687
- );
688
- const listContentByRelation = createEndpoint(
689
- "/content/:typeSlug/by-relation",
690
- {
691
- method: "GET",
692
- params: z.object({ typeSlug: z.string() }),
693
- query: z.object({
694
- field: z.string(),
695
- targetId: z.string(),
696
- limit: z.coerce.number().min(1).max(100).optional().default(20),
697
- offset: z.coerce.number().min(0).optional().default(0)
698
- })
699
- },
700
- async (ctx) => {
701
- const { typeSlug } = ctx.params;
702
- const { field, targetId, limit, offset } = ctx.query;
703
- const contentType = await getContentType(typeSlug);
704
- if (!contentType) {
705
- throw ctx.error(404, { message: "Content type not found" });
706
- }
707
- const contentRelations = await adapter.findMany({
708
- model: "contentRelation",
709
- where: [
710
- { field: "targetId", value: targetId, operator: "eq" },
711
- { field: "fieldName", value: field, operator: "eq" }
712
- ]
713
- });
714
- const sourceIds = [
715
- ...new Set(contentRelations.map((r) => r.sourceId))
716
- ];
717
- if (sourceIds.length === 0) {
718
- return {
719
- items: [],
720
- total: 0,
721
- limit,
722
- offset
545
+ if (slug && slug !== existing.slug) {
546
+ const duplicate = await adapter.findOne({
547
+ model: "contentItem",
548
+ where: [
549
+ {
550
+ field: "contentTypeId",
551
+ value: contentType.id,
552
+ operator: "eq"
553
+ },
554
+ { field: "slug", value: slug, operator: "eq" }
555
+ ]
556
+ });
557
+ if (duplicate) {
558
+ throw ctx.error(409, {
559
+ message: "Content item with this slug already exists"
560
+ });
561
+ }
562
+ }
563
+ let dataWithResolvedRelations;
564
+ let relationIds;
565
+ if (data) {
566
+ const result = await processRelationsInData(
567
+ adapter,
568
+ contentType,
569
+ data,
570
+ getContentType
571
+ );
572
+ dataWithResolvedRelations = result.processedData;
573
+ relationIds = result.relationIds;
574
+ }
575
+ let validatedData = dataWithResolvedRelations;
576
+ if (dataWithResolvedRelations) {
577
+ const existingData = existing.data ? JSON.parse(existing.data) : {};
578
+ const mergedData = {
579
+ ...existingData,
580
+ ...dataWithResolvedRelations
581
+ };
582
+ const zodSchema = getContentTypeZodSchema(contentType);
583
+ const validation = zodSchema.safeParse(mergedData);
584
+ if (!validation.success) {
585
+ throw ctx.error(400, {
586
+ message: "Validation failed",
587
+ errors: validation.error.issues
588
+ });
589
+ }
590
+ validatedData = validation.data;
591
+ }
592
+ const processedData = validatedData;
593
+ if (config.hooks?.onBeforeUpdate && validatedData) {
594
+ const result = await config.hooks.onBeforeUpdate(
595
+ id,
596
+ validatedData,
597
+ context
598
+ );
599
+ if (result === false) {
600
+ throw ctx.error(403, { message: "Update operation denied" });
601
+ }
602
+ }
603
+ if (relationIds) {
604
+ await syncRelations(adapter, id, relationIds);
605
+ }
606
+ const updateData = {
607
+ updatedAt: /* @__PURE__ */ new Date()
723
608
  };
609
+ if (slug) updateData.slug = slug;
610
+ if (processedData) updateData.data = JSON.stringify(processedData);
611
+ await adapter.update({
612
+ model: "contentItem",
613
+ where: [{ field: "id", value: id, operator: "eq" }],
614
+ update: updateData
615
+ });
616
+ const updated = await adapter.findOne({
617
+ model: "contentItem",
618
+ where: [{ field: "id", value: id, operator: "eq" }],
619
+ join: { contentType: true }
620
+ });
621
+ if (!updated) {
622
+ throw ctx.error(500, { message: "Failed to fetch updated item" });
623
+ }
624
+ const serialized = serializeContentItem(updated);
625
+ if (config.hooks?.onAfterUpdate) {
626
+ await config.hooks.onAfterUpdate(serialized, context);
627
+ }
628
+ return serializeContentItemWithType(updated);
724
629
  }
725
- const allItems = [];
726
- for (const sourceId of sourceIds) {
630
+ );
631
+ const deleteContentItem = createEndpoint(
632
+ "/content/:typeSlug/:id",
633
+ {
634
+ method: "DELETE",
635
+ params: z.object({ typeSlug: z.string(), id: z.string() })
636
+ },
637
+ async (ctx) => {
638
+ const { typeSlug, id } = ctx.params;
639
+ const context = createContext(typeSlug, ctx.headers);
640
+ const contentType = await getContentType(typeSlug);
641
+ if (!contentType) {
642
+ throw ctx.error(404, { message: "Content type not found" });
643
+ }
644
+ const existing = await adapter.findOne({
645
+ model: "contentItem",
646
+ where: [{ field: "id", value: id, operator: "eq" }]
647
+ });
648
+ if (!existing || existing.contentTypeId !== contentType.id) {
649
+ throw ctx.error(404, { message: "Content item not found" });
650
+ }
651
+ if (config.hooks?.onBeforeDelete) {
652
+ const canDelete = await config.hooks.onBeforeDelete(id, context);
653
+ if (!canDelete) {
654
+ throw ctx.error(403, { message: "Delete operation denied" });
655
+ }
656
+ }
657
+ await adapter.delete({
658
+ model: "contentItem",
659
+ where: [{ field: "id", value: id, operator: "eq" }]
660
+ });
661
+ if (config.hooks?.onAfterDelete) {
662
+ await config.hooks.onAfterDelete(id, context);
663
+ }
664
+ return { success: true };
665
+ }
666
+ );
667
+ const getContentItemPopulated = createEndpoint(
668
+ "/content/:typeSlug/:id/populated",
669
+ {
670
+ method: "GET",
671
+ params: z.object({ typeSlug: z.string(), id: z.string() })
672
+ },
673
+ async (ctx) => {
674
+ const { typeSlug, id } = ctx.params;
675
+ const contentType = await getContentType(typeSlug);
676
+ if (!contentType) {
677
+ throw ctx.error(404, { message: "Content type not found" });
678
+ }
727
679
  const item = await adapter.findOne({
728
680
  model: "contentItem",
729
- where: [
730
- { field: "id", value: sourceId, operator: "eq" },
731
- {
732
- field: "contentTypeId",
733
- value: contentType.id,
734
- operator: "eq"
735
- }
736
- ],
681
+ where: [{ field: "id", value: id, operator: "eq" }],
737
682
  join: { contentType: true }
738
683
  });
739
- if (item) {
740
- allItems.push(item);
684
+ if (!item || item.contentTypeId !== contentType.id) {
685
+ throw ctx.error(404, { message: "Content item not found" });
741
686
  }
687
+ const _relations = await populateRelations(adapter, item);
688
+ return {
689
+ ...serializeContentItemWithType(item),
690
+ _relations
691
+ };
742
692
  }
743
- allItems.sort(
744
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
745
- );
746
- const total = allItems.length;
747
- const paginatedItems = allItems.slice(offset, offset + limit);
748
- return {
749
- items: paginatedItems.map(serializeContentItemWithType),
750
- total,
751
- limit,
752
- offset
753
- };
754
- }
755
- );
756
- const getInverseRelations = createEndpoint(
757
- "/content-types/:slug/inverse-relations",
758
- {
759
- method: "GET",
760
- params: z.object({ slug: z.string() }),
761
- query: z.object({
762
- itemId: z.string().optional()
763
- })
764
- },
765
- async (ctx) => {
766
- const { slug } = ctx.params;
767
- const { itemId } = ctx.query;
768
- await ensureSynced();
769
- const targetContentType = await getContentType(slug);
770
- if (!targetContentType) {
771
- throw ctx.error(404, { message: "Content type not found" });
693
+ );
694
+ const listContentByRelation = createEndpoint(
695
+ "/content/:typeSlug/by-relation",
696
+ {
697
+ method: "GET",
698
+ params: z.object({ typeSlug: z.string() }),
699
+ query: z.object({
700
+ field: z.string(),
701
+ targetId: z.string(),
702
+ limit: z.coerce.number().min(1).max(100).optional().default(20),
703
+ offset: z.coerce.number().min(0).optional().default(0)
704
+ })
705
+ },
706
+ async (ctx) => {
707
+ const { typeSlug } = ctx.params;
708
+ const { field, targetId, limit, offset } = ctx.query;
709
+ const contentType = await getContentType(typeSlug);
710
+ if (!contentType) {
711
+ throw ctx.error(404, { message: "Content type not found" });
712
+ }
713
+ const contentRelations = await adapter.findMany({
714
+ model: "contentRelation",
715
+ where: [
716
+ { field: "targetId", value: targetId, operator: "eq" },
717
+ { field: "fieldName", value: field, operator: "eq" }
718
+ ]
719
+ });
720
+ const sourceIds = [
721
+ ...new Set(contentRelations.map((r) => r.sourceId))
722
+ ];
723
+ if (sourceIds.length === 0) {
724
+ return {
725
+ items: [],
726
+ total: 0,
727
+ limit,
728
+ offset
729
+ };
730
+ }
731
+ const allItems = [];
732
+ for (const sourceId of sourceIds) {
733
+ const item = await adapter.findOne({
734
+ model: "contentItem",
735
+ where: [
736
+ { field: "id", value: sourceId, operator: "eq" },
737
+ {
738
+ field: "contentTypeId",
739
+ value: contentType.id,
740
+ operator: "eq"
741
+ }
742
+ ],
743
+ join: { contentType: true }
744
+ });
745
+ if (item) {
746
+ allItems.push(item);
747
+ }
748
+ }
749
+ allItems.sort(
750
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
751
+ );
752
+ const total = allItems.length;
753
+ const paginatedItems = allItems.slice(offset, offset + limit);
754
+ return {
755
+ items: paginatedItems.map(serializeContentItemWithType),
756
+ total,
757
+ limit,
758
+ offset
759
+ };
772
760
  }
773
- const allContentTypes = await adapter.findMany({
774
- model: "contentType"
775
- });
776
- const inverseRelations = [];
777
- for (const contentType of allContentTypes) {
778
- const relationFields = extractRelationFields(contentType);
779
- for (const [fieldName, relationConfig] of Object.entries(
780
- relationFields
781
- )) {
782
- if (relationConfig.type === "belongsTo" && relationConfig.targetType === slug) {
783
- let count = 0;
784
- if (itemId) {
785
- const relations = await adapter.findMany({
786
- model: "contentRelation",
787
- where: [
788
- {
789
- field: "targetId",
790
- value: itemId,
791
- operator: "eq"
792
- },
793
- {
794
- field: "fieldName",
795
- value: fieldName,
796
- operator: "eq"
797
- }
798
- ]
799
- });
800
- const itemIds = relations.map((r) => r.sourceId);
801
- for (const sourceId of itemIds) {
802
- const item = await adapter.findOne({
803
- model: "contentItem",
761
+ );
762
+ const getInverseRelations = createEndpoint(
763
+ "/content-types/:slug/inverse-relations",
764
+ {
765
+ method: "GET",
766
+ params: z.object({ slug: z.string() }),
767
+ query: z.object({
768
+ itemId: z.string().optional()
769
+ })
770
+ },
771
+ async (ctx) => {
772
+ const { slug } = ctx.params;
773
+ const { itemId } = ctx.query;
774
+ await ensureSynced(adapter);
775
+ const targetContentType = await getContentType(slug);
776
+ if (!targetContentType) {
777
+ throw ctx.error(404, { message: "Content type not found" });
778
+ }
779
+ const allContentTypes = await adapter.findMany({
780
+ model: "contentType"
781
+ });
782
+ const inverseRelations = [];
783
+ for (const contentType of allContentTypes) {
784
+ const relationFields = extractRelationFields(contentType);
785
+ for (const [fieldName, relationConfig] of Object.entries(
786
+ relationFields
787
+ )) {
788
+ if (relationConfig.type === "belongsTo" && relationConfig.targetType === slug) {
789
+ let count = 0;
790
+ if (itemId) {
791
+ const relations = await adapter.findMany({
792
+ model: "contentRelation",
804
793
  where: [
805
794
  {
806
- field: "id",
807
- value: sourceId,
795
+ field: "targetId",
796
+ value: itemId,
808
797
  operator: "eq"
809
798
  },
810
799
  {
811
- field: "contentTypeId",
812
- value: contentType.id,
800
+ field: "fieldName",
801
+ value: fieldName,
813
802
  operator: "eq"
814
803
  }
815
804
  ]
816
805
  });
817
- if (item) count++;
806
+ const itemIds = relations.map((r) => r.sourceId);
807
+ for (const sourceId of itemIds) {
808
+ const item = await adapter.findOne({
809
+ model: "contentItem",
810
+ where: [
811
+ {
812
+ field: "id",
813
+ value: sourceId,
814
+ operator: "eq"
815
+ },
816
+ {
817
+ field: "contentTypeId",
818
+ value: contentType.id,
819
+ operator: "eq"
820
+ }
821
+ ]
822
+ });
823
+ if (item) count++;
824
+ }
818
825
  }
826
+ inverseRelations.push({
827
+ sourceType: contentType.slug,
828
+ sourceTypeName: contentType.name,
829
+ fieldName,
830
+ count
831
+ });
819
832
  }
820
- inverseRelations.push({
821
- sourceType: contentType.slug,
822
- sourceTypeName: contentType.name,
823
- fieldName,
824
- count
825
- });
826
833
  }
827
834
  }
835
+ return { inverseRelations };
828
836
  }
829
- return { inverseRelations };
830
- }
831
- );
832
- const listInverseRelationItems = createEndpoint(
833
- "/content-types/:slug/inverse-relations/:sourceType",
834
- {
835
- method: "GET",
836
- params: z.object({
837
- slug: z.string(),
838
- sourceType: z.string()
839
- }),
840
- query: z.object({
841
- itemId: z.string(),
842
- fieldName: z.string(),
843
- limit: z.coerce.number().min(1).max(100).optional().default(20),
844
- offset: z.coerce.number().min(0).optional().default(0)
845
- })
846
- },
847
- async (ctx) => {
848
- const { slug, sourceType } = ctx.params;
849
- const { itemId, fieldName, limit, offset } = ctx.query;
850
- await ensureSynced();
851
- const targetContentType = await getContentType(slug);
852
- if (!targetContentType) {
853
- throw ctx.error(404, { message: "Target content type not found" });
854
- }
855
- const sourceContentType = await getContentType(sourceType);
856
- if (!sourceContentType) {
857
- throw ctx.error(404, { message: "Source content type not found" });
858
- }
859
- const relations = await adapter.findMany({
860
- model: "contentRelation",
861
- where: [
862
- { field: "targetId", value: itemId, operator: "eq" },
863
- { field: "fieldName", value: fieldName, operator: "eq" }
864
- ]
865
- });
866
- const sourceIds = [...new Set(relations.map((r) => r.sourceId))];
867
- const allItems = [];
868
- for (const sourceId of sourceIds) {
869
- const item = await adapter.findOne({
870
- model: "contentItem",
837
+ );
838
+ const listInverseRelationItems = createEndpoint(
839
+ "/content-types/:slug/inverse-relations/:sourceType",
840
+ {
841
+ method: "GET",
842
+ params: z.object({
843
+ slug: z.string(),
844
+ sourceType: z.string()
845
+ }),
846
+ query: z.object({
847
+ itemId: z.string(),
848
+ fieldName: z.string(),
849
+ limit: z.coerce.number().min(1).max(100).optional().default(20),
850
+ offset: z.coerce.number().min(0).optional().default(0)
851
+ })
852
+ },
853
+ async (ctx) => {
854
+ const { slug, sourceType } = ctx.params;
855
+ const { itemId, fieldName, limit, offset } = ctx.query;
856
+ await ensureSynced(adapter);
857
+ const targetContentType = await getContentType(slug);
858
+ if (!targetContentType) {
859
+ throw ctx.error(404, { message: "Target content type not found" });
860
+ }
861
+ const sourceContentType = await getContentType(sourceType);
862
+ if (!sourceContentType) {
863
+ throw ctx.error(404, { message: "Source content type not found" });
864
+ }
865
+ const relations = await adapter.findMany({
866
+ model: "contentRelation",
871
867
  where: [
872
- { field: "id", value: sourceId, operator: "eq" },
873
- {
874
- field: "contentTypeId",
875
- value: sourceContentType.id,
876
- operator: "eq"
877
- }
878
- ],
879
- join: { contentType: true }
868
+ { field: "targetId", value: itemId, operator: "eq" },
869
+ { field: "fieldName", value: fieldName, operator: "eq" }
870
+ ]
880
871
  });
881
- if (item) {
882
- allItems.push(item);
872
+ const sourceIds = [...new Set(relations.map((r) => r.sourceId))];
873
+ const allItems = [];
874
+ for (const sourceId of sourceIds) {
875
+ const item = await adapter.findOne({
876
+ model: "contentItem",
877
+ where: [
878
+ { field: "id", value: sourceId, operator: "eq" },
879
+ {
880
+ field: "contentTypeId",
881
+ value: sourceContentType.id,
882
+ operator: "eq"
883
+ }
884
+ ],
885
+ join: { contentType: true }
886
+ });
887
+ if (item) {
888
+ allItems.push(item);
889
+ }
883
890
  }
891
+ allItems.sort(
892
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
893
+ );
894
+ const total = allItems.length;
895
+ const paginatedItems = allItems.slice(offset, offset + limit);
896
+ return {
897
+ items: paginatedItems.map(serializeContentItemWithType),
898
+ total,
899
+ limit,
900
+ offset
901
+ };
884
902
  }
885
- allItems.sort(
886
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
887
- );
888
- const total = allItems.length;
889
- const paginatedItems = allItems.slice(offset, offset + limit);
890
- return {
891
- items: paginatedItems.map(serializeContentItemWithType),
892
- total,
893
- limit,
894
- offset
895
- };
896
- }
897
- );
898
- return {
899
- listContentTypes,
900
- getContentTypeBySlug,
901
- listContentItems,
902
- getContentItem,
903
- createContentItem,
904
- updateContentItem,
905
- deleteContentItem,
906
- getContentItemPopulated,
907
- listContentByRelation,
908
- getInverseRelations,
909
- listInverseRelationItems
910
- };
911
- }
912
- });
903
+ );
904
+ return {
905
+ listContentTypes,
906
+ getContentTypeBySlug,
907
+ listContentItems,
908
+ getContentItem,
909
+ createContentItem,
910
+ updateContentItem,
911
+ deleteContentItem,
912
+ getContentItemPopulated,
913
+ listContentByRelation,
914
+ getInverseRelations,
915
+ listInverseRelationItems
916
+ };
917
+ }
918
+ });
919
+ };
913
920
 
914
921
  export { cmsBackendPlugin };