@alstar/studio 0.0.0-beta.15 → 0.0.0-beta.18
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/api/block.ts +0 -14
- package/components/AdminPanel.ts +11 -5
- package/components/BlockFieldRenderer.ts +26 -20
- package/components/BlockRenderer.ts +4 -4
- package/components/Entries.ts +1 -1
- package/components/Entry.ts +13 -7
- package/components/FieldRenderer.ts +14 -8
- package/components/LivePreview.ts +37 -0
- package/components/Render.ts +8 -3
- package/components/SiteLayout.ts +1 -4
- package/components/fields/Markdown.ts +10 -3
- package/components/fields/Reference.ts +71 -0
- package/components/fields/Slug.ts +6 -6
- package/components/fields/Text.ts +13 -8
- package/components/fields/index.ts +2 -1
- package/components/icons.ts +3 -0
- package/components/settings/ApiKeys.ts +4 -4
- package/components/settings/Backup.ts +3 -3
- package/components/settings/Users.ts +1 -1
- package/index.ts +11 -10
- package/package.json +5 -6
- package/pages/entry/[id].ts +7 -1
- package/pages/error.ts +7 -6
- package/pages/login.ts +1 -1
- package/pages/register.ts +2 -2
- package/public/studio/css/admin-panel.css +27 -9
- package/public/studio/css/blocks-field.css +25 -0
- package/public/studio/css/entry-page.css +4 -0
- package/public/studio/css/entry.css +35 -0
- package/public/studio/css/field.css +14 -0
- package/public/studio/css/live-preview.css +25 -0
- package/public/studio/css/settings.css +4 -0
- package/public/studio/js/live-preview.js +26 -0
- package/public/studio/js/markdown-editor.js +6 -0
- package/public/studio/js/sortable-list.js +6 -4
- package/public/studio/main.css +11 -12
- package/public/studio/main.js +1 -0
- package/queries/block.ts +127 -105
- package/queries/index.ts +3 -2
- package/schemas.ts +1 -1
- package/types.ts +51 -75
- package/utils/define.ts +3 -1
- package/utils/get-or-create-row.ts +2 -1
- package/utils/refresher.ts +56 -0
- package/utils/renderSSE.ts +8 -3
- package/utils/startup-log.ts +4 -4
- package/queries/block-2.ts +0 -339
- package/queries/db-types.ts +0 -15
- package/queries/getBlockTrees-2.ts +0 -71
- package/queries/getBlocks.ts +0 -214
- package/queries/structure-types.ts +0 -97
- package/utils/buildBlocksTree.ts +0 -44
package/queries/block-2.ts
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import { db } from '@alstar/db'
|
|
2
|
-
import { sql } from '../utils/sql.ts'
|
|
3
|
-
import { type DBBlockResult } from '../types.ts'
|
|
4
|
-
|
|
5
|
-
function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
|
|
6
|
-
const map = new Map<number, DBBlockResult>()
|
|
7
|
-
const roots: DBBlockResult[] = []
|
|
8
|
-
|
|
9
|
-
for (const block of blocks) {
|
|
10
|
-
block.children = []
|
|
11
|
-
map.set(block.id, block)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
for (const block of blocks) {
|
|
15
|
-
if (block.parent_id === null) {
|
|
16
|
-
roots.push(block)
|
|
17
|
-
} else {
|
|
18
|
-
const parent = map.get(block.parent_id)
|
|
19
|
-
if (parent) parent.children!.push(block)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Sort children by sort_order recursively
|
|
24
|
-
const sortChildren = (node: DBBlockResult) => {
|
|
25
|
-
node.children!.sort((a, b) => a.sort_order - b.sort_order)
|
|
26
|
-
node.children!.forEach(sortChildren)
|
|
27
|
-
}
|
|
28
|
-
roots.forEach(sortChildren)
|
|
29
|
-
|
|
30
|
-
return roots
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function buildTree(blocks: DBBlockResult[]): DBBlockResult {
|
|
34
|
-
const map = new Map<number, DBBlockResult>()
|
|
35
|
-
const roots: DBBlockResult[] = []
|
|
36
|
-
|
|
37
|
-
for (const block of blocks) {
|
|
38
|
-
block.children = []
|
|
39
|
-
map.set(block.id, block)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const block of blocks) {
|
|
43
|
-
if (block.parent_id === null) {
|
|
44
|
-
roots.push(block)
|
|
45
|
-
} else {
|
|
46
|
-
const parent = map.get(block.parent_id)
|
|
47
|
-
if (parent) parent.children!.push(block)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Sort children by sort_order recursively
|
|
52
|
-
const sortChildren = (node: DBBlockResult) => {
|
|
53
|
-
node.children!.sort((a, b) => a.sort_order - b.sort_order)
|
|
54
|
-
node.children!.forEach(sortChildren)
|
|
55
|
-
}
|
|
56
|
-
roots.forEach(sortChildren)
|
|
57
|
-
|
|
58
|
-
return roots[0]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function transformBlocksTree(
|
|
62
|
-
block: DBBlockResult,
|
|
63
|
-
isBlocksChild?: boolean,
|
|
64
|
-
): DBBlockResult {
|
|
65
|
-
const fields: Record<string, DBBlockResult> = {}
|
|
66
|
-
let hasFields = false
|
|
67
|
-
|
|
68
|
-
for (const child of block.children ?? []) {
|
|
69
|
-
const transformedChild = transformBlocksTree(
|
|
70
|
-
child,
|
|
71
|
-
child.type === 'blocks',
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if (!isBlocksChild) {
|
|
75
|
-
hasFields = true
|
|
76
|
-
fields[transformedChild.name] = transformedChild
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if(hasFields) {
|
|
81
|
-
block.fields = fields
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!isBlocksChild) {
|
|
85
|
-
delete block.children
|
|
86
|
-
} else {
|
|
87
|
-
delete block.fields
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return block
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
|
|
94
|
-
return blocks.map((block) => transformBlocksTree(block))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function rootQuery(filterSql: string, depthLimit?: number) {
|
|
98
|
-
const depthLimitClause =
|
|
99
|
-
depthLimit !== undefined ? `WHERE d.depth + 1 <= ${depthLimit}` : ''
|
|
100
|
-
|
|
101
|
-
return sql`
|
|
102
|
-
with recursive
|
|
103
|
-
ancestors as (
|
|
104
|
-
select
|
|
105
|
-
id,
|
|
106
|
-
created_at,
|
|
107
|
-
updated_at,
|
|
108
|
-
name,
|
|
109
|
-
label,
|
|
110
|
-
type,
|
|
111
|
-
sort_order,
|
|
112
|
-
value,
|
|
113
|
-
options,
|
|
114
|
-
status,
|
|
115
|
-
parent_id,
|
|
116
|
-
0 as depth
|
|
117
|
-
from
|
|
118
|
-
blocks
|
|
119
|
-
where
|
|
120
|
-
${filterSql}
|
|
121
|
-
union all
|
|
122
|
-
select
|
|
123
|
-
b.id,
|
|
124
|
-
b.created_at,
|
|
125
|
-
b.updated_at,
|
|
126
|
-
b.name,
|
|
127
|
-
b.label,
|
|
128
|
-
b.type,
|
|
129
|
-
b.sort_order,
|
|
130
|
-
b.value,
|
|
131
|
-
b.options,
|
|
132
|
-
b.status,
|
|
133
|
-
b.parent_id,
|
|
134
|
-
a.depth + 1
|
|
135
|
-
from
|
|
136
|
-
blocks b
|
|
137
|
-
inner join ancestors a on b.id = a.parent_id
|
|
138
|
-
),
|
|
139
|
-
roots as (
|
|
140
|
-
select
|
|
141
|
-
id,
|
|
142
|
-
created_at,
|
|
143
|
-
updated_at,
|
|
144
|
-
name,
|
|
145
|
-
label,
|
|
146
|
-
type,
|
|
147
|
-
sort_order,
|
|
148
|
-
value,
|
|
149
|
-
options,
|
|
150
|
-
status,
|
|
151
|
-
parent_id,
|
|
152
|
-
0 as depth
|
|
153
|
-
from
|
|
154
|
-
ancestors
|
|
155
|
-
where
|
|
156
|
-
parent_id is null
|
|
157
|
-
),
|
|
158
|
-
descendants as (
|
|
159
|
-
select
|
|
160
|
-
id,
|
|
161
|
-
created_at,
|
|
162
|
-
updated_at,
|
|
163
|
-
name,
|
|
164
|
-
label,
|
|
165
|
-
type,
|
|
166
|
-
sort_order,
|
|
167
|
-
value,
|
|
168
|
-
options,
|
|
169
|
-
status,
|
|
170
|
-
parent_id,
|
|
171
|
-
depth
|
|
172
|
-
from
|
|
173
|
-
roots
|
|
174
|
-
union all
|
|
175
|
-
select
|
|
176
|
-
b.id,
|
|
177
|
-
b.created_at,
|
|
178
|
-
b.updated_at,
|
|
179
|
-
b.name,
|
|
180
|
-
b.label,
|
|
181
|
-
b.type,
|
|
182
|
-
b.sort_order,
|
|
183
|
-
b.value,
|
|
184
|
-
b.options,
|
|
185
|
-
b.status,
|
|
186
|
-
b.parent_id,
|
|
187
|
-
d.depth + 1
|
|
188
|
-
from
|
|
189
|
-
blocks b
|
|
190
|
-
inner join descendants d on b.parent_id = d.id ${depthLimitClause}
|
|
191
|
-
)
|
|
192
|
-
select
|
|
193
|
-
*
|
|
194
|
-
from
|
|
195
|
-
descendants
|
|
196
|
-
order by
|
|
197
|
-
sort_order,
|
|
198
|
-
id;
|
|
199
|
-
`
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function buildFilterSql(params: Record<string, any>) {
|
|
203
|
-
const entries = Object.entries(params)
|
|
204
|
-
const filterSql = entries
|
|
205
|
-
.map(([key, value]) =>
|
|
206
|
-
value === null ? `${key} is null` : `${key} = :${key}`,
|
|
207
|
-
)
|
|
208
|
-
.join(' and ')
|
|
209
|
-
|
|
210
|
-
let sqlParams: Record<keyof typeof params, any> = {}
|
|
211
|
-
|
|
212
|
-
for (const param in params) {
|
|
213
|
-
if (params[param] !== null) {
|
|
214
|
-
sqlParams[param] = params[param]
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return { filterSql, sqlParams }
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function roots(
|
|
222
|
-
params: Record<string, any>,
|
|
223
|
-
options?: {
|
|
224
|
-
depth?: number
|
|
225
|
-
},
|
|
226
|
-
): DBBlockResult[] | [] {
|
|
227
|
-
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
228
|
-
|
|
229
|
-
const query = rootQuery(filterSql, options?.depth)
|
|
230
|
-
const rows = db.database
|
|
231
|
-
.prepare(query)
|
|
232
|
-
.all(sqlParams) as unknown as DBBlockResult[]
|
|
233
|
-
|
|
234
|
-
if (!rows.length) return []
|
|
235
|
-
|
|
236
|
-
const forest = buildForest(rows)
|
|
237
|
-
|
|
238
|
-
return transformForest(forest)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
type DBRow = {
|
|
242
|
-
id: number
|
|
243
|
-
created_at: string
|
|
244
|
-
updated_at: string
|
|
245
|
-
name: string
|
|
246
|
-
label: string
|
|
247
|
-
type: string
|
|
248
|
-
sort_order: number
|
|
249
|
-
value: string | null
|
|
250
|
-
options: string | null
|
|
251
|
-
status: 'enabled' | 'disabled'
|
|
252
|
-
parent_id: number | null
|
|
253
|
-
depth: number
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
type TODO = any
|
|
257
|
-
|
|
258
|
-
function buildTree2(items: DBRow[]): TODO | null {
|
|
259
|
-
const map = new Map<number, TODO>();
|
|
260
|
-
|
|
261
|
-
// First pass: clone items into map
|
|
262
|
-
for (const item of items) {
|
|
263
|
-
map.set(item.id, { ...item });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
let root: TODO | null = null;
|
|
267
|
-
|
|
268
|
-
// Second pass: assign children to parents
|
|
269
|
-
for (const item of map.values()) {
|
|
270
|
-
if (item.parent_id === null) {
|
|
271
|
-
root = item; // Root node
|
|
272
|
-
} else {
|
|
273
|
-
const parent = map.get(item.parent_id);
|
|
274
|
-
if (parent) {
|
|
275
|
-
if(parent.type === 'blocks') {
|
|
276
|
-
if (!parent.children) parent.children = [];
|
|
277
|
-
parent.children.push(item);
|
|
278
|
-
} else {
|
|
279
|
-
if (!parent.fields) parent.fields = {};
|
|
280
|
-
parent.fields[item.name] = item;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return root;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function root(
|
|
290
|
-
params: Record<string, any>,
|
|
291
|
-
options?: { depth?: number },
|
|
292
|
-
): DBBlockResult | null {
|
|
293
|
-
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
294
|
-
|
|
295
|
-
const query = rootQuery(filterSql, options?.depth)
|
|
296
|
-
const rows = db.database
|
|
297
|
-
.prepare(query)
|
|
298
|
-
.all(sqlParams) as unknown as DBRow[]
|
|
299
|
-
|
|
300
|
-
if (!rows.length) return null
|
|
301
|
-
|
|
302
|
-
// const tree = buildTree(rows)
|
|
303
|
-
|
|
304
|
-
const tree = buildTree2(rows)
|
|
305
|
-
|
|
306
|
-
return tree // transformBlocksTree(tree)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function block(params: Record<string, any>) {
|
|
310
|
-
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
311
|
-
|
|
312
|
-
const query = sql`
|
|
313
|
-
select
|
|
314
|
-
*
|
|
315
|
-
from
|
|
316
|
-
blocks
|
|
317
|
-
where
|
|
318
|
-
${filterSql}
|
|
319
|
-
`
|
|
320
|
-
|
|
321
|
-
return db.database.prepare(query).get(sqlParams) as unknown as DBBlockResult
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function blocks(params: Record<string, any>) {
|
|
325
|
-
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
326
|
-
|
|
327
|
-
const query = sql`
|
|
328
|
-
select
|
|
329
|
-
*
|
|
330
|
-
from
|
|
331
|
-
blocks
|
|
332
|
-
where
|
|
333
|
-
${filterSql}
|
|
334
|
-
order by
|
|
335
|
-
sort_order
|
|
336
|
-
`
|
|
337
|
-
|
|
338
|
-
return db.database.prepare(query).all(sqlParams) as unknown as DBBlockResult[]
|
|
339
|
-
}
|
package/queries/db-types.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// db-types.ts
|
|
2
|
-
export type DBBase = {
|
|
3
|
-
id: number;
|
|
4
|
-
created_at: string;
|
|
5
|
-
updated_at: string;
|
|
6
|
-
name: string;
|
|
7
|
-
label: string;
|
|
8
|
-
type: string;
|
|
9
|
-
sort_order: number;
|
|
10
|
-
value: string | null;
|
|
11
|
-
options: any | null;
|
|
12
|
-
status: string;
|
|
13
|
-
parent_id: number | null;
|
|
14
|
-
depth: number;
|
|
15
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { structure } from "../index.ts";
|
|
2
|
-
|
|
3
|
-
export const defineField = <
|
|
4
|
-
F extends Record<string, any> | undefined = undefined,
|
|
5
|
-
C extends Record<string, any> | undefined = undefined
|
|
6
|
-
>(config: {
|
|
7
|
-
type: string;
|
|
8
|
-
label: string;
|
|
9
|
-
fields?: F;
|
|
10
|
-
children?: C;
|
|
11
|
-
}) =>
|
|
12
|
-
({
|
|
13
|
-
kind: 'field',
|
|
14
|
-
type: config.type,
|
|
15
|
-
label: config.label,
|
|
16
|
-
fields: (config.fields ?? {}) as F,
|
|
17
|
-
children: (config.children ?? {}) as C,
|
|
18
|
-
} as const);
|
|
19
|
-
|
|
20
|
-
export const defineBlock = <
|
|
21
|
-
F extends Record<string, any> = {},
|
|
22
|
-
C extends Record<string, any> = {}
|
|
23
|
-
>(config: {
|
|
24
|
-
type: string;
|
|
25
|
-
label: string;
|
|
26
|
-
fields?: F;
|
|
27
|
-
children?: C;
|
|
28
|
-
}) =>
|
|
29
|
-
({
|
|
30
|
-
kind: 'block',
|
|
31
|
-
type: config.type,
|
|
32
|
-
label: config.label,
|
|
33
|
-
fields: (config.fields ?? {}) as F,
|
|
34
|
-
children: (config.children ?? {}) as C,
|
|
35
|
-
} as const);
|
|
36
|
-
|
|
37
|
-
type InferBlock<T> =
|
|
38
|
-
// A Block has fields + children
|
|
39
|
-
T extends { kind: 'block'; fields: infer F; children: infer C }
|
|
40
|
-
? {
|
|
41
|
-
fields: { [K in keyof F]: InferBlock<F[K]> };
|
|
42
|
-
children: { [K in keyof C]: InferBlock<C[K]> }[keyof C][];
|
|
43
|
-
}
|
|
44
|
-
// A Field without children
|
|
45
|
-
: T extends { kind: 'field'; children: infer C; fields: infer F }
|
|
46
|
-
? keyof C extends never
|
|
47
|
-
? { value: string | null; type: string; label: string }
|
|
48
|
-
// A Field with children (like "blocks")
|
|
49
|
-
: {
|
|
50
|
-
fields: { [K in keyof F]: InferBlock<F[K]> };
|
|
51
|
-
children: { [K in keyof C]: InferBlock<C[K]> }[keyof C][];
|
|
52
|
-
type: string;
|
|
53
|
-
label: string;
|
|
54
|
-
}
|
|
55
|
-
: never;
|
|
56
|
-
|
|
57
|
-
// Top-level inference for the structure object
|
|
58
|
-
type InferStructure<S> = {
|
|
59
|
-
[K in keyof S]: InferBlock<S[K]>;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type AppStructure = typeof structure;
|
|
63
|
-
type AppBlocks = InferStructure<AppStructure>;
|
|
64
|
-
|
|
65
|
-
// Example: AppBlocks["page"] is the typed shape for a page block
|
|
66
|
-
export function getBlockTrees<T extends keyof AppBlocks>(
|
|
67
|
-
rootType: T
|
|
68
|
-
): AppBlocks[T][] {
|
|
69
|
-
// Your SQL + builder code goes here
|
|
70
|
-
return [] as any;
|
|
71
|
-
}
|
package/queries/getBlocks.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { sql } from "../utils/sql.ts";
|
|
3
|
-
|
|
4
|
-
// --- Field & block definitions ---
|
|
5
|
-
type FieldType = 'text' | 'slug' | 'markdown' | 'blocks' | 'image';
|
|
6
|
-
|
|
7
|
-
interface BaseField {
|
|
8
|
-
label: string;
|
|
9
|
-
type: FieldType;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface TextField extends BaseField {
|
|
13
|
-
type: 'text' | 'slug' | 'markdown';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface ImageField extends BaseField {
|
|
17
|
-
type: 'image';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface BlocksField extends BaseField {
|
|
21
|
-
type: 'blocks';
|
|
22
|
-
children: Record<string, BlockDef>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type FieldDef = TextField | ImageField | BlocksField;
|
|
26
|
-
|
|
27
|
-
interface BlockDef {
|
|
28
|
-
label: string;
|
|
29
|
-
type: string;
|
|
30
|
-
fields: Record<string, FieldDef>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// --- Identity helpers (preserve literal types) ---
|
|
34
|
-
export function defineField<T extends FieldDef>(field: T) {
|
|
35
|
-
return field;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function defineBlock<T extends BlockDef>(block: T) {
|
|
39
|
-
return block;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function defineStructure<T extends Record<string, BlockDef>>(structure: T) {
|
|
43
|
-
return structure;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// --- Type mapping from structure to data ---
|
|
47
|
-
type FieldToType<F extends FieldDef> =
|
|
48
|
-
F['type'] extends 'text' | 'slug' | 'markdown' ? string :
|
|
49
|
-
F['type'] extends 'image' ? string :
|
|
50
|
-
F['type'] extends 'blocks'
|
|
51
|
-
? {
|
|
52
|
-
[K in keyof F['children']]: {
|
|
53
|
-
type: K;
|
|
54
|
-
data: BlockData<F['children'][K]>;
|
|
55
|
-
}
|
|
56
|
-
}[keyof F['children']][]
|
|
57
|
-
: never;
|
|
58
|
-
|
|
59
|
-
type BlockData<B extends BlockDef> = {
|
|
60
|
-
[K in keyof B['fields']]: FieldToType<B['fields'][K]>;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
type StructureData<S extends Record<string, BlockDef>> = {
|
|
64
|
-
[K in keyof S]: BlockData<S[K]>;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// This will be inferred after you define your structure
|
|
68
|
-
export type CMSData<S extends Record<string, BlockDef>> = StructureData<S>;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
interface DBBase {
|
|
72
|
-
id: number;
|
|
73
|
-
created_at: string;
|
|
74
|
-
updated_at: string;
|
|
75
|
-
name: string;
|
|
76
|
-
label: string;
|
|
77
|
-
type: string;
|
|
78
|
-
sort_order: number;
|
|
79
|
-
value: string | null;
|
|
80
|
-
options: any;
|
|
81
|
-
status: string;
|
|
82
|
-
parent_id: number | null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function buildTypedForest<
|
|
86
|
-
S extends Record<string, BlockDef>,
|
|
87
|
-
K extends keyof S
|
|
88
|
-
>(
|
|
89
|
-
rows: DBBase[],
|
|
90
|
-
rootDef: S[K]
|
|
91
|
-
): CMSData<S>[K][] {
|
|
92
|
-
const map = new Map<number, DBBase & { children: DBBase[] }>();
|
|
93
|
-
|
|
94
|
-
// Initialize with children arrays
|
|
95
|
-
for (const r of rows) {
|
|
96
|
-
map.set(r.id, { ...r, children: [] });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Link children to parents
|
|
100
|
-
const roots: (DBBase & { children: DBBase[] })[] = [];
|
|
101
|
-
for (const r of rows) {
|
|
102
|
-
const node = map.get(r.id)!;
|
|
103
|
-
if (r.parent_id === null) {
|
|
104
|
-
roots.push(node);
|
|
105
|
-
} else {
|
|
106
|
-
const parent = map.get(r.parent_id);
|
|
107
|
-
if (parent) parent.children.push(node);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Recursive transformer: maps a DB row into typed data
|
|
112
|
-
function transformNode<D extends FieldDef | BlockDef>(
|
|
113
|
-
node: DBBase & DBBase['type'] extends 'blocks' ? { children: DBBase[] } : {},
|
|
114
|
-
def: D
|
|
115
|
-
): any {
|
|
116
|
-
if ('fields' in def) {
|
|
117
|
-
// It's a BlockDef
|
|
118
|
-
const result: any = {};
|
|
119
|
-
for (const key in def.fields) {
|
|
120
|
-
const fieldDef = def.fields[key];
|
|
121
|
-
const childNode = node.children.find(c => c.name === key);
|
|
122
|
-
result[key] = childNode
|
|
123
|
-
? transformNode(childNode, fieldDef)
|
|
124
|
-
: getDefaultValue(fieldDef);
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
} else {
|
|
128
|
-
// It's a FieldDef
|
|
129
|
-
if (def.type === 'text' || def.type === 'slug' || def.type === 'markdown') {
|
|
130
|
-
return node.value ?? '';
|
|
131
|
-
}
|
|
132
|
-
if (def.type === 'image') {
|
|
133
|
-
return node.value ?? '';
|
|
134
|
-
}
|
|
135
|
-
if (def.type === 'blocks') {
|
|
136
|
-
return node.children.map(child => {
|
|
137
|
-
const childDef = def.children[child.name as keyof typeof def.children];
|
|
138
|
-
return {
|
|
139
|
-
type: child.name as keyof typeof def.children,
|
|
140
|
-
data: transformNode(child, childDef)
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Provide safe defaults for missing fields
|
|
148
|
-
function getDefaultValue(fieldDef: FieldDef): any {
|
|
149
|
-
if (fieldDef.type === 'blocks') return [];
|
|
150
|
-
return '';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Map all root nodes into typed data
|
|
154
|
-
return roots.map(root => transformNode(root, rootDef));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
function rootQuery(filterSql: string, depthLimit?: number) {
|
|
159
|
-
const depthLimitClause =
|
|
160
|
-
depthLimit !== undefined ? `WHERE d.depth + 1 <= ${depthLimit}` : '';
|
|
161
|
-
|
|
162
|
-
return sql`
|
|
163
|
-
with recursive
|
|
164
|
-
ancestors as (
|
|
165
|
-
select
|
|
166
|
-
id, created_at, updated_at, name, label, type, sort_order, value, options, status, parent_id,
|
|
167
|
-
0 as depth
|
|
168
|
-
from blocks
|
|
169
|
-
where ${filterSql}
|
|
170
|
-
union all
|
|
171
|
-
select
|
|
172
|
-
b.id, b.created_at, b.updated_at, b.name, b.label, b.type, b.sort_order, b.value, b.options, b.status, b.parent_id,
|
|
173
|
-
a.depth + 1
|
|
174
|
-
from blocks b
|
|
175
|
-
inner join ancestors a on b.id = a.parent_id
|
|
176
|
-
),
|
|
177
|
-
roots as (
|
|
178
|
-
select * from ancestors where parent_id is null
|
|
179
|
-
),
|
|
180
|
-
descendants as (
|
|
181
|
-
select * from roots
|
|
182
|
-
union all
|
|
183
|
-
select
|
|
184
|
-
b.id, b.created_at, b.updated_at, b.name, b.label, b.type, b.sort_order, b.value, b.options, b.status, b.parent_id,
|
|
185
|
-
d.depth + 1
|
|
186
|
-
from blocks b
|
|
187
|
-
inner join descendants d on b.parent_id = d.id ${depthLimitClause}
|
|
188
|
-
)
|
|
189
|
-
select * from descendants
|
|
190
|
-
order by parent_id, sort_order
|
|
191
|
-
`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function queryTypedRoot<
|
|
195
|
-
S extends Record<string, BlockDef>,
|
|
196
|
-
K extends keyof S
|
|
197
|
-
>(
|
|
198
|
-
db: DatabaseSync,
|
|
199
|
-
structure: S,
|
|
200
|
-
blockType: K,
|
|
201
|
-
opts: { id?: number; depthLimit?: number }
|
|
202
|
-
): CMSData<S>[K][] {
|
|
203
|
-
let filterSql: string;
|
|
204
|
-
if (opts.id !== undefined) {
|
|
205
|
-
filterSql = `id = ${opts.id}`;
|
|
206
|
-
} else {
|
|
207
|
-
filterSql = `type = '${String(structure[blockType].type)}'`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const sql = rootQuery(filterSql, opts.depthLimit);
|
|
211
|
-
const rows = db.prepare(sql).all() as unknown as DBBase[];
|
|
212
|
-
|
|
213
|
-
return buildTypedForest(rows, structure[blockType]);
|
|
214
|
-
}
|