@btst/stack 1.7.0 → 1.9.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 (110) hide show
  1. package/dist/api/index.d.cts +2 -2
  2. package/dist/api/index.d.mts +2 -2
  3. package/dist/api/index.d.ts +2 -2
  4. package/dist/client/index.cjs +6 -2
  5. package/dist/client/index.d.cts +2 -1
  6. package/dist/client/index.d.mts +2 -1
  7. package/dist/client/index.d.ts +2 -1
  8. package/dist/client/index.mjs +6 -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/better-stack/src/plugins/cms/api/plugin.cjs +445 -16
  13. package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +445 -16
  14. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -7
  15. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +25 -8
  16. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.cjs +224 -0
  17. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.mjs +222 -0
  18. package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.cjs +243 -0
  19. package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.mjs +241 -0
  20. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +56 -2
  21. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +56 -2
  22. package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.cjs +190 -0
  23. package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.mjs +187 -1
  24. package/dist/packages/better-stack/src/plugins/cms/db.cjs +38 -0
  25. package/dist/packages/better-stack/src/plugins/cms/db.mjs +38 -0
  26. package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.cjs +43 -0
  27. package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.mjs +41 -0
  28. package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.cjs +794 -0
  29. package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.mjs +788 -0
  30. package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.cjs +111 -0
  31. package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.mjs +106 -0
  32. package/dist/packages/better-stack/src/plugins/route-docs/generator.cjs +244 -0
  33. package/dist/packages/better-stack/src/plugins/route-docs/generator.mjs +227 -0
  34. package/dist/packages/ui/src/components/auto-form/fields/object.cjs +81 -1
  35. package/dist/packages/ui/src/components/auto-form/fields/object.mjs +81 -1
  36. package/dist/packages/ui/src/components/dialog.cjs +6 -0
  37. package/dist/packages/ui/src/components/dialog.mjs +6 -1
  38. package/dist/packages/ui/src/components/sheet.cjs +25 -0
  39. package/dist/packages/ui/src/components/sheet.mjs +24 -1
  40. package/dist/plugins/api/index.d.cts +2 -2
  41. package/dist/plugins/api/index.d.mts +2 -2
  42. package/dist/plugins/api/index.d.ts +2 -2
  43. package/dist/plugins/blog/api/index.d.cts +1 -1
  44. package/dist/plugins/blog/api/index.d.mts +1 -1
  45. package/dist/plugins/blog/api/index.d.ts +1 -1
  46. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  47. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  48. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  49. package/dist/plugins/blog/client/index.d.cts +1 -1
  50. package/dist/plugins/blog/client/index.d.mts +1 -1
  51. package/dist/plugins/blog/client/index.d.ts +1 -1
  52. package/dist/plugins/blog/query-keys.d.cts +2 -2
  53. package/dist/plugins/blog/query-keys.d.mts +2 -2
  54. package/dist/plugins/blog/query-keys.d.ts +2 -2
  55. package/dist/plugins/client/index.d.cts +2 -2
  56. package/dist/plugins/client/index.d.mts +2 -2
  57. package/dist/plugins/client/index.d.ts +2 -2
  58. package/dist/plugins/cms/api/index.d.cts +67 -3
  59. package/dist/plugins/cms/api/index.d.mts +67 -3
  60. package/dist/plugins/cms/api/index.d.ts +67 -3
  61. package/dist/plugins/cms/client/hooks/index.cjs +4 -0
  62. package/dist/plugins/cms/client/hooks/index.d.cts +82 -3
  63. package/dist/plugins/cms/client/hooks/index.d.mts +82 -3
  64. package/dist/plugins/cms/client/hooks/index.d.ts +82 -3
  65. package/dist/plugins/cms/client/hooks/index.mjs +1 -1
  66. package/dist/plugins/cms/query-keys.d.cts +1 -1
  67. package/dist/plugins/cms/query-keys.d.mts +1 -1
  68. package/dist/plugins/cms/query-keys.d.ts +1 -1
  69. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  70. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  71. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  72. package/dist/plugins/open-api/api/index.d.cts +1 -1
  73. package/dist/plugins/open-api/api/index.d.mts +1 -1
  74. package/dist/plugins/open-api/api/index.d.ts +1 -1
  75. package/dist/plugins/route-docs/client/index.cjs +10 -0
  76. package/dist/plugins/route-docs/client/index.d.cts +126 -0
  77. package/dist/plugins/route-docs/client/index.d.mts +126 -0
  78. package/dist/plugins/route-docs/client/index.d.ts +126 -0
  79. package/dist/plugins/route-docs/client/index.mjs +1 -0
  80. package/dist/plugins/route-docs/client.css +3 -0
  81. package/dist/plugins/route-docs/style.css +19 -0
  82. package/dist/shared/{stack.L-UFwz2G.d.mts → stack.oGOteE6g.d.cts} +27 -5
  83. package/dist/shared/{stack.L-UFwz2G.d.ts → stack.oGOteE6g.d.mts} +27 -5
  84. package/dist/shared/{stack.L-UFwz2G.d.cts → stack.oGOteE6g.d.ts} +27 -5
  85. package/dist/shared/{stack.CSce37mX.d.cts → stack.u9iYV6vt.d.cts} +14 -2
  86. package/dist/shared/{stack.CSce37mX.d.mts → stack.u9iYV6vt.d.mts} +14 -2
  87. package/dist/shared/{stack.CSce37mX.d.ts → stack.u9iYV6vt.d.ts} +14 -2
  88. package/package.json +15 -1
  89. package/src/client/index.ts +11 -4
  90. package/src/plugins/cms/api/plugin.ts +667 -21
  91. package/src/plugins/cms/client/components/forms/content-form.tsx +60 -18
  92. package/src/plugins/cms/client/components/forms/relation-field.tsx +299 -0
  93. package/src/plugins/cms/client/components/inverse-relations-panel.tsx +329 -0
  94. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +127 -1
  95. package/src/plugins/cms/client/hooks/cms-hooks.tsx +344 -0
  96. package/src/plugins/cms/db.ts +38 -0
  97. package/src/plugins/cms/types.ts +99 -10
  98. package/src/plugins/route-docs/client/components/loading/docs-skeleton.tsx +82 -0
  99. package/src/plugins/route-docs/client/components/loading/index.tsx +1 -0
  100. package/src/plugins/route-docs/client/components/pages/docs-page.tsx +1240 -0
  101. package/src/plugins/route-docs/client/index.ts +7 -0
  102. package/src/plugins/route-docs/client/plugin.tsx +187 -0
  103. package/src/plugins/route-docs/client.css +3 -0
  104. package/src/plugins/route-docs/generator.ts +385 -0
  105. package/src/plugins/route-docs/index.ts +12 -0
  106. package/src/plugins/route-docs/style.css +19 -0
  107. package/src/types.ts +19 -1
  108. package/dist/shared/{stack.CcI4sYJP.d.mts → stack.DLhzx1-D.d.cts} +1 -1
  109. package/dist/shared/{stack.CcI4sYJP.d.ts → stack.DLhzx1-D.d.mts} +1 -1
  110. package/dist/shared/{stack.CcI4sYJP.d.cts → stack.DLhzx1-D.d.ts} +1 -1
@@ -1,5 +1,5 @@
1
- import { e as PrefixedPluginRoutes, f as BackendLibConfig, g as BackendLib } from '../shared/stack.CSce37mX.cjs';
2
- export { B as BackendPlugin, a as BetterStackContext } from '../shared/stack.CSce37mX.cjs';
1
+ import { g as PrefixedPluginRoutes, h as BackendLibConfig, i as BackendLib } from '../shared/stack.u9iYV6vt.cjs';
2
+ export { B as BackendPlugin, a as BetterStackContext } from '../shared/stack.u9iYV6vt.cjs';
3
3
  export { toNodeHandler } from 'better-call/node';
4
4
  import '@btst/yar';
5
5
  import '@btst/db';
@@ -1,5 +1,5 @@
1
- import { e as PrefixedPluginRoutes, f as BackendLibConfig, g as BackendLib } from '../shared/stack.CSce37mX.mjs';
2
- export { B as BackendPlugin, a as BetterStackContext } from '../shared/stack.CSce37mX.mjs';
1
+ import { g as PrefixedPluginRoutes, h as BackendLibConfig, i as BackendLib } from '../shared/stack.u9iYV6vt.mjs';
2
+ export { B as BackendPlugin, a as BetterStackContext } from '../shared/stack.u9iYV6vt.mjs';
3
3
  export { toNodeHandler } from 'better-call/node';
4
4
  import '@btst/yar';
5
5
  import '@btst/db';
@@ -1,5 +1,5 @@
1
- import { e as PrefixedPluginRoutes, f as BackendLibConfig, g as BackendLib } from '../shared/stack.CSce37mX.js';
2
- export { B as BackendPlugin, a as BetterStackContext } from '../shared/stack.CSce37mX.js';
1
+ import { g as PrefixedPluginRoutes, h as BackendLibConfig, i as BackendLib } from '../shared/stack.u9iYV6vt.js';
2
+ export { B as BackendPlugin, a as BetterStackContext } from '../shared/stack.u9iYV6vt.js';
3
3
  export { toNodeHandler } from 'better-call/node';
4
4
  import '@btst/yar';
5
5
  import '@btst/db';
@@ -6,10 +6,14 @@ const metaUtils = require('../packages/better-stack/src/client/meta-utils.cjs');
6
6
  const pathUtils = require('../packages/better-stack/src/client/path-utils.cjs');
7
7
 
8
8
  function createStackClient(config) {
9
- const { plugins } = config;
9
+ const { plugins, basePath } = config;
10
10
  const allRoutes = {};
11
+ const clientStackContext = {
12
+ plugins,
13
+ basePath
14
+ };
11
15
  for (const [pluginKey, plugin] of Object.entries(plugins)) {
12
- const pluginRoutes = plugin.routes();
16
+ const pluginRoutes = plugin.routes(clientStackContext);
13
17
  Object.assign(allRoutes, pluginRoutes);
14
18
  }
15
19
  const router = yar.createRouter(allRoutes);
@@ -1,4 +1,5 @@
1
- import { S as Sitemap, C as ClientPlugin, b as PluginRoutes, c as ClientLibConfig, d as ClientLib } from '../shared/stack.CSce37mX.cjs';
1
+ import { c as Sitemap, C as ClientPlugin, d as PluginRoutes, e as ClientLibConfig, f as ClientLib } from '../shared/stack.u9iYV6vt.cjs';
2
+ export { b as ClientStackContext } from '../shared/stack.u9iYV6vt.cjs';
2
3
  import '@btst/yar';
3
4
  import '@btst/db';
4
5
  import 'better-call';
@@ -1,4 +1,5 @@
1
- import { S as Sitemap, C as ClientPlugin, b as PluginRoutes, c as ClientLibConfig, d as ClientLib } from '../shared/stack.CSce37mX.mjs';
1
+ import { c as Sitemap, C as ClientPlugin, d as PluginRoutes, e as ClientLibConfig, f as ClientLib } from '../shared/stack.u9iYV6vt.mjs';
2
+ export { b as ClientStackContext } from '../shared/stack.u9iYV6vt.mjs';
2
3
  import '@btst/yar';
3
4
  import '@btst/db';
4
5
  import 'better-call';
@@ -1,4 +1,5 @@
1
- import { S as Sitemap, C as ClientPlugin, b as PluginRoutes, c as ClientLibConfig, d as ClientLib } from '../shared/stack.CSce37mX.js';
1
+ import { c as Sitemap, C as ClientPlugin, d as PluginRoutes, e as ClientLibConfig, f as ClientLib } from '../shared/stack.u9iYV6vt.js';
2
+ export { b as ClientStackContext } from '../shared/stack.u9iYV6vt.js';
2
3
  import '@btst/yar';
3
4
  import '@btst/db';
4
5
  import 'better-call';
@@ -4,10 +4,14 @@ export { metaElementsToObject } from '../packages/better-stack/src/client/meta-u
4
4
  export { normalizePath } from '../packages/better-stack/src/client/path-utils.mjs';
5
5
 
6
6
  function createStackClient(config) {
7
- const { plugins } = config;
7
+ const { plugins, basePath } = config;
8
8
  const allRoutes = {};
9
+ const clientStackContext = {
10
+ plugins,
11
+ basePath
12
+ };
9
13
  for (const [pluginKey, plugin] of Object.entries(plugins)) {
10
- const pluginRoutes = plugin.routes();
14
+ const pluginRoutes = plugin.routes(clientStackContext);
11
15
  Object.assign(allRoutes, pluginRoutes);
12
16
  }
13
17
  const router = createRouter(allRoutes);
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { betterStack } from './api/index.cjs';
2
2
  export { toNodeHandler } from 'better-call/node';
3
- export { g as BackendLib, f as BackendLibConfig, B as BackendPlugin, a as BetterStackContext } from './shared/stack.CSce37mX.cjs';
3
+ export { i as BackendLib, h as BackendLibConfig, B as BackendPlugin, a as BetterStackContext } from './shared/stack.u9iYV6vt.cjs';
4
4
  import '@btst/yar';
5
5
  import '@btst/db';
6
6
  import 'better-call';
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { betterStack } from './api/index.mjs';
2
2
  export { toNodeHandler } from 'better-call/node';
3
- export { g as BackendLib, f as BackendLibConfig, B as BackendPlugin, a as BetterStackContext } from './shared/stack.CSce37mX.mjs';
3
+ export { i as BackendLib, h as BackendLibConfig, B as BackendPlugin, a as BetterStackContext } from './shared/stack.u9iYV6vt.mjs';
4
4
  import '@btst/yar';
5
5
  import '@btst/db';
6
6
  import 'better-call';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { betterStack } from './api/index.js';
2
2
  export { toNodeHandler } from 'better-call/node';
3
- export { g as BackendLib, f as BackendLibConfig, B as BackendPlugin, a as BetterStackContext } from './shared/stack.CSce37mX.js';
3
+ export { i as BackendLib, h as BackendLibConfig, B as BackendPlugin, a as BetterStackContext } from './shared/stack.u9iYV6vt.js';
4
4
  import '@btst/yar';
5
5
  import '@btst/db';
6
6
  import 'better-call';
@@ -111,6 +111,173 @@ function getContentTypeZodSchema(contentType) {
111
111
  const jsonSchema = JSON.parse(contentType.jsonSchema);
112
112
  return schemaConverter.formSchemaToZod(jsonSchema);
113
113
  }
114
+ function extractRelationFields(contentType) {
115
+ const jsonSchema = JSON.parse(
116
+ contentType.jsonSchema
117
+ );
118
+ const properties = jsonSchema.properties || {};
119
+ const relationFields = {};
120
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
121
+ if (fieldSchema.fieldType === "relation" && fieldSchema.relation) {
122
+ relationFields[fieldName] = fieldSchema.relation;
123
+ }
124
+ }
125
+ return relationFields;
126
+ }
127
+ function isNewRelationValue(value) {
128
+ return typeof value === "object" && value !== null && "_new" in value && value._new === true && "data" in value;
129
+ }
130
+ function isExistingRelationValue(value) {
131
+ return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string";
132
+ }
133
+ async function processRelationsInData(adapter, contentType, data, getContentTypeFn) {
134
+ const relationFields = extractRelationFields(contentType);
135
+ const processedData = { ...data };
136
+ const relationIds = {};
137
+ for (const [fieldName, relationConfig] of Object.entries(relationFields)) {
138
+ if (!(fieldName in data)) {
139
+ continue;
140
+ }
141
+ const fieldValue = data[fieldName];
142
+ if (!fieldValue) {
143
+ relationIds[fieldName] = [];
144
+ continue;
145
+ }
146
+ const targetContentType = await getContentTypeFn(relationConfig.targetType);
147
+ if (!targetContentType) {
148
+ throw new Error(
149
+ `Target content type "${relationConfig.targetType}" not found for relation field "${fieldName}"`
150
+ );
151
+ }
152
+ const ids = [];
153
+ if (relationConfig.type === "belongsTo") {
154
+ const value = fieldValue;
155
+ if (isNewRelationValue(value)) {
156
+ const newItem = await createRelatedItem(
157
+ adapter,
158
+ targetContentType,
159
+ value.data
160
+ );
161
+ ids.push(newItem.id);
162
+ processedData[fieldName] = { id: newItem.id };
163
+ } else if (isExistingRelationValue(value)) {
164
+ ids.push(value.id);
165
+ }
166
+ } else {
167
+ const values = Array.isArray(fieldValue) ? fieldValue : [];
168
+ const processedValues = [];
169
+ for (const value of values) {
170
+ if (isNewRelationValue(value)) {
171
+ const newItem = await createRelatedItem(
172
+ adapter,
173
+ targetContentType,
174
+ value.data
175
+ );
176
+ ids.push(newItem.id);
177
+ processedValues.push({ id: newItem.id });
178
+ } else if (isExistingRelationValue(value)) {
179
+ ids.push(value.id);
180
+ processedValues.push({ id: value.id });
181
+ }
182
+ }
183
+ processedData[fieldName] = processedValues;
184
+ }
185
+ relationIds[fieldName] = ids;
186
+ }
187
+ return { processedData, relationIds };
188
+ }
189
+ async function createRelatedItem(adapter, targetContentType, data) {
190
+ const slug = utils.slugify(
191
+ data.slug || data.name || data.title || `item-${Date.now()}`
192
+ );
193
+ const zodSchema = getContentTypeZodSchema(targetContentType);
194
+ const validation = zodSchema.safeParse(data);
195
+ if (!validation.success) {
196
+ throw new Error(
197
+ `Validation failed for new ${targetContentType.slug}: ${JSON.stringify(validation.error.issues)}`
198
+ );
199
+ }
200
+ const existing = await adapter.findOne({
201
+ model: "contentItem",
202
+ where: [
203
+ {
204
+ field: "contentTypeId",
205
+ value: targetContentType.id,
206
+ operator: "eq"
207
+ },
208
+ { field: "slug", value: slug, operator: "eq" }
209
+ ]
210
+ });
211
+ if (existing) {
212
+ return existing;
213
+ }
214
+ const item = await adapter.create({
215
+ model: "contentItem",
216
+ data: {
217
+ contentTypeId: targetContentType.id,
218
+ slug,
219
+ data: JSON.stringify(validation.data),
220
+ createdAt: /* @__PURE__ */ new Date(),
221
+ updatedAt: /* @__PURE__ */ new Date()
222
+ }
223
+ });
224
+ return item;
225
+ }
226
+ async function syncRelations(adapter, sourceId, relationIds) {
227
+ for (const [fieldName, targetIds] of Object.entries(relationIds)) {
228
+ await adapter.delete({
229
+ model: "contentRelation",
230
+ where: [
231
+ { field: "sourceId", value: sourceId, operator: "eq" },
232
+ { field: "fieldName", value: fieldName, operator: "eq" }
233
+ ]
234
+ });
235
+ for (const targetId of targetIds) {
236
+ await adapter.create({
237
+ model: "contentRelation",
238
+ data: {
239
+ sourceId,
240
+ targetId,
241
+ fieldName,
242
+ createdAt: /* @__PURE__ */ new Date()
243
+ }
244
+ });
245
+ }
246
+ }
247
+ }
248
+ async function populateRelations(adapter, item) {
249
+ const relations = {};
250
+ const contentRelations = await adapter.findMany({
251
+ model: "contentRelation",
252
+ where: [{ field: "sourceId", value: item.id, operator: "eq" }]
253
+ });
254
+ const relationsByField = {};
255
+ for (const rel of contentRelations) {
256
+ if (!relationsByField[rel.fieldName]) {
257
+ relationsByField[rel.fieldName] = [];
258
+ }
259
+ relationsByField[rel.fieldName].push(rel.targetId);
260
+ }
261
+ for (const [fieldName, targetIds] of Object.entries(relationsByField)) {
262
+ if (targetIds.length === 0) {
263
+ relations[fieldName] = [];
264
+ continue;
265
+ }
266
+ const relatedItems = [];
267
+ for (const targetId of targetIds) {
268
+ const relatedItem = await adapter.findOne({
269
+ model: "contentItem",
270
+ where: [{ field: "id", value: targetId, operator: "eq" }],
271
+ join: { contentType: true }
272
+ });
273
+ if (relatedItem) {
274
+ relatedItems.push(serializeContentItemWithType(relatedItem));
275
+ }
276
+ }
277
+ relations[fieldName] = relatedItems;
278
+ }
279
+ return relations;
280
+ }
114
281
  const cmsBackendPlugin = (config) => api.defineBackendPlugin({
115
282
  name: "cms",
116
283
  dbPlugin: db.cmsSchema,
@@ -278,8 +445,14 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
278
445
  if (!contentType) {
279
446
  throw ctx.error(404, { message: "Content type not found" });
280
447
  }
448
+ const { processedData: dataWithResolvedRelations, relationIds } = await processRelationsInData(
449
+ adapter,
450
+ contentType,
451
+ data,
452
+ getContentType
453
+ );
281
454
  const zodSchema = getContentTypeZodSchema(contentType);
282
- const validation = zodSchema.safeParse(data);
455
+ const validation = zodSchema.safeParse(dataWithResolvedRelations);
283
456
  if (!validation.success) {
284
457
  throw ctx.error(400, {
285
458
  message: "Validation failed",
@@ -302,36 +475,34 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
302
475
  message: "Content item with this slug already exists"
303
476
  });
304
477
  }
305
- let finalData = validation.data;
478
+ const processedData = validation.data;
306
479
  if (config.hooks?.onBeforeCreate) {
307
480
  const result = await config.hooks.onBeforeCreate(
308
- validation.data,
481
+ processedData,
309
482
  context
310
483
  );
311
484
  if (result === false) {
312
485
  throw ctx.error(403, { message: "Create operation denied" });
313
486
  }
314
- if (result && typeof result === "object") {
315
- finalData = result;
316
- }
317
487
  }
318
488
  const item = await adapter.create({
319
489
  model: "contentItem",
320
490
  data: {
321
491
  contentTypeId: contentType.id,
322
492
  slug,
323
- data: JSON.stringify(finalData),
493
+ data: JSON.stringify(processedData),
324
494
  createdAt: /* @__PURE__ */ new Date(),
325
495
  updatedAt: /* @__PURE__ */ new Date()
326
496
  }
327
497
  });
498
+ await syncRelations(adapter, item.id, relationIds);
328
499
  const serialized = serializeContentItem(item);
329
500
  if (config.hooks?.onAfterCreate) {
330
501
  await config.hooks.onAfterCreate(serialized, context);
331
502
  }
332
503
  return {
333
504
  ...serialized,
334
- parsedData: finalData
505
+ parsedData: processedData
335
506
  };
336
507
  }
337
508
  );
@@ -385,10 +556,27 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
385
556
  });
386
557
  }
387
558
  }
388
- let validatedData = data;
559
+ let dataWithResolvedRelations;
560
+ let relationIds;
389
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
577
+ };
390
578
  const zodSchema = getContentTypeZodSchema(contentType);
391
- const validation = zodSchema.safeParse(data);
579
+ const validation = zodSchema.safeParse(mergedData);
392
580
  if (!validation.success) {
393
581
  throw ctx.error(400, {
394
582
  message: "Validation failed",
@@ -397,7 +585,7 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
397
585
  }
398
586
  validatedData = validation.data;
399
587
  }
400
- let finalData = validatedData;
588
+ const processedData = validatedData;
401
589
  if (config.hooks?.onBeforeUpdate && validatedData) {
402
590
  const result = await config.hooks.onBeforeUpdate(
403
591
  id,
@@ -407,15 +595,15 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
407
595
  if (result === false) {
408
596
  throw ctx.error(403, { message: "Update operation denied" });
409
597
  }
410
- if (result && typeof result === "object") {
411
- finalData = result;
412
- }
598
+ }
599
+ if (relationIds) {
600
+ await syncRelations(adapter, id, relationIds);
413
601
  }
414
602
  const updateData = {
415
603
  updatedAt: /* @__PURE__ */ new Date()
416
604
  };
417
605
  if (slug) updateData.slug = slug;
418
- if (finalData) updateData.data = JSON.stringify(finalData);
606
+ if (processedData) updateData.data = JSON.stringify(processedData);
419
607
  await adapter.update({
420
608
  model: "contentItem",
421
609
  where: [{ field: "id", value: id, operator: "eq" }],
@@ -472,6 +660,243 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
472
660
  return { success: true };
473
661
  }
474
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
725
+ };
726
+ }
727
+ const allItems = [];
728
+ for (const sourceId of sourceIds) {
729
+ const item = await adapter.findOne({
730
+ model: "contentItem",
731
+ where: [
732
+ { field: "id", value: sourceId, operator: "eq" },
733
+ {
734
+ field: "contentTypeId",
735
+ value: contentType.id,
736
+ operator: "eq"
737
+ }
738
+ ],
739
+ join: { contentType: true }
740
+ });
741
+ if (item) {
742
+ allItems.push(item);
743
+ }
744
+ }
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" });
774
+ }
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",
806
+ where: [
807
+ {
808
+ field: "id",
809
+ value: sourceId,
810
+ operator: "eq"
811
+ },
812
+ {
813
+ field: "contentTypeId",
814
+ value: contentType.id,
815
+ operator: "eq"
816
+ }
817
+ ]
818
+ });
819
+ if (item) count++;
820
+ }
821
+ }
822
+ inverseRelations.push({
823
+ sourceType: contentType.slug,
824
+ sourceTypeName: contentType.name,
825
+ fieldName,
826
+ count
827
+ });
828
+ }
829
+ }
830
+ }
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",
873
+ 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 }
882
+ });
883
+ if (item) {
884
+ allItems.push(item);
885
+ }
886
+ }
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
+ );
475
900
  return {
476
901
  listContentTypes,
477
902
  getContentTypeBySlug,
@@ -479,7 +904,11 @@ const cmsBackendPlugin = (config) => api.defineBackendPlugin({
479
904
  getContentItem,
480
905
  createContentItem,
481
906
  updateContentItem,
482
- deleteContentItem
907
+ deleteContentItem,
908
+ getContentItemPopulated,
909
+ listContentByRelation,
910
+ getInverseRelations,
911
+ listInverseRelationItems
483
912
  };
484
913
  }
485
914
  });