@alstar/studio 0.0.0-beta.5 → 0.0.0-beta.6

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 (41) hide show
  1. package/api/api-key.ts +74 -0
  2. package/api/block.ts +21 -29
  3. package/api/index.ts +9 -1
  4. package/api/mcp.ts +53 -0
  5. package/bin/alstar.ts +42 -0
  6. package/components/{AdminPanel/AdminPanel.ts → AdminPanel.ts} +22 -27
  7. package/components/Block.ts +51 -112
  8. package/components/Entries.ts +3 -3
  9. package/components/Entry.ts +9 -15
  10. package/components/Settings.ts +98 -0
  11. package/components/fields/Blocks.ts +118 -0
  12. package/components/fields/Text.ts +42 -0
  13. package/components/fields/index.ts +4 -0
  14. package/components/icons.ts +59 -0
  15. package/components/index.ts +1 -1
  16. package/components/layout.ts +2 -2
  17. package/index.ts +33 -14
  18. package/package.json +4 -3
  19. package/public/admin-panel.css +90 -0
  20. package/public/blocks.css +53 -0
  21. package/public/main.css +8 -0
  22. package/public/main.js +4 -0
  23. package/public/settings.css +24 -0
  24. package/queries/block-with-children.ts +74 -0
  25. package/queries/block.ts +29 -40
  26. package/queries/db-types.ts +15 -0
  27. package/queries/getBlockTrees-2.ts +0 -0
  28. package/queries/getBlockTrees.ts +316 -0
  29. package/queries/getBlocks.ts +214 -0
  30. package/queries/index.ts +1 -0
  31. package/queries/structure-types.ts +97 -0
  32. package/schemas.ts +14 -3
  33. package/types.ts +84 -5
  34. package/utils/buildBlocksTree.ts +4 -4
  35. package/utils/define.ts +18 -4
  36. package/utils/get-or-create-row.ts +28 -0
  37. package/utils/startup-log.ts +9 -0
  38. package/components/AdminPanel/AdminPanel.css +0 -78
  39. package/components/Field.ts +0 -168
  40. package/components/Fields.ts +0 -43
  41. /package/{components/Entry.css → public/entry.css} +0 -0
@@ -0,0 +1,74 @@
1
+ import { sql } from '../utils/sql.ts'
2
+
3
+ export const blockWithChildren = sql`
4
+ with recursive
5
+ block_tree as (
6
+ -- Start from the root block you want
7
+ select
8
+ id,
9
+ name,
10
+ label,
11
+ type,
12
+ sort_order,
13
+ value,
14
+ options,
15
+ status,
16
+ parent_id,
17
+ 0 as depth
18
+ from
19
+ blocks
20
+ where
21
+ id = ? -- <-- put your starting block id here
22
+ union all
23
+ -- Recursively select children
24
+ select
25
+ b.id,
26
+ b.name,
27
+ b.label,
28
+ b.type,
29
+ b.sort_order,
30
+ b.value,
31
+ b.options,
32
+ b.status,
33
+ b.parent_id,
34
+ bt.depth + 1
35
+ from
36
+ blocks b
37
+ inner join block_tree bt on b.parent_id = bt.id
38
+ )
39
+ select
40
+ *
41
+ from
42
+ block_tree
43
+ order by
44
+ depth,
45
+ sort_order;
46
+ `
47
+
48
+ export const deleteBlockWithChildren = sql`
49
+ with recursive
50
+ block_tree as (
51
+ -- start from the root block you want to delete
52
+ select
53
+ id
54
+ from
55
+ blocks
56
+ where
57
+ id = ? -- <-- put your root block id here
58
+ union all
59
+ -- recursively select children
60
+ select
61
+ b.id
62
+ from
63
+ blocks b
64
+ inner join block_tree bt on b.parent_id = bt.id
65
+ )
66
+ delete from blocks
67
+ where
68
+ id in (
69
+ select
70
+ id
71
+ from
72
+ block_tree
73
+ );
74
+ `
package/queries/block.ts CHANGED
@@ -1,22 +1,6 @@
1
1
  import { db } from '@alstar/db'
2
2
  import { sql } from '../utils/sql.ts'
3
-
4
- type DBBlockResult = {
5
- id: number
6
- created_at: string
7
- updated_at: string
8
- name: string
9
- label: string
10
- type: string
11
- sort_order: number
12
- value: string | null
13
- options: any
14
- status: string
15
- parent_block_id: number | null
16
- depth: number
17
- children: DBBlockResult[]
18
- fields: Record<string, DBBlockResult>
19
- }
3
+ import { type DBBlockResult } from '../types.ts'
20
4
 
21
5
  function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
22
6
  const map = new Map<number, DBBlockResult>()
@@ -28,10 +12,10 @@ function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
28
12
  }
29
13
 
30
14
  for (const block of blocks) {
31
- if (block.parent_block_id === null) {
15
+ if (block.parent_id === null) {
32
16
  roots.push(block)
33
17
  } else {
34
- const parent = map.get(block.parent_block_id)
18
+ const parent = map.get(block.parent_id)
35
19
  if (parent) parent.children!.push(block)
36
20
  }
37
21
  }
@@ -56,10 +40,10 @@ function buildTree(blocks: DBBlockResult[]): DBBlockResult {
56
40
  }
57
41
 
58
42
  for (const block of blocks) {
59
- if (block.parent_block_id === null) {
43
+ if (block.parent_id === null) {
60
44
  roots.push(block)
61
45
  } else {
62
- const parent = map.get(block.parent_block_id)
46
+ const parent = map.get(block.parent_id)
63
47
  if (parent) parent.children!.push(block)
64
48
  }
65
49
  }
@@ -74,35 +58,40 @@ function buildTree(blocks: DBBlockResult[]): DBBlockResult {
74
58
  return roots[0]
75
59
  }
76
60
 
77
- function transformDBBlockResultTree(
61
+ function transformBlocksTree(
78
62
  block: DBBlockResult,
79
63
  isBlocksChild?: boolean,
80
64
  ): DBBlockResult {
81
65
  const fields: Record<string, DBBlockResult> = {}
66
+ let hasFields = false
82
67
 
83
68
  for (const child of block.children ?? []) {
84
- const transformedChild = transformDBBlockResultTree(
69
+ const transformedChild = transformBlocksTree(
85
70
  child,
86
71
  child.type === 'blocks',
87
72
  )
88
73
 
89
- if (isBlocksChild) {
90
- } else {
74
+ if (!isBlocksChild) {
75
+ hasFields = true
91
76
  fields[transformedChild.name] = transformedChild
92
77
  }
93
78
  }
94
79
 
95
- block.fields = fields
96
-
80
+ if(hasFields) {
81
+ block.fields = fields
82
+ }
83
+
97
84
  if (!isBlocksChild) {
98
- block.children = [] // clear children array, since all children moved into fields
85
+ delete block.children
86
+ } else {
87
+ delete block.fields
99
88
  }
100
89
 
101
90
  return block
102
91
  }
103
92
 
104
93
  function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
105
- return blocks.map((block) => transformDBBlockResultTree(block))
94
+ return blocks.map((block) => transformBlocksTree(block))
106
95
  }
107
96
 
108
97
  function rootQuery(filterSql: string, depthLimit?: number) {
@@ -123,7 +112,7 @@ function rootQuery(filterSql: string, depthLimit?: number) {
123
112
  value,
124
113
  options,
125
114
  status,
126
- parent_block_id,
115
+ parent_id,
127
116
  0 as depth
128
117
  from
129
118
  blocks
@@ -141,11 +130,11 @@ function rootQuery(filterSql: string, depthLimit?: number) {
141
130
  b.value,
142
131
  b.options,
143
132
  b.status,
144
- b.parent_block_id,
133
+ b.parent_id,
145
134
  a.depth + 1
146
135
  from
147
136
  blocks b
148
- inner join ancestors a on b.id = a.parent_block_id
137
+ inner join ancestors a on b.id = a.parent_id
149
138
  ),
150
139
  roots as (
151
140
  select
@@ -159,12 +148,12 @@ function rootQuery(filterSql: string, depthLimit?: number) {
159
148
  value,
160
149
  options,
161
150
  status,
162
- parent_block_id,
151
+ parent_id,
163
152
  0 as depth
164
153
  from
165
154
  ancestors
166
155
  where
167
- parent_block_id is null
156
+ parent_id is null
168
157
  ),
169
158
  descendants as (
170
159
  select
@@ -178,7 +167,7 @@ function rootQuery(filterSql: string, depthLimit?: number) {
178
167
  value,
179
168
  options,
180
169
  status,
181
- parent_block_id,
170
+ parent_id,
182
171
  depth
183
172
  from
184
173
  roots
@@ -194,19 +183,19 @@ function rootQuery(filterSql: string, depthLimit?: number) {
194
183
  b.value,
195
184
  b.options,
196
185
  b.status,
197
- b.parent_block_id,
186
+ b.parent_id,
198
187
  d.depth + 1
199
188
  from
200
189
  blocks b
201
- inner join descendants d on b.parent_block_id = d.id ${depthLimitClause}
190
+ inner join descendants d on b.parent_id = d.id ${depthLimitClause}
202
191
  )
203
192
  select
204
193
  *
205
194
  from
206
195
  descendants
207
196
  order by
208
- parent_block_id,
209
- sort_order;
197
+ sort_order,
198
+ id;
210
199
  `
211
200
  }
212
201
 
@@ -264,7 +253,7 @@ export function root(
264
253
 
265
254
  const tree = buildTree(rows)
266
255
 
267
- return transformDBBlockResultTree(tree)
256
+ return transformBlocksTree(tree)
268
257
  }
269
258
 
270
259
  export function block(params: Record<string, any>) {
@@ -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
+ }