@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type HttpBindings } from '@hono/node-server'
2
- // import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
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: 200px;
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
+ }
@@ -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({ parent: id })
19
+ const blocks = query.blocks({ parent_block_id: id })
20
20
 
21
21
  if (!data) return html`<p>No block data</p>`
22
22
 
@@ -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({ parent: null })
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
- parent: block.id.toString(),
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
- ${block.value || title?.value || 'Untitled'}
20
+ ${title?.value || 'Untitled'}
21
21
  </a>
22
- <!-- <button
23
- data-tooltip="Rename"
24
- data-placement="right"
25
- class="ghost"
26
- style="padding: 0"
22
+
23
+ <form
24
+ data-on-submit="@delete('/admin/api/block', { contentType: 'form' })"
27
25
  >
28
- {icons.pen}
29
- </button> -->
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
  })}
@@ -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
- parent: parentId?.toString() || null,
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"
@@ -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
+ `
@@ -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>Alster CMS</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 style="height: 100vh; overflow: auto">
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
- const createStudio = async (studioStructure?: types.Structure) => {
23
- const config = await getConfig<types.StudioConfig>()
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 app = new Hono(config.honoConfig)
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.route('/', await fileBasedRouter('./pages'))
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 { createStudio, query }
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.4",
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/db": "0.0.0-beta.1",
11
- "@alstar/ui": "0.0.0-beta.1"
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
@@ -15,6 +15,8 @@ body {
15
15
 
16
16
  > main {
17
17
  padding: 40px;
18
+ height: 100vh;
19
+ overflow: auto;
18
20
 
19
21
  section {
20
22
  max-width: 900px;
@@ -90,4 +92,4 @@ div.ink-mde {
90
92
  .disclamer {
91
93
  display: flex;
92
94
  justify-content: center;
93
- }
95
+ }
@@ -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
- import { db } from '@alstar/db'
2
- import { sql } from '../utils/sql.ts'
3
- import { type DBBlock } from '../types.ts'
4
- import { buildBlockTree } from '../utils/buildBlocksTree.ts'
5
-
6
- export const blocks = (options: {
7
- parent: string | number | null
8
- }): DBBlock[] | null => {
9
- const q =
10
- options.parent === null
11
- ? sql`
12
- select
13
- *
14
- from
15
- blocks
16
- where
17
- parent_block_id is null;
18
- `
19
- : sql`
20
- select
21
- *
22
- from
23
- blocks
24
- where
25
- parent_block_id = ?;
26
- `
27
-
28
- const transaction = db.database.prepare(q)
29
-
30
- if (options.parent === null) {
31
- return transaction.all() as unknown as DBBlock[]
32
- } else {
33
- return transaction.all(options.parent) as unknown as DBBlock[]
34
- }
35
- }
36
-
37
- export const block = (
38
- query: Record<string, string | null>,
39
- options?: { recursive: boolean },
40
- ): DBBlock | null => {
41
- const str = Object.keys(query)
42
- .map((key) => `${key.replace('parent', 'parent_block_id')} = ?`)
43
- .join(' AND ')
44
-
45
- const q = options?.recursive
46
- ? sql`
47
- with recursive
48
- block_hierarchy as (
49
- -- Anchor member: the root block, depth = 0
50
- select
51
- b.*,
52
- 0 as depth
53
- from
54
- blocks b
55
- where
56
- b.id = ? -- Replace 5 with your root block ID
57
- union all
58
- -- Recursive member: find children and increment depth
59
- select
60
- b.*,
61
- bh.depth + 1 as depth
62
- from
63
- blocks b
64
- inner join block_hierarchy bh on b.parent_block_id = bh.id
65
- )
66
- select
67
- *
68
- from
69
- block_hierarchy
70
- order by
71
- sort_order;
72
- `
73
- : sql`
74
- select
75
- *
76
- from
77
- blocks
78
- where
79
- ${str};
80
- `
81
-
82
- const transaction = db.database.prepare(q)
83
-
84
- try {
85
- if (options?.recursive) {
86
- const res = transaction.all(query.id) as unknown as any[]
87
- return (buildBlockTree(res) as any) || null
88
- }
89
- return (
90
- (transaction.get(...Object.values(query)) as unknown as DBBlock) || null
91
- )
92
- } catch (error) {
93
- console.log('error')
94
- return null
95
- }
96
- }
97
-
98
- export const query = { blocks, block }
1
+ export * as query from './block.ts'
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 HonoOptions } from "hono/hono-base"
2
- import { type BlankEnv } from "hono/types"
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: Context<{ Bindings: HttpBindings }, string, BlankInput>,
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
- const dirs = await fs.readdir(root, { recursive: true })
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(
@@ -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
+ }