@alstar/studio 0.0.0-beta.1 → 0.0.0-beta.10

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 (58) hide show
  1. package/api/api-key.ts +73 -0
  2. package/api/backup.ts +38 -0
  3. package/api/block.ts +96 -30
  4. package/api/index.ts +12 -1
  5. package/api/mcp.ts +53 -0
  6. package/components/AdminPanel.ts +74 -0
  7. package/components/Backup.ts +10 -0
  8. package/components/BlockFieldRenderer.ts +121 -0
  9. package/components/BlockRenderer.ts +22 -0
  10. package/components/Entries.ts +19 -11
  11. package/components/Entry.ts +13 -21
  12. package/components/FieldRenderer.ts +35 -0
  13. package/components/Render.ts +46 -0
  14. package/components/Settings.ts +101 -0
  15. package/components/{layout.ts → SiteLayout.ts} +22 -17
  16. package/components/fields/Markdown.ts +44 -0
  17. package/components/fields/Text.ts +42 -0
  18. package/components/fields/index.ts +4 -0
  19. package/components/icons.ts +100 -7
  20. package/index.ts +66 -34
  21. package/package.json +8 -8
  22. package/pages/entry/[id].ts +17 -0
  23. package/{components → pages}/index.ts +7 -4
  24. package/pages/settings.ts +10 -0
  25. package/public/studio/css/admin-panel.css +103 -0
  26. package/public/studio/css/blocks-field.css +53 -0
  27. package/public/studio/css/settings.css +24 -0
  28. package/public/studio/js/markdown-editor.js +34 -0
  29. package/public/studio/js/sortable-list.js +50 -0
  30. package/public/studio/main.css +161 -0
  31. package/public/studio/main.js +9 -0
  32. package/queries/block-with-children.ts +74 -0
  33. package/queries/block.ts +289 -0
  34. package/queries/db-types.ts +15 -0
  35. package/queries/getBlockTrees-2.ts +0 -0
  36. package/queries/getBlockTrees.ts +316 -0
  37. package/queries/getBlocks.ts +214 -0
  38. package/queries/index.ts +2 -98
  39. package/queries/structure-types.ts +97 -0
  40. package/readme.md +205 -0
  41. package/schemas.ts +15 -54
  42. package/types.ts +133 -5
  43. package/utils/buildBlocksTree.ts +4 -4
  44. package/utils/define.ts +41 -0
  45. package/utils/file-based-router.ts +11 -2
  46. package/utils/get-config.ts +8 -9
  47. package/utils/get-or-create-row.ts +41 -0
  48. package/utils/html.ts +247 -0
  49. package/utils/startup-log.ts +10 -0
  50. package/components/AdminPanel/AdminPanel.css +0 -59
  51. package/components/AdminPanel/AdminPanel.ts +0 -57
  52. package/components/Block.ts +0 -116
  53. package/components/Entry.css +0 -7
  54. package/components/Field.ts +0 -164
  55. package/components/Fields.ts +0 -43
  56. package/public/main.css +0 -92
  57. package/public/main.js +0 -43
  58. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
@@ -0,0 +1,289 @@
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
+ export function root(
242
+ params: Record<string, any>,
243
+ options?: { depth?: number },
244
+ ): DBBlockResult | null {
245
+ const { filterSql, sqlParams } = buildFilterSql(params)
246
+
247
+ const query = rootQuery(filterSql, options?.depth)
248
+ const rows = db.database
249
+ .prepare(query)
250
+ .all(sqlParams) as unknown as DBBlockResult[]
251
+
252
+ if (!rows.length) return null
253
+
254
+ const tree = buildTree(rows)
255
+
256
+ return transformBlocksTree(tree)
257
+ }
258
+
259
+ export function block(params: Record<string, any>) {
260
+ const { filterSql, sqlParams } = buildFilterSql(params)
261
+
262
+ const query = sql`
263
+ select
264
+ *
265
+ from
266
+ blocks
267
+ where
268
+ ${filterSql}
269
+ `
270
+
271
+ return db.database.prepare(query).get(sqlParams) as unknown as DBBlockResult
272
+ }
273
+
274
+ export function blocks(params: Record<string, any>) {
275
+ const { filterSql, sqlParams } = buildFilterSql(params)
276
+
277
+ const query = sql`
278
+ select
279
+ *
280
+ from
281
+ blocks
282
+ where
283
+ ${filterSql}
284
+ order by
285
+ sort_order
286
+ `
287
+
288
+ return db.database.prepare(query).all(sqlParams) as unknown as DBBlockResult[]
289
+ }
@@ -0,0 +1,15 @@
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
+ };
File without changes
@@ -0,0 +1,316 @@
1
+ // types-and-runtime.ts
2
+ // import Database from "better-sqlite3";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { sql } from "../utils/sql.ts";
5
+
6
+ /* =========================
7
+ Runtime helpers (preserve literal types)
8
+ ========================= */
9
+ export type FieldType = "text" | "slug" | "markdown" | "image" | "blocks";
10
+
11
+ export function defineField<const T extends { name: string; label: string; type: FieldType; fields?: readonly any[] }>(def: T) {
12
+ return def;
13
+ }
14
+
15
+ export function defineBlock<const T extends { name: string; label: string; type: string; fields?: readonly any[] }>(def: T) {
16
+ return def;
17
+ }
18
+
19
+ export function defineStructure<const T extends readonly any[]>(blocks: T) {
20
+ return blocks;
21
+ }
22
+
23
+ /* =========================
24
+ DB base type (every row shape)
25
+ ========================= */
26
+ export type DBBase = {
27
+ id: number;
28
+ created_at: string;
29
+ updated_at: string;
30
+ name: string;
31
+ label: string;
32
+ type: string;
33
+ sort_order: number;
34
+ value: string | null;
35
+ options: any | null;
36
+ status: string;
37
+ parent_id: number | null;
38
+ depth: number;
39
+ };
40
+
41
+ /* =========================
42
+ Type-level mapping: from structure literal -> runtime result types
43
+ ========================= */
44
+
45
+ type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
46
+
47
+ /** PrimitiveLeaf: non-'blocks' field instance */
48
+ type PrimitiveLeafNode<Name extends string, TypeStr extends string> = DBBase & {
49
+ readonly name: Name;
50
+ readonly type: TypeStr;
51
+ readonly children?: []; // leaf has no children array
52
+ readonly fields?: {}; // leaf has empty fields
53
+ };
54
+
55
+ /** Blocks wrapper node (a field with type 'blocks') */
56
+ type BlocksWrapperNode<Name extends string, SubBlocks extends readonly any[]> = DBBase & {
57
+ readonly name: Name;
58
+ readonly type: "blocks";
59
+ // children are the instances of BlockNodeFromDefs for nested block defs
60
+ readonly children: BlockNodeFromDefs<SubBlocks>[];
61
+ readonly fields: {}; // wrapper itself doesn't define extra fields in DSL
62
+ };
63
+
64
+ /** For a FieldDef literal F, produce its runtime node type */
65
+ type FieldNodeFromLiteral<F> =
66
+ F extends { name: infer N extends string; type: infer FT extends string }
67
+ ? FT extends "blocks"
68
+ ? F extends { fields: infer SB extends readonly any[] }
69
+ ? BlocksWrapperNode<N, SB>
70
+ : PrimitiveLeafNode<N, FT>
71
+ : PrimitiveLeafNode<N, FT>
72
+ : never;
73
+
74
+ /** Given an array of FieldDefs, produce a mapping name -> appropriate Node */
75
+ type FieldsFromFieldDefs<TDefs> =
76
+ TDefs extends readonly any[]
77
+ ? {
78
+ [P in ArrayElement<TDefs> as P extends { name: infer N extends string } ? N : never]:
79
+ FieldNodeFromLiteral<P>;
80
+ }
81
+ : {};
82
+
83
+ /** A Block node (for a BlockDef literal B) */
84
+ type BlockNodeFromLiteral<B> =
85
+ B extends { name: infer BN extends string; type: infer BT extends string; fields?: infer BF extends readonly any[] }
86
+ ? DBBase & {
87
+ readonly name: BN;
88
+ readonly type: BT;
89
+ // The block's fields mapping is built from B.fields (may include blocks wrappers)
90
+ readonly fields: FieldsFromFieldDefs<BF>;
91
+ // Block instances themselves do not keep a top-level children array (their nested content is in fields),
92
+ // except for "blocks" wrapper nodes (which are FieldNodes, not BlockNodes).
93
+ readonly children?: []; // mainly unused for normal block instances
94
+ }
95
+ : never;
96
+
97
+ /** Build union type for an array of BlockDefs */
98
+ type BlockNodeFromDefs<TDefs extends readonly any[]> =
99
+ ArrayElement<TDefs> extends infer B ? (B extends any ? BlockNodeFromLiteral<B> : never) : never;
100
+
101
+ /** Final result type returned by getBlockTrees for a given structure */
102
+ export type BlockTreeFromStructure<TStructure extends readonly any[]> =
103
+ BlockNodeFromDefs<TStructure>[];
104
+
105
+
106
+ /* =========================
107
+ RUNTIME: fetch + build + transform
108
+ ========================= */
109
+
110
+ /**
111
+ * Builds adjacency (map of id->node) and attaches children arrays.
112
+ * Then transforms nodes so that:
113
+ * - each node gets .fields where immediate children are mapped by child.name
114
+ * - if node.type === "blocks" -> keep node.children as the array of nested blocks
115
+ * - otherwise -> clear node.children (we represent children via fields on the parent)
116
+ */
117
+ function buildForestRows(rows: (DBBase)[]) {
118
+ const map = new Map<number, any>();
119
+ for (const r of rows) {
120
+ // copy DBBase row into mutable node
121
+ map.set(r.id, {
122
+ ...r,
123
+ children: [],
124
+ fields: {}
125
+ });
126
+ }
127
+
128
+ const roots: any[] = [];
129
+ for (const r of rows) {
130
+ const node = map.get(r.id)!;
131
+ if (r.parent_id === null) {
132
+ roots.push(node);
133
+ } else {
134
+ const parent = map.get(r.parent_id);
135
+ if (parent) parent.children.push(node);
136
+ }
137
+ }
138
+
139
+ // Recursively transform nodes bottom-up
140
+ function transformNode(node: any) {
141
+ // first transform children
142
+ for (const child of node.children) {
143
+ transformNode(child);
144
+ }
145
+
146
+ // build fields map from immediate children keyed by child.name
147
+ const fields: Record<string, any> = {};
148
+ for (const child of node.children) {
149
+ // assign the transformed child as field value
150
+ fields[child.name] = child;
151
+ }
152
+
153
+ node.fields = fields;
154
+
155
+ // Important: keep the children array only for 'blocks' wrapper nodes,
156
+ // because wrappers need to hold their nested block instances under children.
157
+ // For all other nodes we clear children (they are exposed via node.fields)
158
+ if (node.type !== "blocks") {
159
+ node.children = [];
160
+ }
161
+ return node;
162
+ }
163
+
164
+ return roots.map(transformNode);
165
+ }
166
+
167
+ function rootQuery(filterSql: string, depthLimit?: number) {
168
+ const depthLimitClause =
169
+ depthLimit !== undefined ? `WHERE d.depth + 1 <= ${depthLimit}` : ''
170
+
171
+ return sql`
172
+ with recursive
173
+ ancestors as (
174
+ select
175
+ id,
176
+ created_at,
177
+ updated_at,
178
+ name,
179
+ label,
180
+ type,
181
+ sort_order,
182
+ value,
183
+ options,
184
+ status,
185
+ parent_id,
186
+ 0 as depth
187
+ from
188
+ blocks
189
+ where
190
+ ${filterSql}
191
+ union all
192
+ select
193
+ b.id,
194
+ b.created_at,
195
+ b.updated_at,
196
+ b.name,
197
+ b.label,
198
+ b.type,
199
+ b.sort_order,
200
+ b.value,
201
+ b.options,
202
+ b.status,
203
+ b.parent_id,
204
+ a.depth + 1
205
+ from
206
+ blocks b
207
+ inner join ancestors a on b.id = a.parent_id
208
+ ),
209
+ roots as (
210
+ select
211
+ id,
212
+ created_at,
213
+ updated_at,
214
+ name,
215
+ label,
216
+ type,
217
+ sort_order,
218
+ value,
219
+ options,
220
+ status,
221
+ parent_id,
222
+ 0 as depth
223
+ from
224
+ ancestors
225
+ where
226
+ parent_id is null
227
+ ),
228
+ descendants as (
229
+ select
230
+ id,
231
+ created_at,
232
+ updated_at,
233
+ name,
234
+ label,
235
+ type,
236
+ sort_order,
237
+ value,
238
+ options,
239
+ status,
240
+ parent_id,
241
+ depth
242
+ from
243
+ roots
244
+ union all
245
+ select
246
+ b.id,
247
+ b.created_at,
248
+ b.updated_at,
249
+ b.name,
250
+ b.label,
251
+ b.type,
252
+ b.sort_order,
253
+ b.value,
254
+ b.options,
255
+ b.status,
256
+ b.parent_id,
257
+ d.depth + 1
258
+ from
259
+ blocks b
260
+ inner join descendants d on b.parent_id = d.id ${depthLimitClause}
261
+ )
262
+ select
263
+ *
264
+ from
265
+ descendants
266
+ order by
267
+ parent_id,
268
+ sort_order;
269
+ `
270
+ }
271
+
272
+ function buildFilterSql(params: Record<string, any>) {
273
+ const entries = Object.entries(params)
274
+ const filterSql = entries
275
+ .map(([key, value]) =>
276
+ value === null ? `${key} is null` : `${key} = :${key}`,
277
+ )
278
+ .join(' and ')
279
+
280
+ let sqlParams: Record<keyof typeof params, any> = {}
281
+
282
+ for (const param in params) {
283
+ if (params[param] !== null) {
284
+ sqlParams[param] = params[param]
285
+ }
286
+ }
287
+
288
+ return { filterSql, sqlParams }
289
+ }
290
+
291
+ /**
292
+ * Generic typed getBlockTrees:
293
+ * - TStructure: the structure literal you pass
294
+ * - returns: Array of typed block nodes derived from that structure
295
+ *
296
+ * NOTE: the runtime SQL must return rows for the correct tree (use your
297
+ * previous recursive CTE with explicit columns + depth reset for roots).
298
+ */
299
+ export function getBlockTrees<
300
+ TStructure extends readonly any[]
301
+ >(
302
+ db: DatabaseSync,
303
+ _structure: TStructure, // used for typing; at runtime it's not strictly needed
304
+ // sql: string, // the recursive SQL with explicit columns (see earlier messages)
305
+ params: Record<string, any> = {}
306
+ ): BlockTreeFromStructure<TStructure> {
307
+ const { filterSql, sqlParams } = buildFilterSql(params)
308
+ // run SQL (you must provide the SQL string matching your earlier working CTE)
309
+ const rows = db.prepare(rootQuery(filterSql)).all(sqlParams) as DBBase[]; // depth included in DBBase.depth
310
+
311
+ // build + transform
312
+ const forest = buildForestRows(rows);
313
+
314
+ // cast to compile-time derived type (we ensure runtime shape matches the type)
315
+ return forest as unknown as BlockTreeFromStructure<TStructure>;
316
+ }