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