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