@btst/stack 1.8.0 → 1.9.1
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.
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +445 -16
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +445 -16
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +28 -11
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +26 -9
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.cjs +224 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.mjs +222 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.cjs +243 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.mjs +241 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +56 -2
- package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +56 -2
- package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.cjs +190 -0
- package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.mjs +187 -1
- package/dist/packages/better-stack/src/plugins/cms/db.cjs +38 -0
- package/dist/packages/better-stack/src/plugins/cms/db.mjs +38 -0
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +2 -2
- package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +1 -1
- package/dist/packages/ui/src/components/auto-form/fields/array.cjs +2 -2
- package/dist/packages/ui/src/components/auto-form/fields/array.mjs +1 -1
- package/dist/packages/ui/src/components/auto-form/fields/date.cjs +2 -2
- package/dist/packages/ui/src/components/auto-form/fields/date.mjs +1 -1
- package/dist/packages/ui/src/components/auto-form/fields/enum.cjs +2 -2
- package/dist/packages/ui/src/components/auto-form/fields/enum.mjs +1 -1
- package/dist/packages/ui/src/components/auto-form/fields/object.cjs +88 -8
- package/dist/packages/ui/src/components/auto-form/fields/object.mjs +82 -2
- package/dist/packages/ui/src/components/auto-form/fields/radio-group.cjs +2 -2
- package/dist/packages/ui/src/components/auto-form/fields/radio-group.mjs +1 -1
- package/dist/packages/ui/src/components/auto-form/index.cjs +5 -5
- package/dist/packages/ui/src/components/auto-form/index.mjs +1 -1
- package/dist/packages/ui/src/components/button.cjs +4 -2
- package/dist/packages/ui/src/components/button.mjs +4 -2
- package/dist/packages/ui/src/components/dialog.cjs +7 -1
- package/dist/packages/ui/src/components/dialog.mjs +7 -2
- package/dist/packages/ui/src/components/form-builder/edit-field-dialog.cjs +2 -2
- package/dist/packages/ui/src/components/form-builder/edit-field-dialog.mjs +1 -1
- package/dist/packages/ui/src/components/form-builder/form-preview.cjs +5 -5
- package/dist/packages/ui/src/components/form-builder/form-preview.mjs +1 -1
- package/dist/packages/ui/src/components/select.cjs +9 -2
- package/dist/packages/ui/src/components/select.mjs +9 -2
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/api/index.d.cts +66 -2
- package/dist/plugins/cms/api/index.d.mts +66 -2
- package/dist/plugins/cms/api/index.d.ts +66 -2
- package/dist/plugins/cms/client/hooks/index.cjs +4 -0
- package/dist/plugins/cms/client/hooks/index.d.cts +82 -3
- package/dist/plugins/cms/client/hooks/index.d.mts +82 -3
- package/dist/plugins/cms/client/hooks/index.d.ts +82 -3
- package/dist/plugins/cms/client/hooks/index.mjs +1 -1
- package/dist/plugins/cms/client/index.d.cts +2 -2
- package/dist/plugins/cms/client/index.d.mts +2 -2
- package/dist/plugins/cms/client/index.d.ts +2 -2
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +2 -2
- package/dist/plugins/form-builder/client/index.d.mts +2 -2
- package/dist/plugins/form-builder/client/index.d.ts +2 -2
- package/dist/shared/{stack.AX5nZ6A3.d.ts → stack.Co034Fpm.d.cts} +0 -21
- package/dist/shared/{stack.AX5nZ6A3.d.cts → stack.Co034Fpm.d.mts} +0 -21
- package/dist/shared/{stack.AX5nZ6A3.d.mts → stack.Co034Fpm.d.ts} +0 -21
- package/dist/shared/{stack.BIh2AXaW.d.cts → stack.DGjhPqmF.d.cts} +0 -9
- package/dist/shared/{stack.BIh2AXaW.d.mts → stack.DGjhPqmF.d.mts} +0 -9
- package/dist/shared/{stack.BIh2AXaW.d.ts → stack.DGjhPqmF.d.ts} +0 -9
- package/dist/shared/{stack.L-UFwz2G.d.mts → stack.oGOteE6g.d.cts} +27 -5
- package/dist/shared/{stack.L-UFwz2G.d.cts → stack.oGOteE6g.d.mts} +27 -5
- package/dist/shared/{stack.L-UFwz2G.d.ts → stack.oGOteE6g.d.ts} +27 -5
- package/package.json +1 -1
- package/src/plugins/cms/api/plugin.ts +667 -21
- package/src/plugins/cms/client/components/forms/content-form.tsx +62 -20
- package/src/plugins/cms/client/components/forms/relation-field.tsx +299 -0
- package/src/plugins/cms/client/components/inverse-relations-panel.tsx +329 -0
- package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +127 -1
- package/src/plugins/cms/client/hooks/cms-hooks.tsx +344 -0
- package/src/plugins/cms/db.ts +38 -0
- package/src/plugins/cms/types.ts +99 -10
- package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +1 -1
- package/dist/packages/ui/src/components/auto-form/{utils.cjs → helpers.cjs} +0 -0
- package/dist/packages/ui/src/components/auto-form/{utils.mjs → helpers.mjs} +0 -0
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -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(
|
|
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
|
-
|
|
478
|
+
const processedData = validation.data;
|
|
306
479
|
if (config.hooks?.onBeforeCreate) {
|
|
307
480
|
const result = await config.hooks.onBeforeCreate(
|
|
308
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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 (
|
|
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
|
});
|