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