@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,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 +1,2 @@
1
1
  export * as query from './block.ts'
2
+ export { getBlockTrees } from './getBlockTrees.ts'
@@ -0,0 +1,97 @@
1
+ // structure-types.ts
2
+ import { type DBBase } from "./db-types.ts";
3
+
4
+ /**
5
+ * Extract helpers
6
+ */
7
+ type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
8
+
9
+ /**
10
+ * Field definition shape inferred from defineField(...) (the runtime helper)
11
+ * We keep it generic as "any" shape but with the important properties present
12
+ */
13
+ type FieldDef = {
14
+ readonly name: string;
15
+ readonly type: string;
16
+ readonly fields?: readonly any[]; // only present when type === 'blocks'
17
+ };
18
+
19
+ /**
20
+ * Block definition shape inferred from defineBlock(...) (the runtime helper)
21
+ */
22
+ type BlockDef = {
23
+ readonly name: string;
24
+ readonly type: string;
25
+ readonly fields?: readonly FieldDef[];
26
+ };
27
+
28
+ /**
29
+ * Primitive field node (non-'blocks'): DBBase + kept fields but no children
30
+ */
31
+ type PrimitiveFieldNode<TField extends FieldDef> =
32
+ DBBase & {
33
+ readonly name: TField["name"];
34
+ readonly type: TField["type"];
35
+ // no children (leaf), no nested fields
36
+ readonly children?: [];
37
+ readonly fields?: {}; // empty object for leaf
38
+ };
39
+
40
+ /**
41
+ * For 'blocks' typed field, we need:
42
+ * - the block node representing the 'blocks' wrapper (has DBBase props)
43
+ * - its 'children' are an array of BlockNodes corresponding to nested block defs supplied in the field's 'fields' array
44
+ * - its 'fields' property is the mapping of its own child-field names (can be empty)
45
+ */
46
+ type BlocksFieldNode<
47
+ TField extends FieldDef,
48
+ TFieldDefs extends readonly BlockDef[]
49
+ > = DBBase & {
50
+ readonly name: TField["name"]; // e.g. "blocks" or "images"
51
+ readonly type: "blocks"; // literally 'blocks'
52
+ readonly children: BlockNodeFromBlockDefs<TFieldDefs>[]; // children are instances of the nested blocks
53
+ readonly fields: FieldsFromFieldDefs<TFieldDefs[number]["fields"]>; // the blocks-wrapper's own fields mapping (if any)
54
+ };
55
+
56
+ /**
57
+ * Build the 'fields' object for a set of FieldDef[].
58
+ * Maps each field name -> either PrimitiveFieldNode or BlocksFieldNode recursively.
59
+ */
60
+ type FieldsFromFieldDefs<TDefs> =
61
+ // If no fields
62
+ TDefs extends readonly any[]
63
+ ? {
64
+ // For each field F in TDefs, map F['name'] -> node type
65
+ [F in ArrayElement<TDefs> as F extends { name: infer N extends string } ? N : never]:
66
+ F extends { type: "blocks"; fields: readonly BlockDef[] }
67
+ ? BlocksFieldNode<F, F["fields"]>
68
+ : PrimitiveFieldNode<F>;
69
+ }
70
+ : {};
71
+
72
+ /**
73
+ * A Block node type for a particular BlockDef.
74
+ * - fields: mapping derived from the block's declared fields
75
+ * - children: by default [], because in our final shape all immediate children are placed under 'fields' of the parent.
76
+ * BUT for nodes that are themselves 'blocks' wrappers (i.e. appear as a Block instance of a nested block def),
77
+ * their 'children' will contain actual child blocks (these are handled via BlocksFieldNode above).
78
+ */
79
+ export type BlockNode<T extends BlockDef> = DBBase & {
80
+ readonly name: T["name"];
81
+ readonly type: T["type"];
82
+ readonly fields: FieldsFromFieldDefs<T["fields"]>;
83
+ // for regular block nodes, children will usually be [] (top-level parent's children moved into fields)
84
+ readonly children: [];
85
+ };
86
+
87
+ /**
88
+ * Construct BlockNode unions for a set of block defs (used when blocks field has multiple block subdefs)
89
+ */
90
+ type BlockNodeFromBlockDefs<TDefs extends readonly BlockDef[]> =
91
+ ArrayElement<TDefs> extends infer B ? (B extends BlockDef ? BlockNode<B> : never) : never;
92
+
93
+ /**
94
+ * The top-level forest return type when you pass a structure: it's an array of BlockNode of any top-level BlockDef
95
+ */
96
+ export type BlockTreeFromStructure<TStructure extends readonly BlockDef[]> =
97
+ BlockNodeFromBlockDefs<TStructure>;
package/schemas.ts CHANGED
@@ -7,11 +7,22 @@ export const blocksTable = {
7
7
  name TEXT not null,
8
8
  label TEXT not null,
9
9
  type TEXT not null,
10
- sort_order INTEGER not null default 0,
11
10
  value TEXT,
12
11
  options JSON,
13
12
  status TEXT default 'enabled',
14
- parent_block_id INTEGER,
15
- foreign key (parent_block_id) references blocks (id)
13
+ sort_order INTEGER not null default 0,
14
+ _depth INTEGER,
15
+ parent_id INTEGER,
16
+ foreign key (parent_id) references blocks (id)
17
+ `,
18
+ }
19
+
20
+ // -- API keys
21
+ export const apiKeysTable = {
22
+ tableName: 'api_keys',
23
+ columns: sql`
24
+ name TEXT not null,
25
+ value TEXT,
26
+ hint TEXT
16
27
  `,
17
28
  }
package/types.ts CHANGED
@@ -3,14 +3,92 @@ import { type Context } from 'hono'
3
3
  import { type HonoOptions } from 'hono/hono-base'
4
4
  import { type BlankInput, type BlankEnv } from 'hono/types'
5
5
 
6
+ export type PrimitiveField = {
7
+ name: string
8
+ label: string
9
+ type: 'text' | 'slug' | 'markdown' | 'image'
10
+ }
11
+
12
+ export type BlockField = {
13
+ name: string
14
+ label: string
15
+ type: 'blocks'
16
+ children: Record<string, Field | Block>
17
+ }
18
+
19
+ export type Field = PrimitiveField | BlockField
20
+
6
21
  export type Block = {
7
22
  name: string
8
23
  label: string
9
24
  type: string
10
- fields?: Block[]
25
+ fields: Record<string, Field | Block>
11
26
  }
12
27
 
13
- export type Structure = Block[]
28
+ export type Structure = Record<string, BlockDef>
29
+
30
+ // --- Field & block definitions ---
31
+ type FieldType = 'text' | 'slug' | 'markdown' | 'blocks' | 'image';
32
+
33
+ interface BaseField {
34
+ label: string;
35
+ type: FieldType;
36
+ description?: string
37
+ }
38
+
39
+ interface TextField extends BaseField {
40
+ type: 'text' | 'slug' | 'markdown';
41
+ }
42
+
43
+ interface ImageField extends BaseField {
44
+ type: 'image';
45
+ }
46
+
47
+ export interface BlocksField extends BaseField {
48
+ type: 'blocks';
49
+ children: Record<string, BlockDef | FieldDef>;
50
+ }
51
+
52
+ export type FieldDef = TextField | ImageField | BlocksField;
53
+
54
+ export interface BlockDef {
55
+ label: string;
56
+ type: string;
57
+ fields: Record<string, FieldDef>;
58
+ description?: string
59
+ }
60
+
61
+ type DBDefaults = {
62
+ id: number
63
+ created_at: string
64
+ updated_at: string
65
+ name: string
66
+ label: string
67
+ // type: string
68
+ sort_order: number
69
+ value: string
70
+ options: string | null
71
+ status: 'enabled' | 'disabled'
72
+ parent_id: number | null
73
+ depth: number
74
+ }
75
+
76
+ export type DBBlockResult = {
77
+ id: number
78
+ created_at: string
79
+ updated_at: string
80
+ name: string
81
+ label: string
82
+ type: string
83
+ sort_order: number
84
+ value: string | null
85
+ options: any
86
+ status: string
87
+ parent_id: number | null
88
+ depth: number
89
+ children?: DBBlockResult[]
90
+ fields?: Record<string, DBBlockResult>
91
+ }
14
92
 
15
93
  export type DBBlock = Block & {
16
94
  id: number
@@ -18,17 +96,18 @@ export type DBBlock = Block & {
18
96
  updated_at: string
19
97
  value: string | null
20
98
  sort_order: number | null
21
- parent_block_id: number | null
99
+ parent_id: number | null
22
100
  options: number | null
23
101
  }
24
102
 
103
+ export type BlockStatus = 'enabled' | 'disabled'
104
+
25
105
  export type StudioConfig = {
26
106
  siteName: string
27
107
  honoConfig?: HonoOptions<BlankEnv>
108
+ structure: Structure
28
109
  }
29
110
 
30
- export type BlockStatus = 'enabled' | 'disabled'
31
-
32
111
  export type RequestContext = Context<
33
112
  { Bindings: HttpBindings },
34
113
  string,
@@ -6,7 +6,7 @@ type Block = {
6
6
  sort_order: number
7
7
  value: string | null
8
8
  options: any // JSON-parsed if necessary
9
- parent_block_id: number | null
9
+ parent_id: number | null
10
10
  depth: number
11
11
  // ... you can add other fields if needed
12
12
  }
@@ -26,13 +26,13 @@ export function buildBlockTree(blocks: Block[]): BlockWithChildren {
26
26
  for (const block of blocks) {
27
27
  const current = blockMap.get(block.id)!
28
28
 
29
- if (block.parent_block_id != null) {
30
- const parent = blockMap.get(block.parent_block_id)
29
+ if (block.parent_id != null) {
30
+ const parent = blockMap.get(block.parent_id)
31
31
  if (parent) {
32
32
  parent.fields.push(current)
33
33
  } else {
34
34
  console.warn(
35
- `Parent with id ${block.parent_block_id} not found for block ${block.id}`,
35
+ `Parent with id ${block.parent_id} not found for block ${block.id}`,
36
36
  )
37
37
  }
38
38
  } else {
package/utils/define.ts CHANGED
@@ -1,15 +1,29 @@
1
- import { type Context } from 'hono'
2
1
  import * as types from '../types.ts'
3
- import { type HttpBindings } from '@hono/node-server'
4
- import { type BlankInput } from 'hono/types'
5
2
  import { type HtmlEscapedString } from './html.ts'
6
3
 
7
4
  export const defineConfig = (config: types.StudioConfig) => config
8
5
 
9
- export const defineStructure = (structure: types.Structure) => structure
6
+ // export const defineStructure = (structure: types.Block[]) => structure
7
+ // export const defineField = (field: types.Field) => field
8
+ // export const defineBlock = (block: types.Block) => block
10
9
 
11
10
  export const defineEntry = (
12
11
  fn: (
13
12
  c: types.RequestContext,
14
13
  ) => HtmlEscapedString | Promise<HtmlEscapedString>,
15
14
  ) => fn
15
+
16
+ // --- Identity helpers (preserve literal types) ---
17
+ export function defineField(field: types.FieldDef) {
18
+ return field
19
+ }
20
+
21
+ export function defineBlock(block: types.BlockDef) {
22
+ return block
23
+ }
24
+
25
+ export function defineStructure(
26
+ structure: Record<string, types.BlockDef>,
27
+ ) {
28
+ return structure
29
+ }
@@ -0,0 +1,28 @@
1
+ import { db } from '@alstar/db'
2
+ import { query } from '../queries/index.ts'
3
+ import type { Block, FieldDef } from '../types.ts'
4
+
5
+ export function getOrCreateRow(
6
+ parentId: string | number,
7
+ name: string,
8
+ field: Block | FieldDef,
9
+ sortOrder: number,
10
+ ) {
11
+ const data = query.block({
12
+ parent_id: parentId?.toString() || null,
13
+ name: name,
14
+ sort_order: sortOrder.toString(),
15
+ })
16
+
17
+ if (data) return data
18
+
19
+ const change = db.insertInto('blocks', {
20
+ name: name?.toString(),
21
+ label: field.label?.toString(),
22
+ type: field.type?.toString(),
23
+ sort_order: sortOrder,
24
+ parent_id: parentId,
25
+ })
26
+
27
+ return query.block({ id: change.lastInsertRowid.toString() })
28
+ }
@@ -0,0 +1,9 @@
1
+ import packageJSON from '../package.json' with { type: 'json' }
2
+
3
+ export default () => {
4
+ console.log('\x1b[32m%s\x1b[0m', '╭───────────────────────╮')
5
+ console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
6
+ console.log('\x1b[32m%s\x1b[0m', `│ ${packageJSON.version}${' '.repeat(22 - packageJSON.version.length)}│`)
7
+ console.log('\x1b[32m%s\x1b[0m', `│ http://localhost:${3000} │`)
8
+ console.log('\x1b[32m%s\x1b[0m', '╰───────────────────────╯')
9
+ }
@@ -1,78 +0,0 @@
1
- .admin-panel {
2
- /* background: hsla(0, 0%, 0%, 0.1); */
3
- padding: 40px;
4
-
5
- height: 100%;
6
- min-height: inherit;
7
-
8
- min-width: 250px;
9
-
10
- > h1 {
11
- padding-bottom: 1rem;
12
-
13
- a {
14
- display: flex;
15
- }
16
-
17
- svg {
18
- height: 1.6rem;
19
- }
20
- }
21
-
22
- form {
23
- padding-bottom: 1rem;
24
-
25
- button {
26
- margin: 10px 0px 20px;
27
- }
28
- }
29
-
30
- #entries ul {
31
- padding: 0;
32
- margin-inline: -1rem;
33
-
34
- form {
35
- display: flex;
36
- padding-bottom: 0;
37
-
38
- button {
39
- margin: 0;
40
- }
41
- }
42
-
43
- > li {
44
- margin-bottom: 0px;
45
- border-radius: 8px;
46
- display: flex;
47
- justify-content: space-between;
48
- align-items: stretch;
49
- list-style: none;
50
-
51
- a {
52
- text-decoration: none;
53
- width: 100%;
54
- padding: 0.5rem 1rem;
55
- }
56
-
57
- button {
58
- border-radius: 7px;
59
- opacity: 0;
60
- transition: opacity 100px;
61
-
62
- svg {
63
- margin: 0.5rem 1rem;
64
- }
65
- }
66
- }
67
- }
68
- }
69
-
70
- #entries ul {
71
- > li:hover {
72
- background-color: var(--pico-form-element-background-color);
73
-
74
- button {
75
- opacity: 1;
76
- }
77
- }
78
- }