@btst/stack 2.1.0 → 2.3.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 +60 -107
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +60 -107
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +16 -1
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +17 -2
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +156 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +147 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +624 -617
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +623 -616
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
- 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/cms/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +120 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +112 -0
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +75 -86
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +71 -82
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
- 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/form-builder/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -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 +37 -123
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +37 -123
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +11 -1
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +12 -2
- package/dist/packages/stack/src/plugins/utils.cjs +6 -0
- package/dist/packages/stack/src/plugins/utils.mjs +6 -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 +9 -0
- package/dist/plugins/blog/api/index.d.cts +20 -4
- package/dist/plugins/blog/api/index.d.mts +20 -4
- package/dist/plugins/blog/api/index.d.ts +20 -4
- package/dist/plugins/blog/api/index.mjs +3 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +5 -5
- package/dist/plugins/blog/client/hooks/index.d.mts +5 -5
- package/dist/plugins/blog/client/hooks/index.d.ts +5 -5
- 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 +13 -9
- package/dist/plugins/blog/query-keys.d.cts +8 -333
- package/dist/plugins/blog/query-keys.d.mts +8 -333
- package/dist/plugins/blog/query-keys.d.ts +8 -333
- package/dist/plugins/blog/query-keys.mjs +13 -9
- package/dist/plugins/client/index.cjs +1 -0
- package/dist/plugins/client/index.d.cts +10 -3
- package/dist/plugins/client/index.d.mts +10 -3
- package/dist/plugins/client/index.d.ts +10 -3
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/cms/api/index.cjs +10 -0
- package/dist/plugins/cms/api/index.d.cts +7 -163
- package/dist/plugins/cms/api/index.d.mts +7 -163
- package/dist/plugins/cms/api/index.d.ts +7 -163
- package/dist/plugins/cms/api/index.mjs +2 -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.cjs +2 -1
- package/dist/plugins/cms/query-keys.d.cts +6 -9
- package/dist/plugins/cms/query-keys.d.mts +6 -9
- package/dist/plugins/cms/query-keys.d.ts +6 -9
- package/dist/plugins/cms/query-keys.mjs +2 -1
- package/dist/plugins/form-builder/api/index.cjs +10 -0
- package/dist/plugins/form-builder/api/index.d.cts +7 -141
- package/dist/plugins/form-builder/api/index.d.mts +7 -141
- package/dist/plugins/form-builder/api/index.d.ts +7 -141
- package/dist/plugins/form-builder/api/index.mjs +2 -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.cjs +3 -2
- package/dist/plugins/form-builder/query-keys.d.cts +7 -6
- package/dist/plugins/form-builder/query-keys.d.mts +7 -6
- package/dist/plugins/form-builder/query-keys.d.ts +7 -6
- package/dist/plugins/form-builder/query-keys.mjs +3 -2
- package/dist/plugins/kanban/api/index.cjs +9 -0
- package/dist/plugins/kanban/api/index.d.cts +17 -395
- package/dist/plugins/kanban/api/index.d.mts +17 -395
- package/dist/plugins/kanban/api/index.d.ts +17 -395
- package/dist/plugins/kanban/api/index.mjs +3 -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 +6 -12
- package/dist/plugins/kanban/query-keys.d.cts +5 -16
- package/dist/plugins/kanban/query-keys.d.mts +5 -16
- package/dist/plugins/kanban/query-keys.d.ts +5 -16
- package/dist/plugins/kanban/query-keys.mjs +6 -12
- 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.B1EeBt1b.d.ts +297 -0
- package/dist/shared/stack.BIXEI6v_.d.mts +419 -0
- package/dist/shared/stack.BKfolAyK.d.ts +419 -0
- package/dist/shared/stack.BeSm90va.d.ts +289 -0
- package/dist/shared/stack.BpolpQpf.d.cts +445 -0
- package/dist/shared/stack.C5dtIncc.d.mts +293 -0
- package/dist/shared/stack.CIP6QS9l.d.ts +293 -0
- package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
- package/dist/shared/stack.CP68pFEH.d.mts +297 -0
- package/dist/shared/{stack.BsXokfNh.d.mts → stack.CVDTkMoO.d.cts} +8 -2
- package/dist/shared/{stack.BsXokfNh.d.ts → stack.CVDTkMoO.d.mts} +8 -2
- package/dist/shared/{stack.BsXokfNh.d.cts → stack.CVDTkMoO.d.ts} +8 -2
- package/dist/shared/{stack.DKDMI-QO.d.mts → stack.DJaKVY7v.d.cts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.ts → stack.DJaKVY7v.d.mts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.cts → stack.DJaKVY7v.d.ts} +7 -1
- package/dist/shared/{stack.DzH_wcvr.d.mts → stack.DdI5W6MB.d.cts} +9 -3
- package/dist/shared/{stack.DzH_wcvr.d.ts → stack.DdI5W6MB.d.mts} +9 -3
- package/dist/shared/{stack.DzH_wcvr.d.cts → stack.DdI5W6MB.d.ts} +9 -3
- package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
- package/dist/shared/stack.Dw0Ly2TM.d.cts +293 -0
- package/dist/shared/stack.IdtKDRka.d.cts +297 -0
- package/dist/shared/stack.TIBF2AOx.d.ts +445 -0
- package/dist/shared/stack.rTy7-wQU.d.mts +445 -0
- package/dist/shared/stack.snB1EDP7.d.cts +419 -0
- package/package.json +3 -3
- 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 +9 -0
- package/src/plugins/blog/api/plugin.ts +98 -141
- package/src/plugins/blog/api/query-key-defs.ts +46 -0
- package/src/plugins/blog/api/serializers.ts +27 -0
- package/src/plugins/blog/client/plugin.tsx +21 -1
- package/src/plugins/blog/query-keys.ts +21 -20
- package/src/plugins/client/index.ts +1 -1
- package/src/plugins/cms/__tests__/getters.test.ts +206 -0
- package/src/plugins/cms/api/getters.ts +268 -0
- package/src/plugins/cms/api/index.ts +15 -1
- package/src/plugins/cms/api/plugin.ts +151 -150
- package/src/plugins/cms/api/query-key-defs.ts +53 -0
- package/src/plugins/cms/api/serializers.ts +12 -0
- 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/client/plugin.tsx +19 -0
- package/src/plugins/cms/query-keys.ts +2 -1
- 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 +226 -0
- package/src/plugins/form-builder/api/index.ts +15 -1
- package/src/plugins/form-builder/api/plugin.ts +107 -109
- package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
- package/src/plugins/form-builder/api/serializers.ts +12 -0
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
- package/src/plugins/form-builder/client/plugin.tsx +19 -0
- package/src/plugins/form-builder/query-keys.ts +6 -2
- 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 +4 -0
- package/src/plugins/kanban/api/plugin.ts +65 -146
- package/src/plugins/kanban/api/query-key-defs.ts +54 -0
- package/src/plugins/kanban/api/serializers.ts +49 -0
- package/src/plugins/kanban/client/plugin.tsx +15 -1
- package/src/plugins/kanban/query-keys.ts +10 -14
- package/src/plugins/utils.ts +19 -0
- package/src/types.ts +44 -5
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CBON0dWL.d.cts} +7 -7
- package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CBON0dWL.d.mts} +7 -7
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CBON0dWL.d.ts} +7 -7
|
@@ -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,96 +21,40 @@ import type {
|
|
|
23
21
|
} from "../types";
|
|
24
22
|
import { listContentQuerySchema } from "../schemas";
|
|
25
23
|
import { slugify } from "../utils";
|
|
24
|
+
import {
|
|
25
|
+
getAllContentTypes,
|
|
26
|
+
getAllContentItems,
|
|
27
|
+
getContentItemBySlug,
|
|
28
|
+
getContentItemById,
|
|
29
|
+
serializeContentType,
|
|
30
|
+
serializeContentItem,
|
|
31
|
+
serializeContentItemWithType,
|
|
32
|
+
} from "./getters";
|
|
33
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
34
|
+
import { CMS_QUERY_KEYS } from "./query-key-defs";
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*/
|
|
31
|
-
function migrateToUnifiedSchema(
|
|
32
|
-
jsonSchemaStr: string,
|
|
33
|
-
fieldConfigStr: string | null | undefined,
|
|
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
|
|
37
|
+
* Route keys for the CMS plugin — matches the keys returned by
|
|
38
|
+
* `stackClient.router.getRoute(path).routeKey`.
|
|
105
39
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
40
|
+
export type CMSRouteKey =
|
|
41
|
+
| "dashboard"
|
|
42
|
+
| "contentList"
|
|
43
|
+
| "newContent"
|
|
44
|
+
| "editContent";
|
|
45
|
+
|
|
46
|
+
interface CMSPrefetchForRoute {
|
|
47
|
+
(key: "dashboard" | "newContent", qc: QueryClient): Promise<void>;
|
|
48
|
+
(
|
|
49
|
+
key: "contentList",
|
|
50
|
+
qc: QueryClient,
|
|
51
|
+
params: { typeSlug: string },
|
|
52
|
+
): Promise<void>;
|
|
53
|
+
(
|
|
54
|
+
key: "editContent",
|
|
55
|
+
qc: QueryClient,
|
|
56
|
+
params: { typeSlug: string; id: string },
|
|
57
|
+
): Promise<void>;
|
|
116
58
|
}
|
|
117
59
|
|
|
118
60
|
/**
|
|
@@ -511,34 +453,130 @@ async function populateRelations(
|
|
|
511
453
|
*
|
|
512
454
|
* @param config - Configuration with content types and optional hooks
|
|
513
455
|
*/
|
|
514
|
-
export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
515
|
-
|
|
456
|
+
export const cmsBackendPlugin = (config: CMSBackendConfig) => {
|
|
457
|
+
// Shared sync state — used by both the api factory and routes handlers so
|
|
458
|
+
// that calling a getter before any HTTP request has been made still
|
|
459
|
+
// triggers the one-time content-type sync.
|
|
460
|
+
let syncPromise: Promise<void> | null = null;
|
|
461
|
+
|
|
462
|
+
const ensureSynced = (adapter: Adapter) => {
|
|
463
|
+
if (!syncPromise) {
|
|
464
|
+
syncPromise = syncContentTypes(adapter, config).catch((err) => {
|
|
465
|
+
// Allow retry on next call if sync fails
|
|
466
|
+
syncPromise = null;
|
|
467
|
+
throw err;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return syncPromise;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const getContentTypesWithCounts = async (adapter: Adapter) => {
|
|
474
|
+
const contentTypes = await getAllContentTypes(adapter);
|
|
475
|
+
return Promise.all(
|
|
476
|
+
contentTypes.map(async (ct) => {
|
|
477
|
+
const count: number = await adapter.count({
|
|
478
|
+
model: "contentItem",
|
|
479
|
+
where: [
|
|
480
|
+
{ field: "contentTypeId", value: ct.id, operator: "eq" as const },
|
|
481
|
+
],
|
|
482
|
+
});
|
|
483
|
+
return { ...ct, itemCount: count };
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const createCMSPrefetchForRoute = (adapter: Adapter): CMSPrefetchForRoute => {
|
|
489
|
+
return async function prefetchForRoute(
|
|
490
|
+
key: CMSRouteKey,
|
|
491
|
+
qc: QueryClient,
|
|
492
|
+
params?: Record<string, string>,
|
|
493
|
+
): Promise<void> {
|
|
494
|
+
// Sync content types once at the top — idempotent for concurrent SSG calls
|
|
495
|
+
await ensureSynced(adapter);
|
|
496
|
+
|
|
497
|
+
switch (key) {
|
|
498
|
+
case "dashboard":
|
|
499
|
+
case "newContent": {
|
|
500
|
+
const typesWithCounts = await getContentTypesWithCounts(adapter);
|
|
501
|
+
qc.setQueryData(CMS_QUERY_KEYS.typesList(), typesWithCounts);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "contentList": {
|
|
505
|
+
const typeSlug = params?.typeSlug ?? "";
|
|
506
|
+
const [contentTypes, contentItems] = await Promise.all([
|
|
507
|
+
getContentTypesWithCounts(adapter),
|
|
508
|
+
getAllContentItems(adapter, typeSlug, { limit: 20, offset: 0 }),
|
|
509
|
+
]);
|
|
510
|
+
qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
511
|
+
qc.setQueryData(
|
|
512
|
+
CMS_QUERY_KEYS.contentList({ typeSlug, limit: 20, offset: 0 }),
|
|
513
|
+
{
|
|
514
|
+
pages: [
|
|
515
|
+
{
|
|
516
|
+
items: contentItems.items,
|
|
517
|
+
total: contentItems.total,
|
|
518
|
+
limit: contentItems.limit ?? 20,
|
|
519
|
+
offset: contentItems.offset ?? 0,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
pageParams: [0],
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case "editContent": {
|
|
528
|
+
const typeSlug = params?.typeSlug ?? "";
|
|
529
|
+
const id = params?.id ?? "";
|
|
530
|
+
const [contentTypes, item] = await Promise.all([
|
|
531
|
+
getContentTypesWithCounts(adapter),
|
|
532
|
+
id ? getContentItemById(adapter, id) : Promise.resolve(null),
|
|
533
|
+
]);
|
|
534
|
+
qc.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
|
|
535
|
+
if (id) {
|
|
536
|
+
qc.setQueryData(CMS_QUERY_KEYS.contentDetail(typeSlug, id), item);
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
default:
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
} as CMSPrefetchForRoute;
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
return defineBackendPlugin({
|
|
516
547
|
name: "cms",
|
|
517
548
|
|
|
518
549
|
dbPlugin: dbSchema,
|
|
519
550
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
551
|
+
api: (adapter) => ({
|
|
552
|
+
getAllContentTypes: async () => {
|
|
553
|
+
await ensureSynced(adapter);
|
|
554
|
+
return getAllContentTypes(adapter);
|
|
555
|
+
},
|
|
556
|
+
getAllContentItems: async (
|
|
557
|
+
contentTypeSlug: string,
|
|
558
|
+
params?: Parameters<typeof getAllContentItems>[2],
|
|
559
|
+
) => {
|
|
560
|
+
await ensureSynced(adapter);
|
|
561
|
+
return getAllContentItems(adapter, contentTypeSlug, params);
|
|
562
|
+
},
|
|
563
|
+
getContentItemBySlug: async (contentTypeSlug: string, slug: string) => {
|
|
564
|
+
await ensureSynced(adapter);
|
|
565
|
+
return getContentItemBySlug(adapter, contentTypeSlug, slug);
|
|
566
|
+
},
|
|
567
|
+
getContentItemById: async (id: string) => {
|
|
568
|
+
await ensureSynced(adapter);
|
|
569
|
+
return getContentItemById(adapter, id);
|
|
570
|
+
},
|
|
571
|
+
prefetchForRoute: createCMSPrefetchForRoute(adapter),
|
|
572
|
+
}),
|
|
536
573
|
|
|
574
|
+
routes: (adapter: Adapter) => {
|
|
537
575
|
// Helper to get content type by slug
|
|
538
576
|
const getContentType = async (
|
|
539
577
|
slug: string,
|
|
540
578
|
): Promise<ContentType | null> => {
|
|
541
|
-
await ensureSynced();
|
|
579
|
+
await ensureSynced(adapter);
|
|
542
580
|
return adapter.findOne<ContentType>({
|
|
543
581
|
model: "contentType",
|
|
544
582
|
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
@@ -560,7 +598,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
560
598
|
"/content-types",
|
|
561
599
|
{ method: "GET" },
|
|
562
600
|
async (ctx) => {
|
|
563
|
-
await ensureSynced();
|
|
601
|
+
await ensureSynced(adapter);
|
|
564
602
|
|
|
565
603
|
const contentTypes = await adapter.findMany<ContentType>({
|
|
566
604
|
model: "contentType",
|
|
@@ -627,45 +665,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
627
665
|
throw ctx.error(404, { message: "Content type not found" });
|
|
628
666
|
}
|
|
629
667
|
|
|
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
|
-
};
|
|
668
|
+
return getAllContentItems(adapter, typeSlug, { slug, limit, offset });
|
|
669
669
|
},
|
|
670
670
|
);
|
|
671
671
|
|
|
@@ -1139,7 +1139,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
1139
1139
|
const { slug } = ctx.params;
|
|
1140
1140
|
const { itemId } = ctx.query;
|
|
1141
1141
|
|
|
1142
|
-
await ensureSynced();
|
|
1142
|
+
await ensureSynced(adapter);
|
|
1143
1143
|
|
|
1144
1144
|
// Get the target content type
|
|
1145
1145
|
const targetContentType = await getContentType(slug);
|
|
@@ -1239,7 +1239,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
1239
1239
|
const { slug, sourceType } = ctx.params;
|
|
1240
1240
|
const { itemId, fieldName, limit, offset } = ctx.query;
|
|
1241
1241
|
|
|
1242
|
-
await ensureSynced();
|
|
1242
|
+
await ensureSynced(adapter);
|
|
1243
1243
|
|
|
1244
1244
|
// Verify target content type exists
|
|
1245
1245
|
const targetContentType = await getContentType(slug);
|
|
@@ -1317,6 +1317,7 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) =>
|
|
|
1317
1317
|
};
|
|
1318
1318
|
},
|
|
1319
1319
|
});
|
|
1320
|
+
};
|
|
1320
1321
|
|
|
1321
1322
|
export type CMSApiRouter = ReturnType<
|
|
1322
1323
|
ReturnType<typeof cmsBackendPlugin>["routes"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal query key constants for the CMS plugin.
|
|
3
|
+
* Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
|
|
4
|
+
* to prevent key drift between SSR loaders and SSG prefetching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ContentListDiscriminator {
|
|
8
|
+
typeSlug: string;
|
|
9
|
+
limit: number;
|
|
10
|
+
offset: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds the discriminator object used as the cache key for the content list.
|
|
15
|
+
* Mirrors the params object used in createContentQueries.list so both paths stay in sync.
|
|
16
|
+
*/
|
|
17
|
+
export function contentListDiscriminator(params: {
|
|
18
|
+
typeSlug: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
offset?: number;
|
|
21
|
+
}): ContentListDiscriminator {
|
|
22
|
+
return {
|
|
23
|
+
typeSlug: params.typeSlug,
|
|
24
|
+
limit: params.limit ?? 20,
|
|
25
|
+
offset: params.offset ?? 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Full query key builders — use these with queryClient.setQueryData() */
|
|
30
|
+
export const CMS_QUERY_KEYS = {
|
|
31
|
+
/**
|
|
32
|
+
* Key for the cmsTypes.list() query.
|
|
33
|
+
* Full key: ["cmsTypes", "list", "list"]
|
|
34
|
+
*/
|
|
35
|
+
typesList: () => ["cmsTypes", "list", "list"] as const,
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Key for the cmsContent.list({ typeSlug, limit, offset }) query.
|
|
39
|
+
* Full key: ["cmsContent", "list", { typeSlug, limit, offset }]
|
|
40
|
+
*/
|
|
41
|
+
contentList: (params: {
|
|
42
|
+
typeSlug: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
}) => ["cmsContent", "list", contentListDiscriminator(params)] as const,
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Key for the cmsContent.detail(typeSlug, id) query.
|
|
49
|
+
* Full key: ["cmsContent", "detail", typeSlug, id]
|
|
50
|
+
*/
|
|
51
|
+
contentDetail: (typeSlug: string, id: string) =>
|
|
52
|
+
["cmsContent", "detail", typeSlug, id] as const,
|
|
53
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports serialization helpers from getters.ts for consumers who import
|
|
3
|
+
* from @btst/stack/plugins/cms/api.
|
|
4
|
+
*
|
|
5
|
+
* The actual implementations live in getters.ts alongside the DB functions
|
|
6
|
+
* they serialize so they stay in sync with the returned types.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
serializeContentType,
|
|
10
|
+
serializeContentItem,
|
|
11
|
+
serializeContentItemWithType,
|
|
12
|
+
} from "./getters";
|
|
@@ -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();
|
|
@@ -2,6 +2,7 @@ import { lazy } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
defineClientPlugin,
|
|
4
4
|
createApiClient,
|
|
5
|
+
isConnectionError,
|
|
5
6
|
} from "@btst/stack/plugins/client";
|
|
6
7
|
import { createRoute } from "@btst/yar";
|
|
7
8
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -181,6 +182,12 @@ function createDashboardLoader(config: CMSClientConfig) {
|
|
|
181
182
|
} catch (error) {
|
|
182
183
|
// Error hook - log the error but don't throw during SSR
|
|
183
184
|
// Let Error Boundaries handle errors when components render
|
|
185
|
+
if (isConnectionError(error)) {
|
|
186
|
+
console.warn(
|
|
187
|
+
"[btst/cms] route.loader() failed — no server running at build time. " +
|
|
188
|
+
"Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
184
191
|
if (hooks?.onLoadError) {
|
|
185
192
|
await hooks.onLoadError(error as Error, context);
|
|
186
193
|
}
|
|
@@ -275,6 +282,12 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
|
|
|
275
282
|
} catch (error) {
|
|
276
283
|
// Error hook - log the error but don't throw during SSR
|
|
277
284
|
// Let Error Boundaries handle errors when components render
|
|
285
|
+
if (isConnectionError(error)) {
|
|
286
|
+
console.warn(
|
|
287
|
+
"[btst/cms] route.loader() failed — no server running at build time. " +
|
|
288
|
+
"Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
278
291
|
if (hooks?.onLoadError) {
|
|
279
292
|
await hooks.onLoadError(error as Error, context);
|
|
280
293
|
}
|
|
@@ -357,6 +370,12 @@ function createContentEditorLoader(
|
|
|
357
370
|
} catch (error) {
|
|
358
371
|
// Error hook - log the error but don't throw during SSR
|
|
359
372
|
// Let Error Boundaries handle errors when components render
|
|
373
|
+
if (isConnectionError(error)) {
|
|
374
|
+
console.warn(
|
|
375
|
+
"[btst/cms] route.loader() failed — no server running at build time. " +
|
|
376
|
+
"Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
360
379
|
if (hooks?.onLoadError) {
|
|
361
380
|
await hooks.onLoadError(error as Error, context);
|
|
362
381
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SerializedContentItemWithType,
|
|
10
10
|
PaginatedContentItems,
|
|
11
11
|
} from "./types";
|
|
12
|
+
import { contentListDiscriminator } from "./api/query-key-defs";
|
|
12
13
|
|
|
13
14
|
interface ContentListParams {
|
|
14
15
|
limit?: number;
|
|
@@ -115,7 +116,7 @@ function createContentQueries(
|
|
|
115
116
|
) {
|
|
116
117
|
return createQueryKeys("cmsContent", {
|
|
117
118
|
list: (params: { typeSlug: string } & ContentListParams) => ({
|
|
118
|
-
queryKey: [params],
|
|
119
|
+
queryKey: [contentListDiscriminator(params)],
|
|
119
120
|
queryFn: async () => {
|
|
120
121
|
try {
|
|
121
122
|
const response: unknown = await client("/content/:typeSlug", {
|
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
|
+
});
|