@alstar/studio 0.0.0-beta.1 → 0.0.0-beta.11
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 +69 -0
- package/api/auth.ts +66 -0
- package/api/backup.ts +40 -0
- package/api/block.ts +131 -66
- package/api/index.ts +19 -1
- package/api/mcp.ts +50 -0
- package/components/AdminPanel.ts +87 -0
- package/components/Backup.ts +13 -0
- package/components/BlockFieldRenderer.ts +125 -0
- package/components/BlockRenderer.ts +22 -0
- package/components/Entries.ts +20 -12
- package/components/Entry.ts +13 -21
- package/components/FieldRenderer.ts +35 -0
- package/components/Render.ts +46 -0
- package/components/Settings.ts +104 -0
- package/components/SiteLayout.ts +61 -0
- package/components/Users.ts +46 -0
- package/components/fields/Markdown.ts +44 -0
- package/components/fields/Slug.ts +113 -0
- package/components/fields/Text.ts +42 -0
- package/components/fields/index.ts +7 -0
- package/components/icons.ts +136 -7
- package/index.ts +94 -34
- package/package.json +10 -7
- package/pages/entry/[id].ts +15 -0
- package/pages/error.ts +14 -0
- package/{components → pages}/index.ts +7 -4
- package/pages/login.ts +21 -0
- package/pages/register.ts +33 -0
- package/pages/settings.ts +8 -0
- package/public/studio/css/admin-panel.css +103 -0
- package/public/studio/css/blocks-field.css +53 -0
- package/public/studio/css/settings.css +28 -0
- package/public/studio/js/markdown-editor.js +34 -0
- package/public/studio/js/sortable-list.js +50 -0
- package/public/studio/main.css +166 -0
- package/public/studio/main.js +21 -0
- package/queries/block-2.ts +339 -0
- package/queries/block-with-children.ts +74 -0
- package/queries/block.ts +289 -0
- package/queries/db-types.ts +15 -0
- package/queries/getBlockTrees-2.ts +71 -0
- package/queries/getBlockTrees.ts +316 -0
- package/queries/getBlocks.ts +214 -0
- package/queries/index.ts +2 -98
- package/queries/structure-types.ts +97 -0
- package/readme.md +205 -0
- package/schema.sql +18 -0
- package/schemas.ts +23 -52
- package/types.ts +144 -5
- package/utils/auth.ts +54 -0
- package/utils/buildBlocksTree.ts +4 -4
- package/utils/create-hash.ts +9 -0
- package/utils/define.ts +39 -0
- package/utils/file-based-router.ts +11 -2
- package/utils/get-config.ts +8 -9
- package/utils/get-or-create-row.ts +41 -0
- package/utils/html.ts +247 -0
- package/utils/startup-log.ts +19 -0
- package/components/AdminPanel/AdminPanel.css +0 -59
- package/components/AdminPanel/AdminPanel.ts +0 -57
- package/components/Block.ts +0 -116
- package/components/Entry.css +0 -7
- package/components/Field.ts +0 -164
- package/components/Fields.ts +0 -43
- package/components/layout.ts +0 -53
- package/public/main.css +0 -92
- package/public/main.js +0 -43
- /package/public/{favicon.svg → studio/favicon.svg} +0 -0
|
@@ -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/readme.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Alstar Studio
|
|
2
|
+
|
|
3
|
+
Alstar Studio is a **fullstack framework** for building CMS-driven applications with **native Node.js** and **Hono**.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Create a new project:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm create @alstar
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Follow the CLI prompts to set up a starter project in your chosen folder.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
Start the dev server:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This runs a **Hono server**.
|
|
26
|
+
|
|
27
|
+
The core app is created via `createStudio(structure)`, which returns the Hono app. This makes it possible to extend the server with plugins or custom settings:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const app = await createStudio(structure)
|
|
31
|
+
// app.use(...) custom middleware
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Routing
|
|
36
|
+
|
|
37
|
+
Pages are defined in the `/pages` directory.
|
|
38
|
+
|
|
39
|
+
* Each `.ts` file becomes a route.
|
|
40
|
+
* Dynamic routes are created with square brackets, e.g. `/pages/[slug].ts`.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## CMS
|
|
44
|
+
|
|
45
|
+
Access the CMS at:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
/studio
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Defining Content Structure
|
|
52
|
+
|
|
53
|
+
Pass a `Structure` object to `createStudio(structure)` to define the schema.
|
|
54
|
+
|
|
55
|
+
Use the helpers:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import {
|
|
59
|
+
defineBlock,
|
|
60
|
+
defineField,
|
|
61
|
+
defineStructure,
|
|
62
|
+
defineBlockField
|
|
63
|
+
} from '@alstar/studio'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Example: Schema Definition
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const titleField = defineField({
|
|
70
|
+
label: 'Title',
|
|
71
|
+
type: 'text' | 'image' | 'markdown' | 'slug',
|
|
72
|
+
description: 'Page title'
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const pageBuilder = defineBlockField({
|
|
76
|
+
label: 'Sections',
|
|
77
|
+
type: 'blocks',
|
|
78
|
+
children: {
|
|
79
|
+
hero: defineBlock({
|
|
80
|
+
label: 'Hero',
|
|
81
|
+
type: 'hero',
|
|
82
|
+
fields: { /* fields */ },
|
|
83
|
+
}),
|
|
84
|
+
gallery: defineBlock({
|
|
85
|
+
label: 'Gallery',
|
|
86
|
+
type: 'gallery',
|
|
87
|
+
fields: { /* fields */ },
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const entryBlock = defineBlock({
|
|
93
|
+
label: 'Entry',
|
|
94
|
+
type: 'entry',
|
|
95
|
+
fields: {
|
|
96
|
+
title: titleField,
|
|
97
|
+
builder: pageBuilder
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export default defineStructure({
|
|
102
|
+
entry: entryBlock
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Concepts
|
|
107
|
+
|
|
108
|
+
* **Blocks** contain **fields**.
|
|
109
|
+
* **Block fields** (`type: 'blocks'`) can nest multiple block types under `children`.
|
|
110
|
+
* This enables **page builders** and reusable structures.
|
|
111
|
+
|
|
112
|
+
All content is stored in a **SQLite database** (`studio.db`) and can be queried in the templates with the `query` module.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Frontend
|
|
116
|
+
|
|
117
|
+
The framework encourages **server-side rendering** with Hono’s HTML helper (re-exported by the `@alstar/studio` package):
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { defineEntry, html } from '@alstar/studio'
|
|
121
|
+
|
|
122
|
+
export default defineEntry((c) => {
|
|
123
|
+
const slug = c.req.param('slug')
|
|
124
|
+
|
|
125
|
+
return html`
|
|
126
|
+
<h1>Hello World</h1>
|
|
127
|
+
<p>This page is: ${slug}</p>
|
|
128
|
+
`
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Interactivity
|
|
133
|
+
|
|
134
|
+
Even though the framework allows for having any library and tool for creating client-side behavior, it's recommended to use lightweight libraries such as:
|
|
135
|
+
|
|
136
|
+
* [Datastar](https://data-star.dev/) (used internally by the Studio)
|
|
137
|
+
* [Alpine.js](https://alpinejs.dev/)
|
|
138
|
+
|
|
139
|
+
## Quickstart Example Project
|
|
140
|
+
|
|
141
|
+
This example shows how to define a simple **page schema** and render it on the frontend.
|
|
142
|
+
|
|
143
|
+
### 1. Define the CMS Schema (`./index.ts`)
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { createStudio, defineBlock, defineField, defineStructure } from '@alstar/studio'
|
|
147
|
+
|
|
148
|
+
const page = defineBlock({
|
|
149
|
+
label: 'Page',
|
|
150
|
+
type: 'page',
|
|
151
|
+
fields: {
|
|
152
|
+
title: defineField({
|
|
153
|
+
label: 'Title',
|
|
154
|
+
type: 'text',
|
|
155
|
+
}),
|
|
156
|
+
slug: defineField({
|
|
157
|
+
label: 'Slug',
|
|
158
|
+
type: 'slug',
|
|
159
|
+
}),
|
|
160
|
+
body: defineField({
|
|
161
|
+
label: 'Body',
|
|
162
|
+
type: 'markdown',
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const structure = defineStructure({
|
|
168
|
+
page,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await createStudio(structure)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. Create a Frontend Route (`/pages/[slug].ts`)
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { defineEntry, html, query } from '@alstar/studio'
|
|
178
|
+
|
|
179
|
+
export default defineEntry(c) => {
|
|
180
|
+
const slug = c.req.param('slug')
|
|
181
|
+
const page = query.root({ type: 'slug', value: slug })
|
|
182
|
+
|
|
183
|
+
if (!page) return c.notFound()
|
|
184
|
+
|
|
185
|
+
return html`
|
|
186
|
+
<main>
|
|
187
|
+
<h1>${page.fields.title.value}</h1>
|
|
188
|
+
<article>${page.fields.body.value}</article>
|
|
189
|
+
</main>
|
|
190
|
+
`
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 3. Run the Project
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
pnpm run dev
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Visit:
|
|
201
|
+
|
|
202
|
+
* **CMS admin**: `http://localhost:3000/studio`
|
|
203
|
+
* **Frontend page**: `http://localhost:3000/my-first-page`
|
|
204
|
+
|
|
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,55 +1,14 @@
|
|
|
1
1
|
import { sql } from './utils/sql.ts'
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// export const fieldTable = {
|
|
13
|
-
// tableName: 'fields',
|
|
14
|
-
// columns: sql`
|
|
15
|
-
// name TEXT not null, -- Name of the field (e.g., "content", "header", "image")
|
|
16
|
-
// type TEXT not null, -- Field type (e.g., "text", "image", "video")
|
|
17
|
-
// label TEXT not null, -- Field label (e.g., "Text", "Image", "Video")
|
|
18
|
-
// options TEXT -- Additional options or settings (can be a JSON string if needed)
|
|
19
|
-
// `,
|
|
20
|
-
// }
|
|
21
|
-
|
|
22
|
-
// export const entriesFieldsTable = {
|
|
23
|
-
// tableName: 'entry_fields',
|
|
24
|
-
// columns: sql`
|
|
25
|
-
// entry_id INTEGER not null, -- Foreign key to pages
|
|
26
|
-
// field_id INTEGER not null, -- Foreign key to fields
|
|
27
|
-
// position INTEGER, -- Optional: order of the field on the page
|
|
28
|
-
// content TEXT, -- Content of the field (e.g., text, image URL, etc.)
|
|
29
|
-
// foreign key (entry_id) references entries (id),
|
|
30
|
-
// foreign key (field_id) references fields (id)
|
|
31
|
-
// `,
|
|
32
|
-
// }
|
|
33
|
-
|
|
34
|
-
// export const entryTypeTable = {
|
|
35
|
-
// tableName: 'entry_types',
|
|
36
|
-
// columns: sql`
|
|
37
|
-
// name TEXT not null, -- Name of the field (e.g., "content", "header", "image")
|
|
38
|
-
// type TEXT not null, -- Field type (e.g., "text", "image", "video")
|
|
39
|
-
// label TEXT not null, -- Field label (e.g., "Text", "Image", "Video")
|
|
40
|
-
// options TEXT -- Additional options or settings (can be a JSON string if needed)
|
|
41
|
-
// `,
|
|
42
|
-
// }
|
|
3
|
+
// -- API keys
|
|
4
|
+
export const usersTable = {
|
|
5
|
+
tableName: 'users',
|
|
6
|
+
columns: sql`
|
|
7
|
+
email TEXT not null,
|
|
8
|
+
hash TEXT
|
|
9
|
+
`,
|
|
10
|
+
}
|
|
43
11
|
|
|
44
|
-
// export const entryEntryTypeTable = {
|
|
45
|
-
// tableName: 'entry_entry_types',
|
|
46
|
-
// columns: sql`
|
|
47
|
-
// entry_id INTEGER not null, -- Foreign key to pages
|
|
48
|
-
// entry_type_id INTEGER not null, -- Foreign key to fields
|
|
49
|
-
// foreign key (entry_id) references entries (id),
|
|
50
|
-
// foreign key (entry_type_id) references entry_types (id)
|
|
51
|
-
// `,
|
|
52
|
-
// }
|
|
53
12
|
|
|
54
13
|
// -- Blocks
|
|
55
14
|
export const blocksTable = {
|
|
@@ -58,10 +17,22 @@ export const blocksTable = {
|
|
|
58
17
|
name TEXT not null,
|
|
59
18
|
label TEXT not null,
|
|
60
19
|
type TEXT not null,
|
|
61
|
-
sort_order INTEGER not null default 0,
|
|
62
20
|
value TEXT,
|
|
63
21
|
options JSON,
|
|
64
|
-
|
|
65
|
-
|
|
22
|
+
status TEXT default 'enabled',
|
|
23
|
+
sort_order INTEGER not null default 0,
|
|
24
|
+
-- _depth INTEGER,
|
|
25
|
+
parent_id INTEGER,
|
|
26
|
+
foreign key (parent_id) references blocks (id)
|
|
27
|
+
`,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// -- API keys
|
|
31
|
+
export const apiKeysTable = {
|
|
32
|
+
tableName: 'api_keys',
|
|
33
|
+
columns: sql`
|
|
34
|
+
name TEXT not null,
|
|
35
|
+
value TEXT,
|
|
36
|
+
hint TEXT
|
|
66
37
|
`,
|
|
67
38
|
}
|
package/types.ts
CHANGED
|
@@ -1,14 +1,142 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { type Context } from 'hono'
|
|
3
|
+
import { type HonoOptions } from 'hono/hono-base'
|
|
4
|
+
import { type BlankInput, type BlankEnv } from 'hono/types'
|
|
5
|
+
import {
|
|
6
|
+
BlockFieldInstance,
|
|
7
|
+
BlockInstance,
|
|
8
|
+
FieldInstance,
|
|
9
|
+
} from './utils/define.ts'
|
|
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
|
+
|
|
21
|
+
export type PrimitiveField = {
|
|
22
|
+
name: string
|
|
23
|
+
label: string
|
|
24
|
+
type: 'text' | 'slug' | 'markdown' | 'image'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type BlockField = {
|
|
28
|
+
name: string
|
|
29
|
+
label: string
|
|
30
|
+
type: 'blocks'
|
|
31
|
+
children: Record<string, Field | Block>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Field = PrimitiveField | BlockField
|
|
3
35
|
|
|
4
36
|
export type Block = {
|
|
5
37
|
name: string
|
|
6
38
|
label: string
|
|
7
39
|
type: string
|
|
8
|
-
fields
|
|
40
|
+
fields: Record<string, Field | Block>
|
|
9
41
|
}
|
|
10
42
|
|
|
11
|
-
export type Structure =
|
|
43
|
+
export type Structure = Record<string, BlockDefStructure>
|
|
44
|
+
// export type Structure = Record<string, BlockDefStructure>
|
|
45
|
+
|
|
46
|
+
// --- Field & block definitions ---
|
|
47
|
+
type FieldType = 'text' | 'slug' | 'markdown' | 'image'
|
|
48
|
+
|
|
49
|
+
interface BaseField {
|
|
50
|
+
label: string
|
|
51
|
+
type: FieldType
|
|
52
|
+
description?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface TextField extends BaseField {
|
|
56
|
+
type: 'text' | 'slug' | 'markdown'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface TextFieldStructure extends TextField {
|
|
60
|
+
instanceOf: typeof FieldInstance
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ImageField extends BaseField {
|
|
64
|
+
type: 'image'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ImageFieldStructure extends ImageField {
|
|
68
|
+
instanceOf: typeof FieldInstance
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BlocksFieldDef {
|
|
72
|
+
label: string
|
|
73
|
+
type: 'blocks'
|
|
74
|
+
description?: string
|
|
75
|
+
children: Record<string, BlockDefStructure | FieldDefStructure>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface BlocksFieldDefStructure extends BlocksFieldDef {
|
|
79
|
+
instanceOf: typeof BlockFieldInstance
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type FieldDef = TextField | ImageField
|
|
83
|
+
export type FieldDefStructure = TextFieldStructure | ImageFieldStructure
|
|
84
|
+
|
|
85
|
+
export interface BlockDef {
|
|
86
|
+
label: string
|
|
87
|
+
type: string
|
|
88
|
+
fields: Record<string, FieldDefStructure | BlocksFieldDefStructure>
|
|
89
|
+
description?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface BlockDefStructure extends BlockDef {
|
|
93
|
+
instanceOf: typeof BlockInstance
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// type DBDefaults = {
|
|
97
|
+
// id: number
|
|
98
|
+
// created_at: string
|
|
99
|
+
// updated_at: string
|
|
100
|
+
// name: string
|
|
101
|
+
// label: string
|
|
102
|
+
// // type: string
|
|
103
|
+
// sort_order: number
|
|
104
|
+
// value: string
|
|
105
|
+
// options: string | null
|
|
106
|
+
// status: 'enabled' | 'disabled'
|
|
107
|
+
// parent_id: number | null
|
|
108
|
+
// depth: number
|
|
109
|
+
// }
|
|
110
|
+
|
|
111
|
+
type BaseDBResult = {
|
|
112
|
+
id: number
|
|
113
|
+
created_at: string
|
|
114
|
+
updated_at: string
|
|
115
|
+
name: string
|
|
116
|
+
label: string
|
|
117
|
+
sort_order: number
|
|
118
|
+
value: string | null
|
|
119
|
+
options: any
|
|
120
|
+
status: 'enabled' | 'disabled'
|
|
121
|
+
parent_id: number | null
|
|
122
|
+
depth: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type DBPrimitiveFieldResult = BaseDBResult & {
|
|
126
|
+
type: FieldDef
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type DBBlockFieldResult = BaseDBResult & {
|
|
130
|
+
type: 'blocks'
|
|
131
|
+
children: DBBlockResult[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type DBBlockResult = BaseDBResult & {
|
|
135
|
+
type: string
|
|
136
|
+
fields: Record<string, DBFieldResult>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type DBFieldResult = DBPrimitiveFieldResult & DBBlockFieldResult
|
|
12
140
|
|
|
13
141
|
export type DBBlock = Block & {
|
|
14
142
|
id: number
|
|
@@ -16,10 +144,21 @@ export type DBBlock = Block & {
|
|
|
16
144
|
updated_at: string
|
|
17
145
|
value: string | null
|
|
18
146
|
sort_order: number | null
|
|
19
|
-
|
|
147
|
+
parent_id: number | null
|
|
20
148
|
options: number | null
|
|
21
149
|
}
|
|
22
150
|
|
|
151
|
+
export type BlockStatus = 'enabled' | 'disabled'
|
|
152
|
+
|
|
23
153
|
export type StudioConfig = {
|
|
154
|
+
siteName?: string
|
|
24
155
|
honoConfig?: HonoOptions<BlankEnv>
|
|
156
|
+
port?: number
|
|
157
|
+
structure: Structure
|
|
25
158
|
}
|
|
159
|
+
|
|
160
|
+
export type RequestContext = Context<
|
|
161
|
+
{ Bindings: HttpBindings },
|
|
162
|
+
string,
|
|
163
|
+
BlankInput
|
|
164
|
+
>
|
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/buildBlocksTree.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
30
|
-
const parent = blockMap.get(block.
|
|
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.
|
|
35
|
+
`Parent with id ${block.parent_id} not found for block ${block.id}`,
|
|
36
36
|
)
|
|
37
37
|
}
|
|
38
38
|
} else {
|