@alstar/studio 0.0.0-beta.4 → 0.0.0-beta.5
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/block.ts +28 -2
- package/components/AdminPanel/AdminPanel.css +24 -5
- package/components/Block.ts +1 -1
- package/components/Entries.ts +17 -10
- package/components/Field.ts +5 -1
- package/components/icons.ts +38 -0
- package/components/layout.ts +6 -3
- package/index.ts +21 -12
- package/package.json +3 -3
- package/public/main.css +3 -1
- package/queries/block.ts +298 -0
- package/queries/index.ts +1 -98
- package/schemas.ts +1 -51
- package/types.ts +13 -2
- package/utils/define.ts +3 -1
- package/utils/file-based-router.ts +9 -1
- package/utils/get-config.ts +8 -9
package/api/block.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
-
|
|
2
|
+
import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { streamSSE } from 'hono/streaming'
|
|
5
5
|
import { stripNewlines } from '../utils/strip-newlines.ts'
|
|
@@ -67,6 +67,7 @@ export const sectionRoutes = (structure: Structure) => {
|
|
|
67
67
|
|
|
68
68
|
const id = formData.get('id')?.toString()
|
|
69
69
|
const value = formData.get('value')?.toString()
|
|
70
|
+
const name = formData.get('name')?.toString()
|
|
70
71
|
const entryId = formData.get('entryId')?.toString()
|
|
71
72
|
const parentId = formData.get('parentId')?.toString()
|
|
72
73
|
const sortOrder = formData.get('sort_order')?.toString()
|
|
@@ -84,7 +85,7 @@ export const sectionRoutes = (structure: Structure) => {
|
|
|
84
85
|
|
|
85
86
|
transaction.run(value, id, sortOrder)
|
|
86
87
|
|
|
87
|
-
if (entryId === parentId) {
|
|
88
|
+
if (entryId === parentId && name?.toString() === 'title') {
|
|
88
89
|
await stream.writeSSE({
|
|
89
90
|
event: 'datastar-patch-elements',
|
|
90
91
|
data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
|
|
@@ -93,5 +94,30 @@ export const sectionRoutes = (structure: Structure) => {
|
|
|
93
94
|
})
|
|
94
95
|
})
|
|
95
96
|
|
|
97
|
+
app.delete('/block', async (c) => {
|
|
98
|
+
return streamSSE(c, async (stream) => {
|
|
99
|
+
const formData = await c.req.formData()
|
|
100
|
+
|
|
101
|
+
const id = formData.get('id')?.toString()
|
|
102
|
+
|
|
103
|
+
if (!id) return
|
|
104
|
+
|
|
105
|
+
const transaction = db.database.prepare(sql`
|
|
106
|
+
update blocks
|
|
107
|
+
set
|
|
108
|
+
status = 'disabled'
|
|
109
|
+
where
|
|
110
|
+
id = ?
|
|
111
|
+
`)
|
|
112
|
+
|
|
113
|
+
transaction.run(id)
|
|
114
|
+
|
|
115
|
+
await stream.writeSSE({
|
|
116
|
+
event: 'datastar-patch-elements',
|
|
117
|
+
data: `elements ${stripNewlines(Entries())}`,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
96
122
|
return app
|
|
97
123
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
.admin-panel {
|
|
2
2
|
/* background: hsla(0, 0%, 0%, 0.1); */
|
|
3
3
|
padding: 40px;
|
|
4
|
-
padding-right: 0;
|
|
5
4
|
|
|
6
5
|
height: 100%;
|
|
7
6
|
min-height: inherit;
|
|
8
7
|
|
|
9
|
-
min-width:
|
|
8
|
+
min-width: 250px;
|
|
10
9
|
|
|
11
10
|
> h1 {
|
|
12
11
|
padding-bottom: 1rem;
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
|
|
23
22
|
form {
|
|
24
23
|
padding-bottom: 1rem;
|
|
25
|
-
|
|
24
|
+
|
|
26
25
|
button {
|
|
27
26
|
margin: 10px 0px 20px;
|
|
28
27
|
}
|
|
@@ -30,6 +29,16 @@
|
|
|
30
29
|
|
|
31
30
|
#entries ul {
|
|
32
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
|
+
}
|
|
33
42
|
|
|
34
43
|
> li {
|
|
35
44
|
margin-bottom: 0px;
|
|
@@ -37,9 +46,7 @@
|
|
|
37
46
|
display: flex;
|
|
38
47
|
justify-content: space-between;
|
|
39
48
|
align-items: stretch;
|
|
40
|
-
/* align-items: center; */
|
|
41
49
|
list-style: none;
|
|
42
|
-
margin-inline: -1rem;
|
|
43
50
|
|
|
44
51
|
a {
|
|
45
52
|
text-decoration: none;
|
|
@@ -49,6 +56,8 @@
|
|
|
49
56
|
|
|
50
57
|
button {
|
|
51
58
|
border-radius: 7px;
|
|
59
|
+
opacity: 0;
|
|
60
|
+
transition: opacity 100px;
|
|
52
61
|
|
|
53
62
|
svg {
|
|
54
63
|
margin: 0.5rem 1rem;
|
|
@@ -57,3 +66,13 @@
|
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
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
|
+
}
|
package/components/Block.ts
CHANGED
|
@@ -16,7 +16,7 @@ export default (
|
|
|
16
16
|
structurePath: StructurePath,
|
|
17
17
|
) => {
|
|
18
18
|
const data = query.block({ id: id.toString() })
|
|
19
|
-
const blocks = query.blocks({
|
|
19
|
+
const blocks = query.blocks({ parent_block_id: id })
|
|
20
20
|
|
|
21
21
|
if (!data) return html`<p>No block data</p>`
|
|
22
22
|
|
package/components/Entries.ts
CHANGED
|
@@ -3,30 +3,37 @@ import { query } from '../index.ts'
|
|
|
3
3
|
import * as icons from './icons.ts'
|
|
4
4
|
|
|
5
5
|
export default () => {
|
|
6
|
-
const entries = query.blocks({
|
|
6
|
+
const entries = query.blocks({ parent_block_id: null, status: 'enabled' })
|
|
7
7
|
|
|
8
8
|
return html`
|
|
9
9
|
<section id="entries">
|
|
10
10
|
<ul>
|
|
11
11
|
${entries?.map((block) => {
|
|
12
12
|
const title = query.block({
|
|
13
|
-
|
|
13
|
+
parent_block_id: block.id.toString(),
|
|
14
14
|
name: 'title',
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
return html`
|
|
18
18
|
<li>
|
|
19
19
|
<a href="/admin/entry/${block.id}" id="block_link_${block.id}">
|
|
20
|
-
${
|
|
20
|
+
${title?.value || 'Untitled'}
|
|
21
21
|
</a>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
data-
|
|
25
|
-
class="ghost"
|
|
26
|
-
style="padding: 0"
|
|
22
|
+
|
|
23
|
+
<form
|
|
24
|
+
data-on-submit="@delete('/admin/api/block', { contentType: 'form' })"
|
|
27
25
|
>
|
|
28
|
-
{
|
|
29
|
-
|
|
26
|
+
<input type="hidden" name="id" value="${block.id}" />
|
|
27
|
+
<button
|
|
28
|
+
data-tooltip="Remove"
|
|
29
|
+
data-placement="right"
|
|
30
|
+
class="ghost"
|
|
31
|
+
style="padding: 0"
|
|
32
|
+
type="submit"
|
|
33
|
+
>
|
|
34
|
+
${icons.trash}
|
|
35
|
+
</button>
|
|
36
|
+
</form>
|
|
30
37
|
</li>
|
|
31
38
|
`
|
|
32
39
|
})}
|
package/components/Field.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
function getData(parentId: string | number, field: Block, sortOrder: number) {
|
|
13
13
|
const data = query.block({
|
|
14
|
-
|
|
14
|
+
parent_block_id: parentId?.toString() || null,
|
|
15
15
|
name: field.name,
|
|
16
16
|
sort_order: sortOrder.toString(),
|
|
17
17
|
})
|
|
@@ -71,6 +71,7 @@ const Field = (props: {
|
|
|
71
71
|
<input type="hidden" name="entryId" value="${entryId}" />
|
|
72
72
|
<input type="hidden" name="parentId" value="${parentId}" />
|
|
73
73
|
<input type="hidden" name="sort_order" value="${sortOrder}" />
|
|
74
|
+
<input type="hidden" name="name" value="${blockStructure.name}" />
|
|
74
75
|
<input
|
|
75
76
|
type="hidden"
|
|
76
77
|
name="path"
|
|
@@ -96,6 +97,7 @@ const Field = (props: {
|
|
|
96
97
|
<input type="hidden" name="entryId" value="${entryId}" />
|
|
97
98
|
<input type="hidden" name="parentId" value="${parentId}" />
|
|
98
99
|
<input type="hidden" name="sort_order" value="${sortOrder}" />
|
|
100
|
+
<input type="hidden" name="name" value="${blockStructure.name}" />
|
|
99
101
|
<input
|
|
100
102
|
type="hidden"
|
|
101
103
|
name="path"
|
|
@@ -121,6 +123,7 @@ const Field = (props: {
|
|
|
121
123
|
<input type="hidden" name="entryId" value="${entryId}" />
|
|
122
124
|
<input type="hidden" name="parentId" value="${parentId}" />
|
|
123
125
|
<input type="hidden" name="sort_order" value="${sortOrder}" />
|
|
126
|
+
<input type="hidden" name="name" value="${blockStructure.name}" />
|
|
124
127
|
<input
|
|
125
128
|
type="hidden"
|
|
126
129
|
name="path"
|
|
@@ -149,6 +152,7 @@ const Field = (props: {
|
|
|
149
152
|
<input type="hidden" name="entryId" value="${entryId}" />
|
|
150
153
|
<input type="hidden" name="parentId" value="${parentId}" />
|
|
151
154
|
<input type="hidden" name="sort_order" value="${sortOrder}" />
|
|
155
|
+
<input type="hidden" name="name" value="${blockStructure.name}" />
|
|
152
156
|
<input
|
|
153
157
|
type="hidden"
|
|
154
158
|
name="path"
|
package/components/icons.ts
CHANGED
|
@@ -97,3 +97,41 @@ export const newDocument = html`
|
|
|
97
97
|
/>
|
|
98
98
|
</svg>
|
|
99
99
|
`
|
|
100
|
+
|
|
101
|
+
export const dots = html`
|
|
102
|
+
<svg
|
|
103
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
104
|
+
width="16"
|
|
105
|
+
height="16"
|
|
106
|
+
viewBox="0 0 24 24"
|
|
107
|
+
>
|
|
108
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
109
|
+
<path
|
|
110
|
+
fill="none"
|
|
111
|
+
stroke="currentColor"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
stroke-linejoin="round"
|
|
114
|
+
stroke-width="1.5"
|
|
115
|
+
d="M6.75 12a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m6 0a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m6 0a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
118
|
+
`
|
|
119
|
+
|
|
120
|
+
export const trash = html`
|
|
121
|
+
<svg
|
|
122
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
123
|
+
width="16"
|
|
124
|
+
height="16"
|
|
125
|
+
viewBox="0 0 24 24"
|
|
126
|
+
>
|
|
127
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
128
|
+
<path
|
|
129
|
+
fill="none"
|
|
130
|
+
stroke="currentColor"
|
|
131
|
+
stroke-linecap="round"
|
|
132
|
+
stroke-linejoin="round"
|
|
133
|
+
stroke-width="1.5"
|
|
134
|
+
d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21q.512.078 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48 48 0 0 0-3.478-.397m-12 .562q.51-.088 1.022-.165m0 0a48 48 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a52 52 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a49 49 0 0 0-7.5 0"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
`
|
package/components/layout.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import adminPanel from './AdminPanel/AdminPanel.ts'
|
|
2
2
|
import { html } from 'hono/html'
|
|
3
3
|
import { type HtmlEscapedString } from 'hono/utils/html'
|
|
4
|
-
import { rootdir } from '../index.ts'
|
|
4
|
+
import { rootdir, studioConfig } from '../index.ts'
|
|
5
5
|
import { type Structure } from '../types.ts'
|
|
6
6
|
|
|
7
7
|
export default (props: {
|
|
@@ -18,7 +18,10 @@ export default (props: {
|
|
|
18
18
|
<head>
|
|
19
19
|
<meta charset="UTF-8" />
|
|
20
20
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
21
|
-
<title>
|
|
21
|
+
<title>
|
|
22
|
+
${studioConfig.siteName ? studioConfig.siteName + ' | ' : ''}Alstar
|
|
23
|
+
Studio
|
|
24
|
+
</title>
|
|
22
25
|
|
|
23
26
|
<link
|
|
24
27
|
rel="icon"
|
|
@@ -49,7 +52,7 @@ export default (props: {
|
|
|
49
52
|
<body data-barba="wrapper">
|
|
50
53
|
<section>${adminPanel(props.structure)}</section>
|
|
51
54
|
|
|
52
|
-
<main
|
|
55
|
+
<main>
|
|
53
56
|
<section data-barba="container" data-barba-namespace="default">
|
|
54
57
|
${props.content}
|
|
55
58
|
</section>
|
package/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import { loadDb } from '@alstar/db'
|
|
3
|
-
import { query } from './queries/index.ts'
|
|
4
3
|
import { sectionRoutes } from './api/index.ts'
|
|
5
4
|
import { getConfig } from './utils/get-config.ts'
|
|
6
5
|
import * as types from './types.ts'
|
|
@@ -14,30 +13,31 @@ import { serveStatic } from '@hono/node-server/serve-static'
|
|
|
14
13
|
import { createStudioTables } from './utils/create-studio-tables.ts'
|
|
15
14
|
import { fileBasedRouter } from './utils/file-based-router.ts'
|
|
16
15
|
|
|
17
|
-
export { html, type HtmlEscapedString } from './utils/html.ts'
|
|
18
|
-
|
|
19
16
|
export let structure: types.Structure
|
|
20
17
|
export let rootdir = '/node_modules/@alstar/studio'
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
structure = studioStructure || []
|
|
19
|
+
export let studioConfig: types.StudioConfig = {
|
|
20
|
+
siteName: '',
|
|
21
|
+
}
|
|
26
22
|
|
|
23
|
+
const createStudio = async (studioStructure?: types.Structure) => {
|
|
27
24
|
loadDb('./studio.db')
|
|
28
|
-
|
|
29
25
|
createStudioTables()
|
|
30
26
|
|
|
31
|
-
const
|
|
27
|
+
const configFile = await getConfig<types.StudioConfig>()
|
|
28
|
+
|
|
29
|
+
structure = studioStructure || []
|
|
30
|
+
studioConfig = { ...studioConfig, ...configFile }
|
|
31
|
+
|
|
32
|
+
const app = new Hono(studioConfig.honoConfig)
|
|
32
33
|
|
|
33
34
|
app.use('*', serveStatic({ root: './' }))
|
|
34
35
|
app.use('*', serveStatic({ root: './public' }))
|
|
35
36
|
|
|
36
|
-
app.
|
|
37
|
+
app.get('/admin', (c) => c.html(Layout({ structure, content: IndexPage() })))
|
|
37
38
|
|
|
38
39
|
app.route('/admin/api', sectionRoutes(structure))
|
|
39
40
|
|
|
40
|
-
app.get('/admin', (c) => c.html(Layout({ structure, content: IndexPage() })))
|
|
41
41
|
app.get('/admin/entry/:id', (c) => {
|
|
42
42
|
return c.html(
|
|
43
43
|
Layout({
|
|
@@ -47,6 +47,12 @@ const createStudio = async (studioStructure?: types.Structure) => {
|
|
|
47
47
|
)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
const pages = await fileBasedRouter('./pages')
|
|
51
|
+
|
|
52
|
+
if (pages) {
|
|
53
|
+
app.route('/', pages)
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
const server = serve(app)
|
|
51
57
|
|
|
52
58
|
// graceful shutdown
|
|
@@ -68,4 +74,7 @@ const createStudio = async (studioStructure?: types.Structure) => {
|
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
export { defineConfig, defineEntry, defineStructure } from './utils/define.ts'
|
|
71
|
-
export {
|
|
77
|
+
export { type RequestContext } from './types.ts'
|
|
78
|
+
export { createStudio }
|
|
79
|
+
export { html, type HtmlEscapedString } from './utils/html.ts'
|
|
80
|
+
export { query } from './queries/index.ts'
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alstar/studio",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@hono/node-server": "^1.18.1",
|
|
8
8
|
"@starfederation/datastar-sdk": "1.0.0-RC.1",
|
|
9
9
|
"hono": "^4.8.12",
|
|
10
|
-
"@alstar/
|
|
11
|
-
"@alstar/
|
|
10
|
+
"@alstar/ui": "0.0.0-beta.1",
|
|
11
|
+
"@alstar/db": "0.0.0-beta.1"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/node": "^24.1.0",
|
package/public/main.css
CHANGED
package/queries/block.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
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
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
|
|
22
|
+
const map = new Map<number, DBBlockResult>()
|
|
23
|
+
const roots: DBBlockResult[] = []
|
|
24
|
+
|
|
25
|
+
for (const block of blocks) {
|
|
26
|
+
block.children = []
|
|
27
|
+
map.set(block.id, block)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const block of blocks) {
|
|
31
|
+
if (block.parent_block_id === null) {
|
|
32
|
+
roots.push(block)
|
|
33
|
+
} else {
|
|
34
|
+
const parent = map.get(block.parent_block_id)
|
|
35
|
+
if (parent) parent.children!.push(block)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sort children by sort_order recursively
|
|
40
|
+
const sortChildren = (node: DBBlockResult) => {
|
|
41
|
+
node.children!.sort((a, b) => a.sort_order - b.sort_order)
|
|
42
|
+
node.children!.forEach(sortChildren)
|
|
43
|
+
}
|
|
44
|
+
roots.forEach(sortChildren)
|
|
45
|
+
|
|
46
|
+
return roots
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildTree(blocks: DBBlockResult[]): DBBlockResult {
|
|
50
|
+
const map = new Map<number, DBBlockResult>()
|
|
51
|
+
const roots: DBBlockResult[] = []
|
|
52
|
+
|
|
53
|
+
for (const block of blocks) {
|
|
54
|
+
block.children = []
|
|
55
|
+
map.set(block.id, block)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const block of blocks) {
|
|
59
|
+
if (block.parent_block_id === null) {
|
|
60
|
+
roots.push(block)
|
|
61
|
+
} else {
|
|
62
|
+
const parent = map.get(block.parent_block_id)
|
|
63
|
+
if (parent) parent.children!.push(block)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Sort children by sort_order recursively
|
|
68
|
+
const sortChildren = (node: DBBlockResult) => {
|
|
69
|
+
node.children!.sort((a, b) => a.sort_order - b.sort_order)
|
|
70
|
+
node.children!.forEach(sortChildren)
|
|
71
|
+
}
|
|
72
|
+
roots.forEach(sortChildren)
|
|
73
|
+
|
|
74
|
+
return roots[0]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function transformDBBlockResultTree(
|
|
78
|
+
block: DBBlockResult,
|
|
79
|
+
isBlocksChild?: boolean,
|
|
80
|
+
): DBBlockResult {
|
|
81
|
+
const fields: Record<string, DBBlockResult> = {}
|
|
82
|
+
|
|
83
|
+
for (const child of block.children ?? []) {
|
|
84
|
+
const transformedChild = transformDBBlockResultTree(
|
|
85
|
+
child,
|
|
86
|
+
child.type === 'blocks',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (isBlocksChild) {
|
|
90
|
+
} else {
|
|
91
|
+
fields[transformedChild.name] = transformedChild
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
block.fields = fields
|
|
96
|
+
|
|
97
|
+
if (!isBlocksChild) {
|
|
98
|
+
block.children = [] // clear children array, since all children moved into fields
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return block
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
|
|
105
|
+
return blocks.map((block) => transformDBBlockResultTree(block))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function rootQuery(filterSql: string, depthLimit?: number) {
|
|
109
|
+
const depthLimitClause =
|
|
110
|
+
depthLimit !== undefined ? `WHERE d.depth + 1 <= ${depthLimit}` : ''
|
|
111
|
+
|
|
112
|
+
return sql`
|
|
113
|
+
with recursive
|
|
114
|
+
ancestors as (
|
|
115
|
+
select
|
|
116
|
+
id,
|
|
117
|
+
created_at,
|
|
118
|
+
updated_at,
|
|
119
|
+
name,
|
|
120
|
+
label,
|
|
121
|
+
type,
|
|
122
|
+
sort_order,
|
|
123
|
+
value,
|
|
124
|
+
options,
|
|
125
|
+
status,
|
|
126
|
+
parent_block_id,
|
|
127
|
+
0 as depth
|
|
128
|
+
from
|
|
129
|
+
blocks
|
|
130
|
+
where
|
|
131
|
+
${filterSql}
|
|
132
|
+
union all
|
|
133
|
+
select
|
|
134
|
+
b.id,
|
|
135
|
+
b.created_at,
|
|
136
|
+
b.updated_at,
|
|
137
|
+
b.name,
|
|
138
|
+
b.label,
|
|
139
|
+
b.type,
|
|
140
|
+
b.sort_order,
|
|
141
|
+
b.value,
|
|
142
|
+
b.options,
|
|
143
|
+
b.status,
|
|
144
|
+
b.parent_block_id,
|
|
145
|
+
a.depth + 1
|
|
146
|
+
from
|
|
147
|
+
blocks b
|
|
148
|
+
inner join ancestors a on b.id = a.parent_block_id
|
|
149
|
+
),
|
|
150
|
+
roots as (
|
|
151
|
+
select
|
|
152
|
+
id,
|
|
153
|
+
created_at,
|
|
154
|
+
updated_at,
|
|
155
|
+
name,
|
|
156
|
+
label,
|
|
157
|
+
type,
|
|
158
|
+
sort_order,
|
|
159
|
+
value,
|
|
160
|
+
options,
|
|
161
|
+
status,
|
|
162
|
+
parent_block_id,
|
|
163
|
+
0 as depth
|
|
164
|
+
from
|
|
165
|
+
ancestors
|
|
166
|
+
where
|
|
167
|
+
parent_block_id is null
|
|
168
|
+
),
|
|
169
|
+
descendants as (
|
|
170
|
+
select
|
|
171
|
+
id,
|
|
172
|
+
created_at,
|
|
173
|
+
updated_at,
|
|
174
|
+
name,
|
|
175
|
+
label,
|
|
176
|
+
type,
|
|
177
|
+
sort_order,
|
|
178
|
+
value,
|
|
179
|
+
options,
|
|
180
|
+
status,
|
|
181
|
+
parent_block_id,
|
|
182
|
+
depth
|
|
183
|
+
from
|
|
184
|
+
roots
|
|
185
|
+
union all
|
|
186
|
+
select
|
|
187
|
+
b.id,
|
|
188
|
+
b.created_at,
|
|
189
|
+
b.updated_at,
|
|
190
|
+
b.name,
|
|
191
|
+
b.label,
|
|
192
|
+
b.type,
|
|
193
|
+
b.sort_order,
|
|
194
|
+
b.value,
|
|
195
|
+
b.options,
|
|
196
|
+
b.status,
|
|
197
|
+
b.parent_block_id,
|
|
198
|
+
d.depth + 1
|
|
199
|
+
from
|
|
200
|
+
blocks b
|
|
201
|
+
inner join descendants d on b.parent_block_id = d.id ${depthLimitClause}
|
|
202
|
+
)
|
|
203
|
+
select
|
|
204
|
+
*
|
|
205
|
+
from
|
|
206
|
+
descendants
|
|
207
|
+
order by
|
|
208
|
+
parent_block_id,
|
|
209
|
+
sort_order;
|
|
210
|
+
`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildFilterSql(params: Record<string, any>) {
|
|
214
|
+
const entries = Object.entries(params)
|
|
215
|
+
const filterSql = entries
|
|
216
|
+
.map(([key, value]) =>
|
|
217
|
+
value === null ? `${key} is null` : `${key} = :${key}`,
|
|
218
|
+
)
|
|
219
|
+
.join(' and ')
|
|
220
|
+
|
|
221
|
+
let sqlParams: Record<keyof typeof params, any> = {}
|
|
222
|
+
|
|
223
|
+
for (const param in params) {
|
|
224
|
+
if (params[param] !== null) {
|
|
225
|
+
sqlParams[param] = params[param]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { filterSql, sqlParams }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function roots(
|
|
233
|
+
params: Record<string, any>,
|
|
234
|
+
options?: {
|
|
235
|
+
depth?: number
|
|
236
|
+
},
|
|
237
|
+
): DBBlockResult[] | [] {
|
|
238
|
+
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
239
|
+
|
|
240
|
+
const query = rootQuery(filterSql, options?.depth)
|
|
241
|
+
const rows = db.database
|
|
242
|
+
.prepare(query)
|
|
243
|
+
.all(sqlParams) as unknown as DBBlockResult[]
|
|
244
|
+
|
|
245
|
+
if (!rows.length) return []
|
|
246
|
+
|
|
247
|
+
const forest = buildForest(rows)
|
|
248
|
+
|
|
249
|
+
return transformForest(forest)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function root(
|
|
253
|
+
params: Record<string, any>,
|
|
254
|
+
options?: { depth?: number },
|
|
255
|
+
): DBBlockResult | null {
|
|
256
|
+
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
257
|
+
|
|
258
|
+
const query = rootQuery(filterSql, options?.depth)
|
|
259
|
+
const rows = db.database
|
|
260
|
+
.prepare(query)
|
|
261
|
+
.all(sqlParams) as unknown as DBBlockResult[]
|
|
262
|
+
|
|
263
|
+
if (!rows.length) return null
|
|
264
|
+
|
|
265
|
+
const tree = buildTree(rows)
|
|
266
|
+
|
|
267
|
+
return transformDBBlockResultTree(tree)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function block(params: Record<string, any>) {
|
|
271
|
+
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
272
|
+
|
|
273
|
+
const query = sql`
|
|
274
|
+
select
|
|
275
|
+
*
|
|
276
|
+
from
|
|
277
|
+
blocks
|
|
278
|
+
where
|
|
279
|
+
${filterSql}
|
|
280
|
+
`
|
|
281
|
+
|
|
282
|
+
return db.database.prepare(query).get(sqlParams) as unknown as DBBlockResult
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function blocks(params: Record<string, any>) {
|
|
286
|
+
const { filterSql, sqlParams } = buildFilterSql(params)
|
|
287
|
+
|
|
288
|
+
const query = sql`
|
|
289
|
+
select
|
|
290
|
+
*
|
|
291
|
+
from
|
|
292
|
+
blocks
|
|
293
|
+
where
|
|
294
|
+
${filterSql}
|
|
295
|
+
`
|
|
296
|
+
|
|
297
|
+
return db.database.prepare(query).all(sqlParams) as unknown as DBBlockResult[]
|
|
298
|
+
}
|
package/queries/index.ts
CHANGED
|
@@ -1,98 +1 @@
|
|
|
1
|
-
|
|
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'
|
package/schemas.ts
CHANGED
|
@@ -1,56 +1,5 @@
|
|
|
1
1
|
import { sql } from './utils/sql.ts'
|
|
2
2
|
|
|
3
|
-
// export const entriesTable = {
|
|
4
|
-
// tableName: 'entries',
|
|
5
|
-
// columns: sql`
|
|
6
|
-
// title TEXT not null, -- Title of the page
|
|
7
|
-
// slug TEXT not null unique, -- URL slug for the page
|
|
8
|
-
// meta_description TEXT -- Optional meta description for SEO
|
|
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
|
-
// }
|
|
43
|
-
|
|
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
|
-
|
|
54
3
|
// -- Blocks
|
|
55
4
|
export const blocksTable = {
|
|
56
5
|
tableName: 'blocks',
|
|
@@ -61,6 +10,7 @@ export const blocksTable = {
|
|
|
61
10
|
sort_order INTEGER not null default 0,
|
|
62
11
|
value TEXT,
|
|
63
12
|
options JSON,
|
|
13
|
+
status TEXT default 'enabled',
|
|
64
14
|
parent_block_id INTEGER,
|
|
65
15
|
foreign key (parent_block_id) references blocks (id)
|
|
66
16
|
`,
|
package/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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'
|
|
3
5
|
|
|
4
6
|
export type Block = {
|
|
5
7
|
name: string
|
|
@@ -21,5 +23,14 @@ export type DBBlock = Block & {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export type StudioConfig = {
|
|
26
|
+
siteName: string
|
|
24
27
|
honoConfig?: HonoOptions<BlankEnv>
|
|
25
28
|
}
|
|
29
|
+
|
|
30
|
+
export type BlockStatus = 'enabled' | 'disabled'
|
|
31
|
+
|
|
32
|
+
export type RequestContext = Context<
|
|
33
|
+
{ Bindings: HttpBindings },
|
|
34
|
+
string,
|
|
35
|
+
BlankInput
|
|
36
|
+
>
|
package/utils/define.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { type BlankInput } from 'hono/types'
|
|
|
5
5
|
import { type HtmlEscapedString } from './html.ts'
|
|
6
6
|
|
|
7
7
|
export const defineConfig = (config: types.StudioConfig) => config
|
|
8
|
+
|
|
8
9
|
export const defineStructure = (structure: types.Structure) => structure
|
|
10
|
+
|
|
9
11
|
export const defineEntry = (
|
|
10
12
|
fn: (
|
|
11
|
-
c:
|
|
13
|
+
c: types.RequestContext,
|
|
12
14
|
) => HtmlEscapedString | Promise<HtmlEscapedString>,
|
|
13
15
|
) => fn
|
|
@@ -9,7 +9,15 @@ export const fileBasedRouter = async (rootdir: string) => {
|
|
|
9
9
|
const router = new Hono()
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(rootdir)
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
let dirs
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
dirs = await fs.readdir(root, { recursive: true })
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
const files = dirs.filter((dir) => path.extname(dir))
|
|
14
22
|
|
|
15
23
|
await Promise.all(
|
package/utils/get-config.ts
CHANGED
|
@@ -3,15 +3,6 @@ import path from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
const CONFIG_FILE_NAME = 'alstar.config.ts'
|
|
5
5
|
|
|
6
|
-
async function fileExists(filepath: string) {
|
|
7
|
-
// does the file exist?
|
|
8
|
-
try {
|
|
9
|
-
await fs.stat(filepath)
|
|
10
|
-
} catch (error) {
|
|
11
|
-
return null
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
export const getConfig = async <P>(): Promise<P> => {
|
|
16
7
|
const root = path.resolve('./')
|
|
17
8
|
|
|
@@ -24,3 +15,11 @@ export const getConfig = async <P>(): Promise<P> => {
|
|
|
24
15
|
|
|
25
16
|
return config as P
|
|
26
17
|
}
|
|
18
|
+
|
|
19
|
+
async function fileExists(filepath: string) {
|
|
20
|
+
try {
|
|
21
|
+
return await fs.stat(filepath)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|