@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.
Files changed (179) hide show
  1. package/dist/api/index.cjs +9 -1
  2. package/dist/api/index.d.cts +4 -4
  3. package/dist/api/index.d.mts +4 -4
  4. package/dist/api/index.d.ts +4 -4
  5. package/dist/api/index.mjs +9 -1
  6. package/dist/client/index.d.cts +2 -2
  7. package/dist/client/index.d.mts +2 -2
  8. package/dist/client/index.d.ts +2 -2
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +42 -0
  13. package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +39 -0
  14. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
  15. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
  16. package/dist/packages/stack/src/plugins/blog/api/getters.cjs +131 -0
  17. package/dist/packages/stack/src/plugins/blog/api/getters.mjs +127 -0
  18. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +9 -107
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +9 -107
  20. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +1 -1
  21. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +1 -1
  22. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +146 -0
  23. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +138 -0
  24. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +560 -622
  25. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +559 -621
  26. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
  27. package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
  28. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
  29. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
  30. package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +111 -0
  31. package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +104 -0
  32. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +16 -88
  33. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +12 -84
  34. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
  35. package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
  36. package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
  37. package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
  38. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +9 -123
  39. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +9 -123
  40. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +1 -1
  41. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +1 -1
  42. package/dist/plugins/ai-chat/api/index.cjs +3 -0
  43. package/dist/plugins/ai-chat/api/index.d.cts +27 -4
  44. package/dist/plugins/ai-chat/api/index.d.mts +27 -4
  45. package/dist/plugins/ai-chat/api/index.d.ts +27 -4
  46. package/dist/plugins/ai-chat/api/index.mjs +1 -0
  47. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  48. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  49. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  50. package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
  51. package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
  52. package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
  53. package/dist/plugins/api/index.d.cts +4 -3
  54. package/dist/plugins/api/index.d.mts +4 -3
  55. package/dist/plugins/api/index.d.ts +4 -3
  56. package/dist/plugins/blog/api/index.cjs +4 -0
  57. package/dist/plugins/blog/api/index.d.cts +3 -2
  58. package/dist/plugins/blog/api/index.d.mts +3 -2
  59. package/dist/plugins/blog/api/index.d.ts +3 -2
  60. package/dist/plugins/blog/api/index.mjs +1 -0
  61. package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
  62. package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
  63. package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
  64. package/dist/plugins/blog/client/index.d.cts +1 -1
  65. package/dist/plugins/blog/client/index.d.mts +1 -1
  66. package/dist/plugins/blog/client/index.d.ts +1 -1
  67. package/dist/plugins/blog/query-keys.cjs +7 -4
  68. package/dist/plugins/blog/query-keys.d.cts +81 -27
  69. package/dist/plugins/blog/query-keys.d.mts +81 -27
  70. package/dist/plugins/blog/query-keys.d.ts +81 -27
  71. package/dist/plugins/blog/query-keys.mjs +7 -4
  72. package/dist/plugins/client/index.d.cts +2 -2
  73. package/dist/plugins/client/index.d.mts +2 -2
  74. package/dist/plugins/client/index.d.ts +2 -2
  75. package/dist/plugins/cms/api/index.cjs +4 -0
  76. package/dist/plugins/cms/api/index.d.cts +61 -5
  77. package/dist/plugins/cms/api/index.d.mts +61 -5
  78. package/dist/plugins/cms/api/index.d.ts +61 -5
  79. package/dist/plugins/cms/api/index.mjs +1 -0
  80. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  81. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  82. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  83. package/dist/plugins/cms/query-keys.d.cts +2 -1
  84. package/dist/plugins/cms/query-keys.d.mts +2 -1
  85. package/dist/plugins/cms/query-keys.d.ts +2 -1
  86. package/dist/plugins/form-builder/api/index.cjs +4 -0
  87. package/dist/plugins/form-builder/api/index.d.cts +77 -7
  88. package/dist/plugins/form-builder/api/index.d.mts +77 -7
  89. package/dist/plugins/form-builder/api/index.d.ts +77 -7
  90. package/dist/plugins/form-builder/api/index.mjs +1 -0
  91. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  92. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  93. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  94. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  95. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  96. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  97. package/dist/plugins/form-builder/query-keys.d.cts +2 -1
  98. package/dist/plugins/form-builder/query-keys.d.mts +2 -1
  99. package/dist/plugins/form-builder/query-keys.d.ts +2 -1
  100. package/dist/plugins/kanban/api/index.cjs +3 -0
  101. package/dist/plugins/kanban/api/index.d.cts +40 -43
  102. package/dist/plugins/kanban/api/index.d.mts +40 -43
  103. package/dist/plugins/kanban/api/index.d.ts +40 -43
  104. package/dist/plugins/kanban/api/index.mjs +1 -0
  105. package/dist/plugins/kanban/client/components/index.d.cts +1 -1
  106. package/dist/plugins/kanban/client/components/index.d.mts +1 -1
  107. package/dist/plugins/kanban/client/components/index.d.ts +1 -1
  108. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  109. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  110. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  111. package/dist/plugins/kanban/client/index.d.cts +1 -1
  112. package/dist/plugins/kanban/client/index.d.mts +1 -1
  113. package/dist/plugins/kanban/client/index.d.ts +1 -1
  114. package/dist/plugins/kanban/query-keys.cjs +4 -3
  115. package/dist/plugins/kanban/query-keys.d.cts +2 -1
  116. package/dist/plugins/kanban/query-keys.d.mts +2 -1
  117. package/dist/plugins/kanban/query-keys.d.ts +2 -1
  118. package/dist/plugins/kanban/query-keys.mjs +4 -3
  119. package/dist/plugins/open-api/api/index.d.cts +2 -2
  120. package/dist/plugins/open-api/api/index.d.mts +2 -2
  121. package/dist/plugins/open-api/api/index.d.ts +2 -2
  122. package/dist/plugins/route-docs/client/index.d.cts +1 -1
  123. package/dist/plugins/route-docs/client/index.d.mts +1 -1
  124. package/dist/plugins/route-docs/client/index.d.ts +1 -1
  125. package/dist/plugins/ui-builder/index.d.cts +1 -1
  126. package/dist/plugins/ui-builder/index.d.mts +1 -1
  127. package/dist/plugins/ui-builder/index.d.ts +1 -1
  128. package/dist/shared/{stack.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
  129. package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
  130. package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
  131. package/dist/shared/stack.BeSm90va.d.ts +289 -0
  132. package/dist/shared/{stack.DzH_wcvr.d.mts → stack.CIrIsc-A.d.cts} +2 -2
  133. package/dist/shared/{stack.DzH_wcvr.d.ts → stack.CIrIsc-A.d.mts} +2 -2
  134. package/dist/shared/{stack.DzH_wcvr.d.cts → stack.CIrIsc-A.d.ts} +2 -2
  135. package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
  136. package/dist/shared/{stack.BsXokfNh.d.mts → stack.CXjzTMsb.d.cts} +1 -1
  137. package/dist/shared/{stack.BsXokfNh.d.ts → stack.CXjzTMsb.d.mts} +1 -1
  138. package/dist/shared/{stack.BsXokfNh.d.cts → stack.CXjzTMsb.d.ts} +1 -1
  139. package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
  140. package/dist/shared/{stack.DKDMI-QO.d.mts → stack.QD1y_7NY.d.cts} +7 -1
  141. package/dist/shared/{stack.DKDMI-QO.d.ts → stack.QD1y_7NY.d.mts} +7 -1
  142. package/dist/shared/{stack.DKDMI-QO.d.cts → stack.QD1y_7NY.d.ts} +7 -1
  143. package/package.json +1 -1
  144. package/src/__tests__/stack-api.test.ts +118 -0
  145. package/src/api/index.ts +15 -1
  146. package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
  147. package/src/plugins/ai-chat/api/getters.ts +71 -0
  148. package/src/plugins/ai-chat/api/index.ts +1 -0
  149. package/src/plugins/ai-chat/api/plugin.ts +8 -0
  150. package/src/plugins/api/index.ts +3 -1
  151. package/src/plugins/blog/__tests__/getters.test.ts +540 -0
  152. package/src/plugins/blog/api/getters.ts +243 -0
  153. package/src/plugins/blog/api/index.ts +7 -0
  154. package/src/plugins/blog/api/plugin.ts +13 -141
  155. package/src/plugins/blog/client/plugin.tsx +2 -1
  156. package/src/plugins/blog/query-keys.ts +16 -13
  157. package/src/plugins/cms/__tests__/getters.test.ts +206 -0
  158. package/src/plugins/cms/api/getters.ts +244 -0
  159. package/src/plugins/cms/api/index.ts +5 -0
  160. package/src/plugins/cms/api/plugin.ts +50 -154
  161. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
  162. package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
  163. package/src/plugins/cms/types.ts +1 -1
  164. package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
  165. package/src/plugins/form-builder/api/getters.ts +203 -0
  166. package/src/plugins/form-builder/api/index.ts +1 -0
  167. package/src/plugins/form-builder/api/plugin.ts +22 -115
  168. package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
  169. package/src/plugins/form-builder/types.ts +2 -2
  170. package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
  171. package/src/plugins/kanban/api/getters.ts +149 -0
  172. package/src/plugins/kanban/api/index.ts +1 -0
  173. package/src/plugins/kanban/api/plugin.ts +16 -146
  174. package/src/plugins/kanban/client/plugin.tsx +2 -1
  175. package/src/plugins/kanban/query-keys.ts +8 -5
  176. package/src/types.ts +44 -5
  177. package/dist/shared/{stack.CbuN2zVV.d.cts → stack.BkYlUT_8.d.cts} +6 -6
  178. package/dist/shared/{stack.CbuN2zVV.d.mts → stack.BkYlUT_8.d.mts} +6 -6
  179. package/dist/shared/{stack.CbuN2zVV.d.ts → stack.BkYlUT_8.d.ts} +6 -6
@@ -0,0 +1,206 @@
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 { cmsSchema } from "../db";
6
+ import {
7
+ getAllContentTypes,
8
+ getAllContentItems,
9
+ getContentItemBySlug,
10
+ } from "../api/getters";
11
+
12
+ const createTestAdapter = (): Adapter => {
13
+ const db = defineDb({}).use(cmsSchema);
14
+ return createMemoryAdapter(db)({});
15
+ };
16
+
17
+ const SIMPLE_SCHEMA = JSON.stringify({
18
+ type: "object",
19
+ properties: {
20
+ title: { type: "string" },
21
+ },
22
+ autoFormVersion: 2,
23
+ });
24
+
25
+ describe("cms getters", () => {
26
+ let adapter: Adapter;
27
+
28
+ beforeEach(() => {
29
+ adapter = createTestAdapter();
30
+ });
31
+
32
+ describe("getAllContentTypes", () => {
33
+ it("returns empty array when no content types exist", async () => {
34
+ const types = await getAllContentTypes(adapter);
35
+ expect(types).toEqual([]);
36
+ });
37
+
38
+ it("returns serialized content types sorted by name", async () => {
39
+ await adapter.create({
40
+ model: "contentType",
41
+ data: {
42
+ name: "Post",
43
+ slug: "post",
44
+ jsonSchema: SIMPLE_SCHEMA,
45
+ autoFormVersion: 2,
46
+ createdAt: new Date(),
47
+ updatedAt: new Date(),
48
+ },
49
+ });
50
+ await adapter.create({
51
+ model: "contentType",
52
+ data: {
53
+ name: "Article",
54
+ slug: "article",
55
+ jsonSchema: SIMPLE_SCHEMA,
56
+ autoFormVersion: 2,
57
+ createdAt: new Date(),
58
+ updatedAt: new Date(),
59
+ },
60
+ });
61
+
62
+ const types = await getAllContentTypes(adapter);
63
+ expect(types).toHaveLength(2);
64
+ // Sorted by name
65
+ expect(types[0]!.slug).toBe("article");
66
+ expect(types[1]!.slug).toBe("post");
67
+ // Dates are serialized as strings
68
+ expect(typeof types[0]!.createdAt).toBe("string");
69
+ });
70
+ });
71
+
72
+ describe("getAllContentItems", () => {
73
+ it("returns empty result when content type does not exist", async () => {
74
+ const result = await getAllContentItems(adapter, "nonexistent");
75
+ expect(result.items).toEqual([]);
76
+ expect(result.total).toBe(0);
77
+ });
78
+
79
+ it("returns items for a content type", async () => {
80
+ const ct = (await adapter.create({
81
+ model: "contentType",
82
+ data: {
83
+ name: "Post",
84
+ slug: "post",
85
+ jsonSchema: SIMPLE_SCHEMA,
86
+ autoFormVersion: 2,
87
+ createdAt: new Date(),
88
+ updatedAt: new Date(),
89
+ },
90
+ })) as any;
91
+
92
+ await adapter.create({
93
+ model: "contentItem",
94
+ data: {
95
+ contentTypeId: ct.id,
96
+ slug: "my-post",
97
+ data: JSON.stringify({ title: "My Post" }),
98
+ createdAt: new Date(),
99
+ updatedAt: new Date(),
100
+ },
101
+ });
102
+
103
+ const result = await getAllContentItems(adapter, "post");
104
+ expect(result.items).toHaveLength(1);
105
+ expect(result.total).toBe(1);
106
+ expect(result.items[0]!.slug).toBe("my-post");
107
+ expect(result.items[0]!.parsedData).toEqual({ title: "My Post" });
108
+ });
109
+
110
+ it("filters items by slug", async () => {
111
+ const ct = (await adapter.create({
112
+ model: "contentType",
113
+ data: {
114
+ name: "Post",
115
+ slug: "post",
116
+ jsonSchema: SIMPLE_SCHEMA,
117
+ autoFormVersion: 2,
118
+ createdAt: new Date(),
119
+ updatedAt: new Date(),
120
+ },
121
+ })) as any;
122
+
123
+ await adapter.create({
124
+ model: "contentItem",
125
+ data: {
126
+ contentTypeId: ct.id,
127
+ slug: "first",
128
+ data: JSON.stringify({ title: "First" }),
129
+ createdAt: new Date(),
130
+ updatedAt: new Date(),
131
+ },
132
+ });
133
+ await adapter.create({
134
+ model: "contentItem",
135
+ data: {
136
+ contentTypeId: ct.id,
137
+ slug: "second",
138
+ data: JSON.stringify({ title: "Second" }),
139
+ createdAt: new Date(),
140
+ updatedAt: new Date(),
141
+ },
142
+ });
143
+
144
+ const result = await getAllContentItems(adapter, "post", {
145
+ slug: "first",
146
+ });
147
+ expect(result.items).toHaveLength(1);
148
+ expect(result.items[0]!.slug).toBe("first");
149
+ });
150
+ });
151
+
152
+ describe("getContentItemBySlug", () => {
153
+ it("returns null when content type does not exist", async () => {
154
+ const item = await getContentItemBySlug(adapter, "nonexistent", "item");
155
+ expect(item).toBeNull();
156
+ });
157
+
158
+ it("returns null when item does not exist", async () => {
159
+ await adapter.create({
160
+ model: "contentType",
161
+ data: {
162
+ name: "Post",
163
+ slug: "post",
164
+ jsonSchema: SIMPLE_SCHEMA,
165
+ autoFormVersion: 2,
166
+ createdAt: new Date(),
167
+ updatedAt: new Date(),
168
+ },
169
+ });
170
+
171
+ const item = await getContentItemBySlug(adapter, "post", "nonexistent");
172
+ expect(item).toBeNull();
173
+ });
174
+
175
+ it("returns the serialized item when it exists", async () => {
176
+ const ct = (await adapter.create({
177
+ model: "contentType",
178
+ data: {
179
+ name: "Post",
180
+ slug: "post",
181
+ jsonSchema: SIMPLE_SCHEMA,
182
+ autoFormVersion: 2,
183
+ createdAt: new Date(),
184
+ updatedAt: new Date(),
185
+ },
186
+ })) as any;
187
+
188
+ await adapter.create({
189
+ model: "contentItem",
190
+ data: {
191
+ contentTypeId: ct.id,
192
+ slug: "hello",
193
+ data: JSON.stringify({ title: "Hello" }),
194
+ createdAt: new Date(),
195
+ updatedAt: new Date(),
196
+ },
197
+ });
198
+
199
+ const item = await getContentItemBySlug(adapter, "post", "hello");
200
+ expect(item).not.toBeNull();
201
+ expect(item!.slug).toBe("hello");
202
+ expect(item!.parsedData).toEqual({ title: "Hello" });
203
+ expect(typeof item!.createdAt).toBe("string");
204
+ });
205
+ });
206
+ });
@@ -0,0 +1,244 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import type {
3
+ ContentType,
4
+ ContentItem,
5
+ ContentItemWithType,
6
+ SerializedContentType,
7
+ SerializedContentItem,
8
+ SerializedContentItemWithType,
9
+ } from "../types";
10
+
11
+ /**
12
+ * Serialize a ContentType for SSR/SSG use (convert dates to strings).
13
+ * Applies lazy migration for legacy schemas (version 1 → 2).
14
+ */
15
+ export function serializeContentType(ct: ContentType): SerializedContentType {
16
+ const needsMigration = !ct.autoFormVersion || ct.autoFormVersion < 2;
17
+ const migratedJsonSchema = needsMigration
18
+ ? migrateToUnifiedSchema(ct.jsonSchema, ct.fieldConfig)
19
+ : ct.jsonSchema;
20
+
21
+ return {
22
+ id: ct.id,
23
+ name: ct.name,
24
+ slug: ct.slug,
25
+ description: ct.description,
26
+ jsonSchema: migratedJsonSchema,
27
+ createdAt: ct.createdAt.toISOString(),
28
+ updatedAt: ct.updatedAt.toISOString(),
29
+ };
30
+ }
31
+
32
+ export function migrateToUnifiedSchema(
33
+ jsonSchemaStr: string,
34
+ fieldConfigStr: string | null | undefined,
35
+ ): string {
36
+ if (!fieldConfigStr) return jsonSchemaStr;
37
+ try {
38
+ const jsonSchema = JSON.parse(jsonSchemaStr);
39
+ const fieldConfig = JSON.parse(fieldConfigStr);
40
+ if (!jsonSchema.properties || typeof fieldConfig !== "object") {
41
+ return jsonSchemaStr;
42
+ }
43
+ for (const [key, config] of Object.entries(fieldConfig)) {
44
+ if (
45
+ jsonSchema.properties[key] &&
46
+ typeof config === "object" &&
47
+ config !== null &&
48
+ "fieldType" in config
49
+ ) {
50
+ jsonSchema.properties[key].fieldType = (
51
+ config as { fieldType: string }
52
+ ).fieldType;
53
+ }
54
+ }
55
+ return JSON.stringify(jsonSchema);
56
+ } catch {
57
+ return jsonSchemaStr;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Serialize a ContentItem for SSR/SSG use (convert dates to strings).
63
+ */
64
+ export function serializeContentItem(item: ContentItem): SerializedContentItem {
65
+ return {
66
+ ...item,
67
+ createdAt: item.createdAt.toISOString(),
68
+ updatedAt: item.updatedAt.toISOString(),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Serialize a ContentItem with parsed data and joined ContentType.
74
+ * Throws a SyntaxError if `item.data` is not valid JSON, so corrupted rows
75
+ * produce a visible, debuggable error rather than silently returning null.
76
+ */
77
+ export function serializeContentItemWithType(
78
+ item: ContentItemWithType,
79
+ ): SerializedContentItemWithType {
80
+ const parsedData = JSON.parse(item.data) as Record<string, unknown>;
81
+ return {
82
+ ...serializeContentItem(item),
83
+ parsedData,
84
+ contentType: item.contentType
85
+ ? serializeContentType(item.contentType)
86
+ : undefined,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Retrieve all content types.
92
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
93
+ *
94
+ * @remarks **Security:** Authorization hooks are NOT called. The caller is
95
+ * responsible for any access-control checks before invoking this function.
96
+ *
97
+ * @param adapter - The database adapter
98
+ */
99
+ export async function getAllContentTypes(
100
+ adapter: Adapter,
101
+ ): Promise<SerializedContentType[]> {
102
+ const contentTypes = await adapter.findMany<ContentType>({
103
+ model: "contentType",
104
+ sortBy: { field: "name", direction: "asc" },
105
+ });
106
+ return contentTypes.map(serializeContentType);
107
+ }
108
+
109
+ /**
110
+ * Retrieve all content items for a given content type, with optional pagination.
111
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
112
+ *
113
+ * @remarks **Security:** Authorization hooks (e.g. `onBeforeListItems`) are NOT
114
+ * called. The caller is responsible for any access-control checks before
115
+ * invoking this function.
116
+ *
117
+ * @param adapter - The database adapter
118
+ * @param contentTypeSlug - The slug of the content type to query
119
+ * @param params - Optional filter/pagination parameters
120
+ */
121
+ export async function getAllContentItems(
122
+ adapter: Adapter,
123
+ contentTypeSlug: string,
124
+ params?: { slug?: string; limit?: number; offset?: number },
125
+ ): Promise<{
126
+ items: SerializedContentItemWithType[];
127
+ total: number;
128
+ limit?: number;
129
+ offset?: number;
130
+ }> {
131
+ const contentType = await adapter.findOne<ContentType>({
132
+ model: "contentType",
133
+ where: [
134
+ {
135
+ field: "slug",
136
+ value: contentTypeSlug,
137
+ operator: "eq" as const,
138
+ },
139
+ ],
140
+ });
141
+
142
+ if (!contentType) {
143
+ return {
144
+ items: [],
145
+ total: 0,
146
+ limit: params?.limit,
147
+ offset: params?.offset,
148
+ };
149
+ }
150
+
151
+ const whereConditions: Array<{
152
+ field: string;
153
+ value: string;
154
+ operator: "eq";
155
+ }> = [
156
+ {
157
+ field: "contentTypeId",
158
+ value: contentType.id,
159
+ operator: "eq" as const,
160
+ },
161
+ ];
162
+
163
+ if (params?.slug) {
164
+ whereConditions.push({
165
+ field: "slug",
166
+ value: params.slug,
167
+ operator: "eq" as const,
168
+ });
169
+ }
170
+
171
+ // TODO: remove cast once @btst/db types expose adapter.count()
172
+ const total: number = await adapter.count({
173
+ model: "contentItem",
174
+ where: whereConditions,
175
+ });
176
+
177
+ const items = await adapter.findMany<ContentItemWithType>({
178
+ model: "contentItem",
179
+ where: whereConditions,
180
+ limit: params?.limit,
181
+ offset: params?.offset,
182
+ sortBy: { field: "createdAt", direction: "desc" },
183
+ join: { contentType: true },
184
+ });
185
+
186
+ return {
187
+ items: items.map(serializeContentItemWithType),
188
+ total,
189
+ limit: params?.limit,
190
+ offset: params?.offset,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Retrieve a single content item by its slug within a content type.
196
+ * Returns null if the content type or item is not found.
197
+ * Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
198
+ *
199
+ * @remarks **Security:** Authorization hooks are NOT called. The caller is
200
+ * responsible for any access-control checks before invoking this function.
201
+ *
202
+ * @param adapter - The database adapter
203
+ * @param contentTypeSlug - The slug of the content type
204
+ * @param slug - The slug of the content item
205
+ */
206
+ export async function getContentItemBySlug(
207
+ adapter: Adapter,
208
+ contentTypeSlug: string,
209
+ slug: string,
210
+ ): Promise<SerializedContentItemWithType | null> {
211
+ const contentType = await adapter.findOne<ContentType>({
212
+ model: "contentType",
213
+ where: [
214
+ {
215
+ field: "slug",
216
+ value: contentTypeSlug,
217
+ operator: "eq" as const,
218
+ },
219
+ ],
220
+ });
221
+
222
+ if (!contentType) {
223
+ return null;
224
+ }
225
+
226
+ const item = await adapter.findOne<ContentItemWithType>({
227
+ model: "contentItem",
228
+ where: [
229
+ {
230
+ field: "contentTypeId",
231
+ value: contentType.id,
232
+ operator: "eq" as const,
233
+ },
234
+ { field: "slug", value: slug, operator: "eq" as const },
235
+ ],
236
+ join: { contentType: true },
237
+ });
238
+
239
+ if (!item) {
240
+ return null;
241
+ }
242
+
243
+ return serializeContentItemWithType(item);
244
+ }
@@ -1 +1,6 @@
1
1
  export { cmsBackendPlugin, type CMSApiRouter } from "./plugin";
2
+ export {
3
+ getAllContentTypes,
4
+ getAllContentItems,
5
+ getContentItemBySlug,
6
+ } from "./getters";