@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
|
@@ -14,8 +14,6 @@ import type {
|
|
|
14
14
|
ContentRelation,
|
|
15
15
|
CMSBackendConfig,
|
|
16
16
|
CMSHookContext,
|
|
17
|
-
SerializedContentType,
|
|
18
|
-
SerializedContentItem,
|
|
19
17
|
SerializedContentItemWithType,
|
|
20
18
|
RelationConfig,
|
|
21
19
|
RelationValue,
|
|
@@ -23,97 +21,14 @@ import type {
|
|
|
23
21
|
} from "../types";
|
|
24
22
|
import { listContentQuerySchema } from "../schemas";
|
|
25
23
|
import { slugify } from "../utils";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
): string {
|
|
35
|
-
if (!fieldConfigStr) {
|
|
36
|
-
return jsonSchemaStr;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const jsonSchema = JSON.parse(jsonSchemaStr);
|
|
41
|
-
const fieldConfig = JSON.parse(fieldConfigStr);
|
|
42
|
-
|
|
43
|
-
if (!jsonSchema.properties || typeof fieldConfig !== "object") {
|
|
44
|
-
return jsonSchemaStr;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Merge fieldType from fieldConfig into each property
|
|
48
|
-
for (const [key, config] of Object.entries(fieldConfig)) {
|
|
49
|
-
if (
|
|
50
|
-
jsonSchema.properties[key] &&
|
|
51
|
-
typeof config === "object" &&
|
|
52
|
-
config !== null &&
|
|
53
|
-
"fieldType" in config
|
|
54
|
-
) {
|
|
55
|
-
jsonSchema.properties[key].fieldType = (
|
|
56
|
-
config as { fieldType: string }
|
|
57
|
-
).fieldType;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return JSON.stringify(jsonSchema);
|
|
62
|
-
} catch {
|
|
63
|
-
// If parsing fails, return original
|
|
64
|
-
return jsonSchemaStr;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Serialize a ContentType for API response (convert dates to strings)
|
|
70
|
-
* Also applies lazy migration for legacy schemas (version 1 → 2)
|
|
71
|
-
*/
|
|
72
|
-
function serializeContentType(ct: ContentType): SerializedContentType {
|
|
73
|
-
// Check if this is a legacy schema that needs migration
|
|
74
|
-
const needsMigration = !ct.autoFormVersion || ct.autoFormVersion < 2;
|
|
75
|
-
|
|
76
|
-
// Apply lazy migration: merge fieldConfig into jsonSchema on read
|
|
77
|
-
const migratedJsonSchema = needsMigration
|
|
78
|
-
? migrateToUnifiedSchema(ct.jsonSchema, ct.fieldConfig)
|
|
79
|
-
: ct.jsonSchema;
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
id: ct.id,
|
|
83
|
-
name: ct.name,
|
|
84
|
-
slug: ct.slug,
|
|
85
|
-
description: ct.description,
|
|
86
|
-
jsonSchema: migratedJsonSchema,
|
|
87
|
-
createdAt: ct.createdAt.toISOString(),
|
|
88
|
-
updatedAt: ct.updatedAt.toISOString(),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Serialize a ContentItem for API response (convert dates to strings)
|
|
94
|
-
*/
|
|
95
|
-
function serializeContentItem(item: ContentItem): SerializedContentItem {
|
|
96
|
-
return {
|
|
97
|
-
...item,
|
|
98
|
-
createdAt: item.createdAt.toISOString(),
|
|
99
|
-
updatedAt: item.updatedAt.toISOString(),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Serialize a ContentItem with parsed data and joined ContentType
|
|
105
|
-
*/
|
|
106
|
-
function serializeContentItemWithType(
|
|
107
|
-
item: ContentItemWithType,
|
|
108
|
-
): SerializedContentItemWithType {
|
|
109
|
-
return {
|
|
110
|
-
...serializeContentItem(item),
|
|
111
|
-
parsedData: JSON.parse(item.data),
|
|
112
|
-
contentType: item.contentType
|
|
113
|
-
? serializeContentType(item.contentType)
|
|
114
|
-
: undefined,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
24
|
+
import {
|
|
25
|
+
getAllContentTypes,
|
|
26
|
+
getAllContentItems,
|
|
27
|
+
getContentItemBySlug,
|
|
28
|
+
serializeContentType,
|
|
29
|
+
serializeContentItem,
|
|
30
|
+
serializeContentItemWithType,
|
|
31
|
+
} from "./getters";
|
|
117
32
|
|
|
118
33
|
/**
|
|
119
34
|
* Sync content types from config to database
|
|
@@ -511,34 +426,52 @@ async function populateRelations(
|
|
|
511
426
|
*
|
|
512
427
|
* @param config - Configuration with content types and optional hooks
|
|
513
428
|
*/
|
|
514
|
-
export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
515
|
-
|
|
429
|
+
export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
430
|
+
// Shared sync state — used by both the api factory and routes handlers so
|
|
431
|
+
// that calling a getter before any HTTP request has been made still
|
|
432
|
+
// triggers the one-time content-type sync.
|
|
433
|
+
let syncPromise: Promise<void> | null = null;
|
|
434
|
+
|
|
435
|
+
const ensureSynced = (adapter: Adapter) => {
|
|
436
|
+
if (!syncPromise) {
|
|
437
|
+
syncPromise = syncContentTypes(adapter, config).catch((err) => {
|
|
438
|
+
// Allow retry on next call if sync fails
|
|
439
|
+
syncPromise = null;
|
|
440
|
+
throw err;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return syncPromise;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
return defineBackendPlugin({
|
|
516
447
|
name: "cms",
|
|
517
448
|
|
|
518
449
|
dbPlugin: dbSchema,
|
|
519
450
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
451
|
+
api: (adapter) => ({
|
|
452
|
+
getAllContentTypes: async () => {
|
|
453
|
+
await ensureSynced(adapter);
|
|
454
|
+
return getAllContentTypes(adapter);
|
|
455
|
+
},
|
|
456
|
+
getAllContentItems: async (
|
|
457
|
+
contentTypeSlug: string,
|
|
458
|
+
params?: Parameters<typeof getAllContentItems>[2],
|
|
459
|
+
) => {
|
|
460
|
+
await ensureSynced(adapter);
|
|
461
|
+
return getAllContentItems(adapter, contentTypeSlug, params);
|
|
462
|
+
},
|
|
463
|
+
getContentItemBySlug: async (contentTypeSlug: string, slug: string) => {
|
|
464
|
+
await ensureSynced(adapter);
|
|
465
|
+
return getContentItemBySlug(adapter, contentTypeSlug, slug);
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
536
468
|
|
|
469
|
+
routes: (adapter: Adapter) => {
|
|
537
470
|
// Helper to get content type by slug
|
|
538
471
|
const getContentType = async (
|
|
539
472
|
slug: string,
|
|
540
473
|
): Promise<ContentType | null> => {
|
|
541
|
-
await ensureSynced();
|
|
474
|
+
await ensureSynced(adapter);
|
|
542
475
|
return adapter.findOne<ContentType>({
|
|
543
476
|
model: "contentType",
|
|
544
477
|
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
@@ -560,7 +493,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
560
493
|
"/content-types",
|
|
561
494
|
{ method: "GET" },
|
|
562
495
|
async (ctx) => {
|
|
563
|
-
await ensureSynced();
|
|
496
|
+
await ensureSynced(adapter);
|
|
564
497
|
|
|
565
498
|
const contentTypes = await adapter.findMany<ContentType>({
|
|
566
499
|
model: "contentType",
|
|
@@ -627,45 +560,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
627
560
|
throw ctx.error(404, { message: "Content type not found" });
|
|
628
561
|
}
|
|
629
562
|
|
|
630
|
-
|
|
631
|
-
{
|
|
632
|
-
field: "contentTypeId",
|
|
633
|
-
value: contentType.id,
|
|
634
|
-
operator: "eq" as const,
|
|
635
|
-
},
|
|
636
|
-
];
|
|
637
|
-
|
|
638
|
-
if (slug) {
|
|
639
|
-
whereConditions.push({
|
|
640
|
-
field: "slug",
|
|
641
|
-
value: slug,
|
|
642
|
-
operator: "eq" as const,
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Get total count
|
|
647
|
-
const allItems = await adapter.findMany<ContentItem>({
|
|
648
|
-
model: "contentItem",
|
|
649
|
-
where: whereConditions,
|
|
650
|
-
});
|
|
651
|
-
const total = allItems.length;
|
|
652
|
-
|
|
653
|
-
// Get paginated items
|
|
654
|
-
const items = await adapter.findMany<ContentItemWithType>({
|
|
655
|
-
model: "contentItem",
|
|
656
|
-
where: whereConditions,
|
|
657
|
-
limit,
|
|
658
|
-
offset,
|
|
659
|
-
sortBy: { field: "createdAt", direction: "desc" },
|
|
660
|
-
join: { contentType: true },
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
return {
|
|
664
|
-
items: items.map(serializeContentItemWithType),
|
|
665
|
-
total,
|
|
666
|
-
limit,
|
|
667
|
-
offset,
|
|
668
|
-
};
|
|
563
|
+
return getAllContentItems(adapter, typeSlug, { slug, limit, offset });
|
|
669
564
|
},
|
|
670
565
|
);
|
|
671
566
|
|
|
@@ -1139,7 +1034,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
1139
1034
|
const { slug } = ctx.params;
|
|
1140
1035
|
const { itemId } = ctx.query;
|
|
1141
1036
|
|
|
1142
|
-
await ensureSynced();
|
|
1037
|
+
await ensureSynced(adapter);
|
|
1143
1038
|
|
|
1144
1039
|
// Get the target content type
|
|
1145
1040
|
const targetContentType = await getContentType(slug);
|
|
@@ -1239,7 +1134,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
1239
1134
|
const { slug, sourceType } = ctx.params;
|
|
1240
1135
|
const { itemId, fieldName, limit, offset } = ctx.query;
|
|
1241
1136
|
|
|
1242
|
-
await ensureSynced();
|
|
1137
|
+
await ensureSynced(adapter);
|
|
1243
1138
|
|
|
1244
1139
|
// Verify target content type exists
|
|
1245
1140
|
const targetContentType = await getContentType(slug);
|
|
@@ -1317,6 +1212,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
1317
1212
|
};
|
|
1318
1213
|
},
|
|
1319
1214
|
});
|
|
1215
|
+
};
|
|
1320
1216
|
|
|
1321
1217
|
export type CMSApiRouter = ReturnType<
|
|
1322
1218
|
ReturnType<typeof cmsBackendPlugin>["routes"]
|
|
@@ -241,7 +241,7 @@ export function ContentEditorPage({ typeSlug, id }: ContentEditorPageProps) {
|
|
|
241
241
|
contentType={contentType}
|
|
242
242
|
initialData={
|
|
243
243
|
isEditing
|
|
244
|
-
? item?.parsedData
|
|
244
|
+
? (item?.parsedData ?? undefined)
|
|
245
245
|
: Object.keys(prefillParams).length > 0
|
|
246
246
|
? convertPrefillToFormData(
|
|
247
247
|
prefillParams,
|
|
@@ -608,9 +608,11 @@ export function useCreateContent<TData = Record<string, unknown>>(
|
|
|
608
608
|
onSuccess: async () => {
|
|
609
609
|
await queryClient.invalidateQueries({
|
|
610
610
|
queryKey: queries.cmsContent.list._def,
|
|
611
|
+
refetchType: "all",
|
|
611
612
|
});
|
|
612
613
|
await queryClient.invalidateQueries({
|
|
613
614
|
queryKey: queries.cmsTypes.list._def,
|
|
615
|
+
refetchType: "all",
|
|
614
616
|
});
|
|
615
617
|
if (refresh) {
|
|
616
618
|
await refresh();
|
|
@@ -675,6 +677,7 @@ export function useUpdateContent<TData = Record<string, unknown>>(
|
|
|
675
677
|
}
|
|
676
678
|
await queryClient.invalidateQueries({
|
|
677
679
|
queryKey: queries.cmsContent.list._def,
|
|
680
|
+
refetchType: "all",
|
|
678
681
|
});
|
|
679
682
|
if (refresh) {
|
|
680
683
|
await refresh();
|
package/src/plugins/cms/types.ts
CHANGED
|
@@ -178,7 +178,7 @@ export interface SerializedContentItem
|
|
|
178
178
|
*/
|
|
179
179
|
export interface SerializedContentItemWithType<TData = Record<string, unknown>>
|
|
180
180
|
extends SerializedContentItem {
|
|
181
|
-
/** Parsed data object (JSON.parse of data field) */
|
|
181
|
+
/** Parsed data object (JSON.parse of data field). */
|
|
182
182
|
parsedData: TData;
|
|
183
183
|
/** Joined content type */
|
|
184
184
|
contentType?: SerializedContentType;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
3
|
+
import { defineDb } from "@btst/db";
|
|
4
|
+
import type { Adapter } from "@btst/db";
|
|
5
|
+
import { formBuilderSchema } from "../db";
|
|
6
|
+
import { getAllForms, getFormBySlug, getFormSubmissions } from "../api/getters";
|
|
7
|
+
|
|
8
|
+
const createTestAdapter = (): Adapter => {
|
|
9
|
+
const db = defineDb({}).use(formBuilderSchema);
|
|
10
|
+
return createMemoryAdapter(db)({});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const SIMPLE_SCHEMA = JSON.stringify({
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: { name: { type: "string" } },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function createForm(
|
|
19
|
+
adapter: Adapter,
|
|
20
|
+
slug: string,
|
|
21
|
+
status = "active",
|
|
22
|
+
): Promise<any> {
|
|
23
|
+
return adapter.create({
|
|
24
|
+
model: "form",
|
|
25
|
+
data: {
|
|
26
|
+
name: `Form ${slug}`,
|
|
27
|
+
slug,
|
|
28
|
+
schema: SIMPLE_SCHEMA,
|
|
29
|
+
status,
|
|
30
|
+
createdAt: new Date(),
|
|
31
|
+
updatedAt: new Date(),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("form-builder getters", () => {
|
|
37
|
+
let adapter: Adapter;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
adapter = createTestAdapter();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("getAllForms", () => {
|
|
44
|
+
it("returns empty result when no forms exist", async () => {
|
|
45
|
+
const result = await getAllForms(adapter);
|
|
46
|
+
expect(result.items).toEqual([]);
|
|
47
|
+
expect(result.total).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns all forms serialized", async () => {
|
|
51
|
+
await createForm(adapter, "contact");
|
|
52
|
+
await createForm(adapter, "feedback");
|
|
53
|
+
|
|
54
|
+
const result = await getAllForms(adapter);
|
|
55
|
+
expect(result.items).toHaveLength(2);
|
|
56
|
+
expect(result.total).toBe(2);
|
|
57
|
+
expect(typeof result.items[0]!.createdAt).toBe("string");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("filters forms by status", async () => {
|
|
61
|
+
await createForm(adapter, "active-form", "active");
|
|
62
|
+
await createForm(adapter, "inactive-form", "inactive");
|
|
63
|
+
|
|
64
|
+
const active = await getAllForms(adapter, { status: "active" });
|
|
65
|
+
expect(active.items).toHaveLength(1);
|
|
66
|
+
expect(active.items[0]!.slug).toBe("active-form");
|
|
67
|
+
|
|
68
|
+
const inactive = await getAllForms(adapter, { status: "inactive" });
|
|
69
|
+
expect(inactive.items).toHaveLength(1);
|
|
70
|
+
expect(inactive.items[0]!.slug).toBe("inactive-form");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("respects limit and offset", async () => {
|
|
74
|
+
for (let i = 1; i <= 4; i++) {
|
|
75
|
+
await createForm(adapter, `form-${i}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const page1 = await getAllForms(adapter, { limit: 2, offset: 0 });
|
|
79
|
+
expect(page1.items).toHaveLength(2);
|
|
80
|
+
expect(page1.total).toBe(4);
|
|
81
|
+
|
|
82
|
+
const page2 = await getAllForms(adapter, { limit: 2, offset: 2 });
|
|
83
|
+
expect(page2.items).toHaveLength(2);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("getFormBySlug", () => {
|
|
88
|
+
it("returns null when form does not exist", async () => {
|
|
89
|
+
const form = await getFormBySlug(adapter, "nonexistent");
|
|
90
|
+
expect(form).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns the form when it exists", async () => {
|
|
94
|
+
await createForm(adapter, "contact");
|
|
95
|
+
|
|
96
|
+
const form = await getFormBySlug(adapter, "contact");
|
|
97
|
+
expect(form).not.toBeNull();
|
|
98
|
+
expect(form!.slug).toBe("contact");
|
|
99
|
+
expect(typeof form!.createdAt).toBe("string");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("getFormSubmissions", () => {
|
|
104
|
+
it("returns empty result when form does not exist", async () => {
|
|
105
|
+
const result = await getFormSubmissions(adapter, "nonexistent-id");
|
|
106
|
+
expect(result.items).toEqual([]);
|
|
107
|
+
expect(result.total).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns submissions for a form", async () => {
|
|
111
|
+
const form = (await createForm(adapter, "contact")) as any;
|
|
112
|
+
|
|
113
|
+
await adapter.create({
|
|
114
|
+
model: "formSubmission",
|
|
115
|
+
data: {
|
|
116
|
+
formId: form.id,
|
|
117
|
+
data: JSON.stringify({ name: "Alice" }),
|
|
118
|
+
submittedAt: new Date(),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
await adapter.create({
|
|
122
|
+
model: "formSubmission",
|
|
123
|
+
data: {
|
|
124
|
+
formId: form.id,
|
|
125
|
+
data: JSON.stringify({ name: "Bob" }),
|
|
126
|
+
submittedAt: new Date(),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await getFormSubmissions(adapter, form.id);
|
|
131
|
+
expect(result.items).toHaveLength(2);
|
|
132
|
+
expect(result.total).toBe(2);
|
|
133
|
+
expect(typeof result.items[0]!.submittedAt).toBe("string");
|
|
134
|
+
expect(result.items[0]!.parsedData).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("respects pagination", async () => {
|
|
138
|
+
const form = (await createForm(adapter, "survey")) as any;
|
|
139
|
+
|
|
140
|
+
for (let i = 1; i <= 5; i++) {
|
|
141
|
+
await adapter.create({
|
|
142
|
+
model: "formSubmission",
|
|
143
|
+
data: {
|
|
144
|
+
formId: form.id,
|
|
145
|
+
data: JSON.stringify({ name: `User ${i}` }),
|
|
146
|
+
submittedAt: new Date(Date.now() + i * 1000),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const page1 = await getFormSubmissions(adapter, form.id, {
|
|
152
|
+
limit: 2,
|
|
153
|
+
offset: 0,
|
|
154
|
+
});
|
|
155
|
+
expect(page1.items).toHaveLength(2);
|
|
156
|
+
expect(page1.total).toBe(5);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type {
|
|
3
|
+
Form,
|
|
4
|
+
FormSubmission,
|
|
5
|
+
FormSubmissionWithForm,
|
|
6
|
+
SerializedForm,
|
|
7
|
+
SerializedFormSubmission,
|
|
8
|
+
SerializedFormSubmissionWithData,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a Form for SSR/SSG use (convert dates to strings).
|
|
13
|
+
*/
|
|
14
|
+
export function serializeForm(form: Form): SerializedForm {
|
|
15
|
+
return {
|
|
16
|
+
id: form.id,
|
|
17
|
+
name: form.name,
|
|
18
|
+
slug: form.slug,
|
|
19
|
+
description: form.description,
|
|
20
|
+
schema: form.schema,
|
|
21
|
+
successMessage: form.successMessage,
|
|
22
|
+
redirectUrl: form.redirectUrl,
|
|
23
|
+
status: form.status,
|
|
24
|
+
createdBy: form.createdBy,
|
|
25
|
+
createdAt: form.createdAt.toISOString(),
|
|
26
|
+
updatedAt: form.updatedAt.toISOString(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serialize a FormSubmission for SSR/SSG use (convert dates to strings).
|
|
32
|
+
*/
|
|
33
|
+
export function serializeFormSubmission(
|
|
34
|
+
submission: FormSubmission,
|
|
35
|
+
): SerializedFormSubmission {
|
|
36
|
+
return {
|
|
37
|
+
...submission,
|
|
38
|
+
submittedAt: submission.submittedAt.toISOString(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Serialize a FormSubmission with parsed data and joined Form.
|
|
44
|
+
* If `submission.data` is corrupted JSON, `parsedData` is set to `null` rather
|
|
45
|
+
* than throwing, so one bad row cannot crash the entire list or SSG build.
|
|
46
|
+
*/
|
|
47
|
+
export function serializeFormSubmissionWithData(
|
|
48
|
+
submission: FormSubmissionWithForm,
|
|
49
|
+
): SerializedFormSubmissionWithData {
|
|
50
|
+
let parsedData: Record<string, unknown> | null = null;
|
|
51
|
+
try {
|
|
52
|
+
parsedData = JSON.parse(submission.data);
|
|
53
|
+
} catch {
|
|
54
|
+
// Corrupted JSON — leave parsedData as null so callers can handle it
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...serializeFormSubmission(submission),
|
|
58
|
+
parsedData,
|
|
59
|
+
form: submission.form ? serializeForm(submission.form) : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retrieve all forms with optional status filter and pagination.
|
|
65
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
66
|
+
*
|
|
67
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeListForms`) are NOT
|
|
68
|
+
* called. The caller is responsible for any access-control checks before
|
|
69
|
+
* invoking this function.
|
|
70
|
+
*
|
|
71
|
+
* @param adapter - The database adapter
|
|
72
|
+
* @param params - Optional filter/pagination parameters
|
|
73
|
+
*/
|
|
74
|
+
export async function getAllForms(
|
|
75
|
+
adapter: Adapter,
|
|
76
|
+
params?: { status?: string; limit?: number; offset?: number },
|
|
77
|
+
): Promise<{
|
|
78
|
+
items: SerializedForm[];
|
|
79
|
+
total: number;
|
|
80
|
+
limit?: number;
|
|
81
|
+
offset?: number;
|
|
82
|
+
}> {
|
|
83
|
+
const whereConditions: Array<{
|
|
84
|
+
field: string;
|
|
85
|
+
value: string;
|
|
86
|
+
operator: "eq";
|
|
87
|
+
}> = [];
|
|
88
|
+
|
|
89
|
+
if (params?.status) {
|
|
90
|
+
whereConditions.push({
|
|
91
|
+
field: "status",
|
|
92
|
+
value: params.status,
|
|
93
|
+
operator: "eq" as const,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// TODO: remove cast once @btst/db types expose adapter.count()
|
|
98
|
+
const total: number = await adapter.count({
|
|
99
|
+
model: "form",
|
|
100
|
+
where: whereConditions.length > 0 ? whereConditions : undefined,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const forms = await adapter.findMany<Form>({
|
|
104
|
+
model: "form",
|
|
105
|
+
where: whereConditions.length > 0 ? whereConditions : undefined,
|
|
106
|
+
limit: params?.limit,
|
|
107
|
+
offset: params?.offset,
|
|
108
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
items: forms.map(serializeForm),
|
|
113
|
+
total,
|
|
114
|
+
limit: params?.limit,
|
|
115
|
+
offset: params?.offset,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retrieve a single form by its slug.
|
|
121
|
+
* Returns null if the form is not found.
|
|
122
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
123
|
+
*
|
|
124
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
125
|
+
* responsible for any access-control checks before invoking this function.
|
|
126
|
+
*
|
|
127
|
+
* @param adapter - The database adapter
|
|
128
|
+
* @param slug - The form slug
|
|
129
|
+
*/
|
|
130
|
+
export async function getFormBySlug(
|
|
131
|
+
adapter: Adapter,
|
|
132
|
+
slug: string,
|
|
133
|
+
): Promise<SerializedForm | null> {
|
|
134
|
+
const form = await adapter.findOne<Form>({
|
|
135
|
+
model: "form",
|
|
136
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!form) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return serializeForm(form);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Retrieve submissions for a form by form ID, with optional pagination.
|
|
148
|
+
* Returns an empty result if the form does not exist.
|
|
149
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side use.
|
|
150
|
+
*
|
|
151
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
152
|
+
* responsible for any access-control checks before invoking this function.
|
|
153
|
+
*
|
|
154
|
+
* @param adapter - The database adapter
|
|
155
|
+
* @param formId - The form ID
|
|
156
|
+
* @param params - Optional pagination parameters
|
|
157
|
+
*/
|
|
158
|
+
export async function getFormSubmissions(
|
|
159
|
+
adapter: Adapter,
|
|
160
|
+
formId: string,
|
|
161
|
+
params?: { limit?: number; offset?: number },
|
|
162
|
+
): Promise<{
|
|
163
|
+
items: SerializedFormSubmissionWithData[];
|
|
164
|
+
total: number;
|
|
165
|
+
limit?: number;
|
|
166
|
+
offset?: number;
|
|
167
|
+
}> {
|
|
168
|
+
const form = await adapter.findOne<Form>({
|
|
169
|
+
model: "form",
|
|
170
|
+
where: [{ field: "id", value: formId, operator: "eq" as const }],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!form) {
|
|
174
|
+
return {
|
|
175
|
+
items: [],
|
|
176
|
+
total: 0,
|
|
177
|
+
limit: params?.limit,
|
|
178
|
+
offset: params?.offset,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// TODO: remove cast once @btst/db types expose adapter.count()
|
|
183
|
+
const total: number = await adapter.count({
|
|
184
|
+
model: "formSubmission",
|
|
185
|
+
where: [{ field: "formId", value: formId, operator: "eq" as const }],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const submissions = await adapter.findMany<FormSubmissionWithForm>({
|
|
189
|
+
model: "formSubmission",
|
|
190
|
+
where: [{ field: "formId", value: formId, operator: "eq" as const }],
|
|
191
|
+
limit: params?.limit,
|
|
192
|
+
offset: params?.offset,
|
|
193
|
+
sortBy: { field: "submittedAt", direction: "desc" },
|
|
194
|
+
join: { form: true },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
items: submissions.map(serializeFormSubmissionWithData),
|
|
199
|
+
total,
|
|
200
|
+
limit: params?.limit,
|
|
201
|
+
offset: params?.offset,
|
|
202
|
+
};
|
|
203
|
+
}
|