@alstar/studio 0.0.0-beta.10 → 0.0.0-beta.12
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/api-key.ts +37 -49
- package/api/auth.ts +64 -0
- package/api/backup.ts +39 -28
- package/api/block.ts +80 -119
- package/api/index.ts +17 -10
- package/api/mcp.ts +39 -42
- package/components/AdminPanel.ts +29 -4
- package/components/BlockFieldRenderer.ts +20 -4
- package/components/Entries.ts +7 -3
- package/components/FieldRenderer.ts +1 -1
- package/components/Settings.ts +5 -93
- package/components/SiteLayout.ts +11 -9
- package/components/fields/Markdown.ts +1 -1
- package/components/fields/Slug.ts +122 -0
- package/components/fields/Text.ts +20 -4
- package/components/fields/index.ts +4 -1
- package/components/icons.ts +55 -0
- package/components/settings/ApiKeys.ts +122 -0
- package/components/settings/Backup.ts +82 -0
- package/components/settings/Users.ts +63 -0
- package/index.ts +51 -11
- package/package.json +6 -3
- package/pages/entry/[id].ts +1 -3
- package/pages/error.ts +14 -0
- package/pages/index.ts +1 -1
- package/pages/login.ts +21 -0
- package/pages/register.ts +33 -0
- package/pages/settings.ts +1 -3
- package/public/studio/css/settings.css +7 -15
- package/public/studio/js/markdown-editor.js +1 -1
- package/public/studio/js/sortable-list.js +1 -1
- package/public/studio/main.css +5 -1
- package/public/studio/main.js +13 -0
- package/queries/block-2.ts +339 -0
- package/queries/getBlockTrees-2.ts +71 -0
- package/queries/index.ts +1 -1
- package/readme.md +2 -2
- package/schema.sql +18 -0
- package/schemas.ts +11 -1
- package/types.ts +11 -0
- package/utils/auth.ts +54 -0
- package/utils/create-hash.ts +9 -0
- package/utils/define.ts +1 -3
- package/utils/renderSSE.ts +27 -0
- package/utils/slugify.ts +3 -1
- package/utils/startup-log.ts +15 -6
- package/components/Backup.ts +0 -10
- package/utils/build-structure-path.ts +0 -43
|
@@ -0,0 +1,339 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * as query from './block.ts'
|
|
1
|
+
export * as query from './block-2.ts'
|
|
2
2
|
export { getBlockTrees } from './getBlockTrees.ts'
|
package/readme.md
CHANGED
|
@@ -45,7 +45,7 @@ Pages are defined in the `/pages` directory.
|
|
|
45
45
|
Access the CMS at:
|
|
46
46
|
|
|
47
47
|
```
|
|
48
|
-
/
|
|
48
|
+
/studio
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
### Defining Content Structure
|
|
@@ -199,7 +199,7 @@ pnpm run dev
|
|
|
199
199
|
|
|
200
200
|
Visit:
|
|
201
201
|
|
|
202
|
-
* **CMS admin**: `http://localhost:3000/
|
|
202
|
+
* **CMS admin**: `http://localhost:3000/studio`
|
|
203
203
|
* **Frontend page**: `http://localhost:3000/my-first-page`
|
|
204
204
|
|
|
205
205
|
Create a new page in the CMS, set its slug field to `my-first-page`, and the frontend will render it automatically.
|
package/schema.sql
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CREATE TABLE blocks (
|
|
2
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
|
+
created_at DATE DEFAULT (datetime('now')),
|
|
4
|
+
updated_at DATE DEFAULT (datetime('now')),
|
|
5
|
+
|
|
6
|
+
name TEXT not null,
|
|
7
|
+
label TEXT not null,
|
|
8
|
+
type TEXT not null,
|
|
9
|
+
sort_order INTEGER not null default 0,
|
|
10
|
+
value TEXT,
|
|
11
|
+
options JSON,
|
|
12
|
+
status TEXT default 'enabled',
|
|
13
|
+
parent_id INTEGER,
|
|
14
|
+
_depth INTEGER,
|
|
15
|
+
foreign key (parent_id) references blocks (id)
|
|
16
|
+
|
|
17
|
+
);
|
|
18
|
+
CREATE TABLE sqlite_sequence(name,seq);
|
package/schemas.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { sql } from './utils/sql.ts'
|
|
2
2
|
|
|
3
|
+
// -- API keys
|
|
4
|
+
export const usersTable = {
|
|
5
|
+
tableName: 'users',
|
|
6
|
+
columns: sql`
|
|
7
|
+
email TEXT not null,
|
|
8
|
+
hash TEXT
|
|
9
|
+
`,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
3
13
|
// -- Blocks
|
|
4
14
|
export const blocksTable = {
|
|
5
15
|
tableName: 'blocks',
|
|
@@ -11,7 +21,7 @@ export const blocksTable = {
|
|
|
11
21
|
options JSON,
|
|
12
22
|
status TEXT default 'enabled',
|
|
13
23
|
sort_order INTEGER not null default 0,
|
|
14
|
-
_depth INTEGER,
|
|
24
|
+
-- _depth INTEGER,
|
|
15
25
|
parent_id INTEGER,
|
|
16
26
|
foreign key (parent_id) references blocks (id)
|
|
17
27
|
`,
|
package/types.ts
CHANGED
|
@@ -8,6 +8,16 @@ import {
|
|
|
8
8
|
FieldInstance,
|
|
9
9
|
} from './utils/define.ts'
|
|
10
10
|
|
|
11
|
+
// DeepReadonly utility type
|
|
12
|
+
export type DeepReadonly<T> =
|
|
13
|
+
T extends (...args: any[]) => any // functions stay as-is
|
|
14
|
+
? T
|
|
15
|
+
: T extends any[] // arrays/tuples
|
|
16
|
+
? { [K in keyof T]: DeepReadonly<T[K]> }
|
|
17
|
+
: T extends object // objects
|
|
18
|
+
? { [K in keyof T]: DeepReadonly<T[K]> }
|
|
19
|
+
: T; // primitives
|
|
20
|
+
|
|
11
21
|
export type PrimitiveField = {
|
|
12
22
|
name: string
|
|
13
23
|
label: string
|
|
@@ -31,6 +41,7 @@ export type Block = {
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export type Structure = Record<string, BlockDefStructure>
|
|
44
|
+
// export type Structure = Record<string, BlockDefStructure>
|
|
34
45
|
|
|
35
46
|
// --- Field & block definitions ---
|
|
36
47
|
type FieldType = 'text' | 'slug' | 'markdown' | 'image'
|
package/utils/auth.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
2
|
+
import { type MiddlewareHandler } from 'hono'
|
|
3
|
+
import { getCookie, setCookie } from 'hono/cookie'
|
|
4
|
+
import { HTTPException } from 'hono/http-exception'
|
|
5
|
+
import { sql } from './sql.ts'
|
|
6
|
+
|
|
7
|
+
const middleware: MiddlewareHandler = async (c, next) => {
|
|
8
|
+
const url = new URL(c.req.url)
|
|
9
|
+
const user = db.database
|
|
10
|
+
.prepare(sql`
|
|
11
|
+
select
|
|
12
|
+
email
|
|
13
|
+
from
|
|
14
|
+
users
|
|
15
|
+
`)
|
|
16
|
+
.get()
|
|
17
|
+
|
|
18
|
+
const cookie = getCookie(c, 'login')
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
!user &&
|
|
22
|
+
url.pathname !== '/studio/register' &&
|
|
23
|
+
url.pathname !== '/studio/api/auth/register'
|
|
24
|
+
) {
|
|
25
|
+
return c.redirect('/studio/register')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
user &&
|
|
30
|
+
!cookie &&
|
|
31
|
+
url.pathname !== '/studio/login' &&
|
|
32
|
+
url.pathname !== '/studio/api/auth/login'
|
|
33
|
+
) {
|
|
34
|
+
return c.redirect('/studio/login')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// console.log(cookie)
|
|
38
|
+
|
|
39
|
+
// deleteCookie(c, 'cookie_name')
|
|
40
|
+
|
|
41
|
+
// const allCookies = getCookie(c)
|
|
42
|
+
|
|
43
|
+
await next()
|
|
44
|
+
|
|
45
|
+
// const authorized = false
|
|
46
|
+
|
|
47
|
+
// if(!authorized) {
|
|
48
|
+
// throw new HTTPException(401, { message: 'Custom error message' })
|
|
49
|
+
// } else {
|
|
50
|
+
// await next()
|
|
51
|
+
// }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default middleware
|
package/utils/define.ts
CHANGED
|
@@ -34,8 +34,6 @@ export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
|
|
|
34
34
|
return { ...block, instanceOf: BlockInstance }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function defineStructure(
|
|
38
|
-
structure: Record<string, types.BlockDefStructure>,
|
|
39
|
-
) {
|
|
37
|
+
export function defineStructure(structure: types.Structure) {
|
|
40
38
|
return structure
|
|
41
39
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { stripNewlines } from './strip-newlines.ts'
|
|
3
|
+
import { type SSEStreamingApi } from 'hono/streaming'
|
|
4
|
+
import { type Context } from 'hono'
|
|
5
|
+
|
|
6
|
+
export const renderSSE = async (stream: SSEStreamingApi, c: Context) => {
|
|
7
|
+
const componentPath = c.req.header('render')
|
|
8
|
+
const props = c.req.header('props')
|
|
9
|
+
|
|
10
|
+
if (componentPath) {
|
|
11
|
+
try {
|
|
12
|
+
const partialToRender = await import(
|
|
13
|
+
path.join('../', 'components', componentPath + '.ts')
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const propsJSON = props ? JSON.parse(props) : undefined
|
|
17
|
+
const component = await partialToRender.default(propsJSON)
|
|
18
|
+
|
|
19
|
+
await stream.writeSSE({
|
|
20
|
+
event: 'datastar-patch-elements',
|
|
21
|
+
data: `elements ${stripNewlines(component)}`,
|
|
22
|
+
})
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.log(error)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/utils/slugify.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export function slugify(str
|
|
1
|
+
export function slugify(str?: string | null) {
|
|
2
|
+
if (!str) return ''
|
|
3
|
+
|
|
2
4
|
return String(str)
|
|
3
5
|
.normalize('NFKD') // split accented characters into their base characters and diacritical marks
|
|
4
6
|
.replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block.
|
package/utils/startup-log.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import packageJSON from '../package.json' with { type: 'json' }
|
|
2
2
|
|
|
3
3
|
export default ({ port, refresherPort }: { port: number, refresherPort: number }) => {
|
|
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',
|
|
7
|
-
console.log('
|
|
8
|
-
console.log('\x1b[32m%s\x1b[0m',
|
|
9
|
-
console.log(
|
|
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', '╰───────────────╯')
|
|
7
|
+
console.log(' ')
|
|
8
|
+
console.log('\x1b[32m%s\x1b[0m', `Version:`)
|
|
9
|
+
console.log(packageJSON.version)
|
|
10
|
+
console.log(' ')
|
|
11
|
+
console.log('\x1b[32m%s\x1b[0m', `App:`)
|
|
12
|
+
console.log(`http://localhost:${port}`)
|
|
13
|
+
console.log(' ')
|
|
14
|
+
console.log('\x1b[32m%s\x1b[0m', `Studio:`)
|
|
15
|
+
console.log(`http://localhost:${port}/studio`)
|
|
16
|
+
console.log(' ')
|
|
17
|
+
console.log('\x1b[32m%s\x1b[0m', `Refresher:`)
|
|
18
|
+
console.log(`http://localhost:${refresherPort}`)
|
|
10
19
|
}
|
package/components/Backup.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { html } from 'hono/html'
|
|
2
|
-
|
|
3
|
-
export default () => {
|
|
4
|
-
return html`<article>
|
|
5
|
-
<header>Backup</header>
|
|
6
|
-
<form data-on-submit="@post('/admin/api/backup', { contentType: 'form' })">
|
|
7
|
-
<button type="submit">Backup database</button>
|
|
8
|
-
</form>
|
|
9
|
-
</article>`
|
|
10
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { type Block } from '../types.ts'
|
|
2
|
-
import { structure } from '../index.ts'
|
|
3
|
-
|
|
4
|
-
export type StructurePath = (string | number)[]
|
|
5
|
-
|
|
6
|
-
function getTargetPath(
|
|
7
|
-
target: Block,
|
|
8
|
-
path: StructurePath,
|
|
9
|
-
): number | string | undefined {
|
|
10
|
-
if (!path.length) {
|
|
11
|
-
return structure.findIndex((block) => block.name === target.name)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let sub = structure
|
|
15
|
-
|
|
16
|
-
path.forEach((key: number | string) => {
|
|
17
|
-
if (sub) {
|
|
18
|
-
// @ts-ignore
|
|
19
|
-
sub = sub[key]
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
if (Array.isArray(sub)) {
|
|
24
|
-
return sub.findIndex((block) => block.name === target.name)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const buildStructurePath = (
|
|
31
|
-
target: Block | undefined,
|
|
32
|
-
path: StructurePath = [],
|
|
33
|
-
) => {
|
|
34
|
-
if (!target) return path
|
|
35
|
-
|
|
36
|
-
const targetPlacement = getTargetPath(target, path)
|
|
37
|
-
|
|
38
|
-
if (targetPlacement !== undefined) {
|
|
39
|
-
path.push(targetPlacement)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return path
|
|
43
|
-
}
|