@alstar/studio 0.0.0-beta.4 → 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 (43) hide show
  1. package/api/api-key.ts +74 -0
  2. package/api/block.ts +39 -21
  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 +17 -10
  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 +97 -0
  15. package/components/index.ts +1 -1
  16. package/components/layout.ts +8 -5
  17. package/index.ts +48 -20
  18. package/package.json +2 -1
  19. package/public/admin-panel.css +90 -0
  20. package/public/blocks.css +53 -0
  21. package/public/main.css +11 -1
  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 +287 -0
  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 +2 -98
  31. package/queries/structure-types.ts +97 -0
  32. package/schemas.ts +15 -54
  33. package/types.ts +95 -5
  34. package/utils/buildBlocksTree.ts +4 -4
  35. package/utils/define.ts +21 -5
  36. package/utils/file-based-router.ts +9 -1
  37. package/utils/get-config.ts +8 -9
  38. package/utils/get-or-create-row.ts +28 -0
  39. package/utils/startup-log.ts +9 -0
  40. package/components/AdminPanel/AdminPanel.css +0 -59
  41. package/components/Field.ts +0 -164
  42. package/components/Fields.ts +0 -43
  43. /package/{components/Entry.css → public/entry.css} +0 -0
@@ -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
+ }
@@ -0,0 +1,214 @@
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
+ }
package/queries/index.ts CHANGED
@@ -1,98 +1,2 @@
1
- import { db } from '@alstar/db'
2
- import { sql } from '../utils/sql.ts'
3
- import { type DBBlock } from '../types.ts'
4
- import { buildBlockTree } from '../utils/buildBlocksTree.ts'
5
-
6
- export const blocks = (options: {
7
- parent: string | number | null
8
- }): DBBlock[] | null => {
9
- const q =
10
- options.parent === null
11
- ? sql`
12
- select
13
- *
14
- from
15
- blocks
16
- where
17
- parent_block_id is null;
18
- `
19
- : sql`
20
- select
21
- *
22
- from
23
- blocks
24
- where
25
- parent_block_id = ?;
26
- `
27
-
28
- const transaction = db.database.prepare(q)
29
-
30
- if (options.parent === null) {
31
- return transaction.all() as unknown as DBBlock[]
32
- } else {
33
- return transaction.all(options.parent) as unknown as DBBlock[]
34
- }
35
- }
36
-
37
- export const block = (
38
- query: Record<string, string | null>,
39
- options?: { recursive: boolean },
40
- ): DBBlock | null => {
41
- const str = Object.keys(query)
42
- .map((key) => `${key.replace('parent', 'parent_block_id')} = ?`)
43
- .join(' AND ')
44
-
45
- const q = options?.recursive
46
- ? sql`
47
- with recursive
48
- block_hierarchy as (
49
- -- Anchor member: the root block, depth = 0
50
- select
51
- b.*,
52
- 0 as depth
53
- from
54
- blocks b
55
- where
56
- b.id = ? -- Replace 5 with your root block ID
57
- union all
58
- -- Recursive member: find children and increment depth
59
- select
60
- b.*,
61
- bh.depth + 1 as depth
62
- from
63
- blocks b
64
- inner join block_hierarchy bh on b.parent_block_id = bh.id
65
- )
66
- select
67
- *
68
- from
69
- block_hierarchy
70
- order by
71
- sort_order;
72
- `
73
- : sql`
74
- select
75
- *
76
- from
77
- blocks
78
- where
79
- ${str};
80
- `
81
-
82
- const transaction = db.database.prepare(q)
83
-
84
- try {
85
- if (options?.recursive) {
86
- const res = transaction.all(query.id) as unknown as any[]
87
- return (buildBlockTree(res) as any) || null
88
- }
89
- return (
90
- (transaction.get(...Object.values(query)) as unknown as DBBlock) || null
91
- )
92
- } catch (error) {
93
- console.log('error')
94
- return null
95
- }
96
- }
97
-
98
- export const query = { blocks, block }
1
+ export * as query from './block.ts'
2
+ export { getBlockTrees } from './getBlockTrees.ts'