@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.
Files changed (69) hide show
  1. package/api/api-key.ts +69 -0
  2. package/api/auth.ts +66 -0
  3. package/api/backup.ts +40 -0
  4. package/api/block.ts +131 -66
  5. package/api/index.ts +19 -1
  6. package/api/mcp.ts +50 -0
  7. package/components/AdminPanel.ts +87 -0
  8. package/components/Backup.ts +13 -0
  9. package/components/BlockFieldRenderer.ts +125 -0
  10. package/components/BlockRenderer.ts +22 -0
  11. package/components/Entries.ts +20 -12
  12. package/components/Entry.ts +13 -21
  13. package/components/FieldRenderer.ts +35 -0
  14. package/components/Render.ts +46 -0
  15. package/components/Settings.ts +104 -0
  16. package/components/SiteLayout.ts +61 -0
  17. package/components/Users.ts +46 -0
  18. package/components/fields/Markdown.ts +44 -0
  19. package/components/fields/Slug.ts +113 -0
  20. package/components/fields/Text.ts +42 -0
  21. package/components/fields/index.ts +7 -0
  22. package/components/icons.ts +136 -7
  23. package/index.ts +94 -34
  24. package/package.json +10 -7
  25. package/pages/entry/[id].ts +15 -0
  26. package/pages/error.ts +14 -0
  27. package/{components → pages}/index.ts +7 -4
  28. package/pages/login.ts +21 -0
  29. package/pages/register.ts +33 -0
  30. package/pages/settings.ts +8 -0
  31. package/public/studio/css/admin-panel.css +103 -0
  32. package/public/studio/css/blocks-field.css +53 -0
  33. package/public/studio/css/settings.css +28 -0
  34. package/public/studio/js/markdown-editor.js +34 -0
  35. package/public/studio/js/sortable-list.js +50 -0
  36. package/public/studio/main.css +166 -0
  37. package/public/studio/main.js +21 -0
  38. package/queries/block-2.ts +339 -0
  39. package/queries/block-with-children.ts +74 -0
  40. package/queries/block.ts +289 -0
  41. package/queries/db-types.ts +15 -0
  42. package/queries/getBlockTrees-2.ts +71 -0
  43. package/queries/getBlockTrees.ts +316 -0
  44. package/queries/getBlocks.ts +214 -0
  45. package/queries/index.ts +2 -98
  46. package/queries/structure-types.ts +97 -0
  47. package/readme.md +205 -0
  48. package/schema.sql +18 -0
  49. package/schemas.ts +23 -52
  50. package/types.ts +144 -5
  51. package/utils/auth.ts +54 -0
  52. package/utils/buildBlocksTree.ts +4 -4
  53. package/utils/create-hash.ts +9 -0
  54. package/utils/define.ts +39 -0
  55. package/utils/file-based-router.ts +11 -2
  56. package/utils/get-config.ts +8 -9
  57. package/utils/get-or-create-row.ts +41 -0
  58. package/utils/html.ts +247 -0
  59. package/utils/startup-log.ts +19 -0
  60. package/components/AdminPanel/AdminPanel.css +0 -59
  61. package/components/AdminPanel/AdminPanel.ts +0 -57
  62. package/components/Block.ts +0 -116
  63. package/components/Entry.css +0 -7
  64. package/components/Field.ts +0 -164
  65. package/components/Fields.ts +0 -43
  66. package/components/layout.ts +0 -53
  67. package/public/main.css +0 -92
  68. package/public/main.js +0 -43
  69. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
package/api/api-key.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { type HttpBindings } from '@hono/node-server'
2
+ import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
3
+ import { Hono } from 'hono'
4
+ import { streamSSE } from 'hono/streaming'
5
+ import { db } from '@alstar/db'
6
+ import crypto from 'node:crypto'
7
+
8
+ import { stripNewlines } from '../utils/strip-newlines.ts'
9
+ import { sql } from '../utils/sql.ts'
10
+ import Settings from '../components/Settings.ts'
11
+ import { createHash } from '../utils/create-hash.ts'
12
+
13
+ const app = new Hono<{ Bindings: HttpBindings }>()
14
+
15
+ app.post('/api-key', async (c) => {
16
+ return streamSSE(c, async (stream) => {
17
+ const formData = await c.req.formData()
18
+ const data = Object.fromEntries(formData.entries())
19
+
20
+ if (!data) return
21
+
22
+ const apiKey = crypto.randomUUID()
23
+
24
+ const hash = createHash(apiKey)
25
+
26
+ const xs = (length: number) => '*'.repeat(length)
27
+
28
+ db.insertInto('api_keys', {
29
+ name: data.name?.toString(),
30
+ value: hash,
31
+ hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
32
+ })
33
+
34
+ await stream.writeSSE({
35
+ event: 'datastar-patch-signals',
36
+ data: `signals { apiKey: '${apiKey}', name: '' }`,
37
+ })
38
+
39
+ await stream.writeSSE({
40
+ event: 'datastar-patch-elements',
41
+ data: `elements ${stripNewlines(Settings())}`,
42
+ })
43
+ })
44
+ })
45
+
46
+ app.delete('/api-key', async (c) => {
47
+ return streamSSE(c, async (stream) => {
48
+ const formData = await c.req.formData()
49
+
50
+ const value = formData.get('value')?.toString()
51
+
52
+ if (!value) return
53
+
54
+ db.database
55
+ .prepare(sql`
56
+ delete from api_keys
57
+ where
58
+ value = ?
59
+ `)
60
+ .run(value)
61
+
62
+ await stream.writeSSE({
63
+ event: 'datastar-patch-elements',
64
+ data: `elements ${stripNewlines(Settings())}`,
65
+ })
66
+ })
67
+ })
68
+
69
+ export const apiKeyRoutes = app
package/api/auth.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { type HttpBindings } from '@hono/node-server'
2
+ import { Hono } from 'hono'
3
+ import { createHash } from '../utils/create-hash.ts'
4
+ import { db } from '@alstar/db'
5
+ import { streamSSE } from 'hono/streaming'
6
+ import { sql } from '../utils/sql.ts'
7
+ import { setCookie } from 'hono/cookie'
8
+
9
+ const app = new Hono<{ Bindings: HttpBindings }>()
10
+
11
+ app.post('/register', async (c) => {
12
+ return streamSSE(c, async (stream) => {
13
+ const formData = await c.req.formData()
14
+ const data = Object.fromEntries(formData.entries())
15
+
16
+ if (!data || !data.email || !data.password) return
17
+
18
+ const hash = createHash(data.password.toString())
19
+
20
+ db.insertInto('users', {
21
+ email: data.email?.toString(),
22
+ hash: hash,
23
+ })
24
+
25
+ await stream.writeSSE({
26
+ event: 'datastar-patch-signals',
27
+ data: `signals { status: 200, message: 'User "${data.email}" created successfully' }`,
28
+ })
29
+ })
30
+ })
31
+
32
+ app.post('/login', async (c) => {
33
+ const formData = await c.req.formData()
34
+ const data = Object.fromEntries(formData.entries())
35
+
36
+ if (!data || !data.email || !data.password) return
37
+
38
+ const user = db.database
39
+ .prepare(sql`
40
+ select
41
+ *
42
+ from
43
+ users
44
+ where
45
+ email = ?
46
+ `)
47
+ .get(data.email.toString())
48
+
49
+ if (!user) {
50
+ return c.json({ status: 404, message: 'No user with that email' })
51
+ }
52
+
53
+ const passwordHash = createHash(data.password.toString())
54
+
55
+ if (passwordHash !== user.hash) {
56
+ return c.json({ status: 401, message: 'Wrong password' })
57
+ }
58
+
59
+ setCookie(c, 'login', 'yes')
60
+
61
+ return c.redirect('/studio')
62
+
63
+ // return c.json({ status: 200, message: 'Logged in!' })
64
+ })
65
+
66
+ export const authRoutes = app
package/api/backup.ts ADDED
@@ -0,0 +1,40 @@
1
+ import fsp from 'node:fs/promises'
2
+ import { backup } from 'node:sqlite'
3
+
4
+ import { Hono } from 'hono'
5
+ import { type HttpBindings } from '@hono/node-server'
6
+ import { streamSSE } from 'hono/streaming'
7
+ import { db } from '@alstar/db'
8
+
9
+ const app = new Hono<{ Bindings: HttpBindings }>()
10
+
11
+ app.post('/backup', async (c) => {
12
+ const date = new Date()
13
+ const name = `./backups/backup-${date.toISOString()}.db`
14
+
15
+ try {
16
+ fsp.mkdir('./backups', { recursive: true })
17
+
18
+ await backup(db.database, name)
19
+
20
+ console.log('Backup')
21
+
22
+ return streamSSE(c, async (stream) => {
23
+ await stream.writeSSE({
24
+ event: 'datastar-patch-signals',
25
+ data: `signals { status: 200, message: '${name} created' }`,
26
+ })
27
+ })
28
+ } catch (error) {
29
+ console.log(error)
30
+
31
+ return streamSSE(c, async (stream) => {
32
+ await stream.writeSSE({
33
+ event: 'datastar-patch-signals',
34
+ data: `signals { status: 500, message: '${name} failed' }`,
35
+ })
36
+ })
37
+ }
38
+ })
39
+
40
+ export const backupRoutes = app
package/api/block.ts CHANGED
@@ -1,97 +1,162 @@
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'
6
6
  import { sql } from '../utils/sql.ts'
7
7
  import { type Structure } from '../types.ts'
8
8
  import { db } from '@alstar/db'
9
- import Entries from '../components/Entries.ts'
10
9
  import Entry from '../components/Entry.ts'
10
+ import {
11
+ blockWithChildren,
12
+ deleteBlockWithChildren,
13
+ } from '../queries/block-with-children.ts'
14
+ import AdminPanel from '../components/AdminPanel.ts'
15
+ import { query } from '../queries/index.ts'
16
+ import { studioStructure } from '../index.ts'
11
17
 
12
- export const sectionRoutes = (structure: Structure) => {
13
- const app = new Hono<{ Bindings: HttpBindings }>()
18
+ const app = new Hono<{ Bindings: HttpBindings }>()
14
19
 
15
- app.post('/block', async (c) => {
16
- return streamSSE(c, async (stream) => {
17
- const formData = await c.req.formData()
18
- const data = Object.fromEntries(formData.entries())
20
+ app.post('/block', async (c) => {
21
+ return streamSSE(c, async (stream) => {
22
+ const formData = await c.req.formData()
23
+ const data = Object.fromEntries(formData.entries())
19
24
 
20
- const row = structure.find((block) => block.type === data.type)
25
+ const row = studioStructure[data.name?.toString()]
21
26
 
22
- if (!row) return
27
+ if (!row) return
23
28
 
24
- db.insertInto('blocks', {
25
- name: row.name?.toString(),
26
- label: row.label?.toString(),
27
- type: row.type?.toString(),
28
- })
29
+ db.insertInto('blocks', {
30
+ name: data.name?.toString(),
31
+ label: row.label,
32
+ type: row.type,
33
+ })
29
34
 
30
- await stream.writeSSE({
31
- event: 'datastar-patch-elements',
32
- data: `elements ${stripNewlines(Entries())}`,
33
- })
35
+ await stream.writeSSE({
36
+ event: 'datastar-patch-elements',
37
+ data: `elements ${stripNewlines(AdminPanel())}`,
34
38
  })
35
39
  })
40
+ })
41
+
42
+ app.post('/new-block', async (c) => {
43
+ return streamSSE(c, async (stream) => {
44
+ const formData = await c.req.formData()
45
+ const data = Object.fromEntries(formData)
46
+
47
+ db.insertInto('blocks', {
48
+ type: data.type.toString(),
49
+ name: data.name.toString(),
50
+ label: data.label.toString(),
51
+ parent_id: data.parent_id.toString(),
52
+ sort_order: data.sort_order.toString(),
53
+ })
36
54
 
37
- app.post('/new-block', async (c) => {
38
- return streamSSE(c, async (stream) => {
39
- const formData = await c.req.formData()
40
- const newBlock = formData.get('block')?.toString().split(';')
41
- const columns = newBlock?.map((field) => field.split(':')) as string[][]
42
- const data = Object.fromEntries(columns)
55
+ await stream.writeSSE({
56
+ event: 'datastar-patch-elements',
57
+ data: `elements ${stripNewlines(Entry({ entryId: parseInt(data.entry_id.toString()) }))}`,
58
+ })
59
+ })
60
+ })
61
+
62
+ app.patch('/block', async (c) => {
63
+ return streamSSE(c, async (stream) => {
64
+ const formData = await c.req.formData()
65
+
66
+ const id = formData.get('id')?.toString()
67
+ const value = formData.get('value')?.toString()
68
+ const name = formData.get('name')?.toString()
69
+ const entryId = formData.get('entryId')?.toString()
70
+ const parentId = formData.get('parentId')?.toString()
71
+ // const sortOrder = formData.get('sort_order')?.toString()
72
+
73
+ if (!id || !value) return
74
+
75
+ const transaction = db.database.prepare(sql`
76
+ update blocks
77
+ set
78
+ value = ?
79
+ where
80
+ id = ?;
81
+ `)
82
+
83
+ transaction.run(value, id)
84
+
85
+ if (entryId === parentId && name?.toString() === 'title') {
86
+ const rootBlock = query.block({
87
+ id: parentId?.toString() || null,
88
+ })
43
89
 
44
- const parent_block_id = formData?.get('parent_block_id')?.toString()
45
- const sort_order = formData?.get('sort_order')?.toString()
90
+ if (rootBlock.type !== 'single') {
91
+ await stream.writeSSE({
92
+ event: 'datastar-patch-elements',
93
+ data: `elements <a href="/studio/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
94
+ })
95
+ }
96
+ }
97
+ })
98
+ })
46
99
 
47
- if (!parent_block_id || !sort_order) return
100
+ app.patch('/value', async (c) => {
101
+ const body = await c.req.json()
48
102
 
49
- db.insertInto('blocks', {
50
- type: data.type,
51
- name: data.name,
52
- label: data.label,
53
- parent_block_id,
54
- sort_order,
55
- })
103
+ const transaction = db.database.prepare(sql`
104
+ update blocks
105
+ set
106
+ value = ?
107
+ where
108
+ id = ?;
109
+ `)
110
+
111
+ transaction.run(body.value, body.id)
112
+
113
+ return c.json({ status: 200, message: 'success' })
114
+ })
56
115
 
116
+ app.delete('/block', async (c) => {
117
+ return streamSSE(c, async (stream) => {
118
+ const formData = await c.req.formData()
119
+
120
+ const id = formData.get('id')?.toString()
121
+ const entryId = formData.get('entry_id')?.toString()
122
+
123
+ if (!id) return
124
+
125
+ const transaction = db.database.prepare(deleteBlockWithChildren)
126
+
127
+ transaction.all(id)
128
+
129
+ if (entryId) {
57
130
  await stream.writeSSE({
58
131
  event: 'datastar-patch-elements',
59
- data: `elements ${stripNewlines(Entry({ entryId: data.entry_id, structure }))}`,
132
+ data: `elements ${stripNewlines(Entry({ entryId: parseInt(entryId.toString()) }))}`,
60
133
  })
61
- })
134
+ } else {
135
+ await stream.writeSSE({
136
+ event: 'datastar-patch-elements',
137
+ data: `elements ${stripNewlines(AdminPanel())}`,
138
+ })
139
+ }
62
140
  })
141
+ })
63
142
 
64
- app.patch('/block', async (c) => {
65
- return streamSSE(c, async (stream) => {
66
- const formData = await c.req.formData()
67
-
68
- const id = formData.get('id')?.toString()
69
- const value = formData.get('value')?.toString()
70
- const entryId = formData.get('entryId')?.toString()
71
- const parentId = formData.get('parentId')?.toString()
72
- const sortOrder = formData.get('sort_order')?.toString()
143
+ app.post('/sort-order', async (c) => {
144
+ const id = c.req.query('id')
145
+ const sortOrder = c.req.query('sort-order')
73
146
 
74
- if (!id || !value || !sortOrder) return
147
+ if (!id || !sortOrder) return
75
148
 
76
- const transaction = db.database.prepare(sql`
77
- update blocks
78
- set
79
- value = ?
80
- where
81
- id = ?
82
- and sort_order = ?;
83
- `)
149
+ const transaction = db.database.prepare(sql`
150
+ update blocks
151
+ set
152
+ sort_order = ?
153
+ where
154
+ id = ?
155
+ `)
84
156
 
85
- transaction.run(value, id, sortOrder)
157
+ transaction.run(sortOrder, id)
86
158
 
87
- if (entryId === parentId) {
88
- await stream.writeSSE({
89
- event: 'datastar-patch-elements',
90
- data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
91
- })
92
- }
93
- })
94
- })
159
+ return c.json({ status: 200, message: 'success' })
160
+ })
95
161
 
96
- return app
97
- }
162
+ export const blockRoutes = app
package/api/index.ts CHANGED
@@ -1,2 +1,20 @@
1
- export * from './block.ts'
1
+ import { Hono } from 'hono'
2
2
 
3
+ import { blockRoutes } from './block.ts'
4
+ import { apiKeyRoutes } from './api-key.ts'
5
+ import { backupRoutes } from './backup.ts'
6
+ import { authRoutes } from './auth.ts'
7
+ import { fieldRoutes } from '../components/fields/index.ts'
8
+
9
+ const routes = new Hono()
10
+
11
+ routes.route('/', blockRoutes)
12
+ routes.route('/', apiKeyRoutes)
13
+ routes.route('/', backupRoutes)
14
+ routes.route('/auth', authRoutes)
15
+
16
+ fieldRoutes.forEach((fieldRoute) => {
17
+ routes.route('/field', fieldRoute)
18
+ })
19
+
20
+ export const apiRoutes = routes
package/api/mcp.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { type HttpBindings } from '@hono/node-server'
2
+ import { Hono } from 'hono'
3
+ import { sql } from '../utils/sql.ts'
4
+ import { db } from '@alstar/db'
5
+ import { bearerAuth } from 'hono/bearer-auth'
6
+ import crypto from 'node:crypto'
7
+
8
+ const app = new Hono<{ Bindings: HttpBindings }>()
9
+
10
+ app.use(
11
+ '/*',
12
+ bearerAuth({
13
+ verifyToken: async (token, c) => {
14
+ const hash = crypto.createHash('sha256')
15
+
16
+ hash.update(token)
17
+
18
+ const digest = hash.digest().toString('base64')
19
+
20
+ const exists = db.database
21
+ .prepare(sql`
22
+ select
23
+ value
24
+ from
25
+ api_keys
26
+ where
27
+ value = ?
28
+ `)
29
+ .get(digest)
30
+
31
+ return !!exists
32
+ },
33
+ }),
34
+ )
35
+
36
+ app.get('/entry', async (c) => {
37
+ return c.json({
38
+ status: 'success',
39
+ message: 'Response from MCP server!',
40
+ })
41
+ })
42
+
43
+ app.post('/entry', async (c) => {
44
+ return c.json({
45
+ status: 'success',
46
+ message: 'New entry created!',
47
+ })
48
+ })
49
+
50
+ export const mcpRoutes = app
@@ -0,0 +1,87 @@
1
+ import { html } from 'hono/html'
2
+ import { logo } from './icons.ts'
3
+ import Entries from './Entries.ts'
4
+ import * as icons from './icons.ts'
5
+ import { studioStructure } from '../index.ts'
6
+ import { getOrCreateRow } from '../utils/get-or-create-row.ts'
7
+
8
+ export default () => {
9
+ const entries = Object.entries(studioStructure)
10
+
11
+ return html`
12
+ <div class="admin-panel" id="admin_panel">
13
+ <h1>
14
+ <a href="/studio" aria-label="Go to dashboard"> ${logo} </a>
15
+ </h1>
16
+
17
+ <aside style="width: 100%;">
18
+ ${entries.map(([name, block]) => {
19
+ if (block.type === 'single') {
20
+ const data = getOrCreateRow({ parentId: null, name, field: block })
21
+
22
+ return html`
23
+ <section id="entries">
24
+ <ul>
25
+ <li>
26
+ <a
27
+ href="/studio/entry/${data.id}"
28
+ id="block_link_${data.id}"
29
+ >
30
+ ${block.label}
31
+ </a>
32
+ </li>
33
+ </ul>
34
+ </section>
35
+ `
36
+ }
37
+
38
+ return html`
39
+ <form
40
+ data-on-submit="@post('/studio/api/block', { contentType: 'form' })"
41
+ style="display: flex; align-items: center; gap: 1rem;"
42
+ >
43
+ <input type="hidden" name="name" value="${name}" />
44
+ <button
45
+ class="ghost"
46
+ style="padding: 10px; margin: 0 -13px; display: flex;"
47
+ data-tooltip="New ${block.label}"
48
+ data-placement="right"
49
+ >
50
+ ${icons.newDocument}
51
+ </button>
52
+ <p style="user-select: none;"><small>${block.label}</small></p>
53
+ </form>
54
+
55
+ ${Entries({ name })}
56
+ `
57
+ })}
58
+ </aside>
59
+
60
+ <footer>
61
+ <a
62
+ role="button"
63
+ href="/studio/settings"
64
+ class="ghost"
65
+ style="padding: 10px; margin: 0 -13px; display: flex;"
66
+ data-tooltip="Settings"
67
+ data-placement="right"
68
+ aria-label="Settings"
69
+ >
70
+ ${icons.cog}
71
+ </a>
72
+
73
+ <a
74
+ role="button"
75
+ href="/"
76
+ class="ghost"
77
+ style="padding: 10px; margin: 0 -13px; display: flex;"
78
+ data-tooltip="Leave Studio"
79
+ data-placement="right"
80
+ aria-label="Leave"
81
+ >
82
+ ${icons.leave}
83
+ </a>
84
+ </footer>
85
+ </div>
86
+ `
87
+ }
@@ -0,0 +1,13 @@
1
+ import { html } from 'hono/html'
2
+
3
+ export default () => {
4
+ return html`<article data-signals="{ status: null, message: '' }">
5
+ <header>Backup</header>
6
+ <form
7
+ data-on-submit="@post('/studio/api/backup', { contentType: 'form' })"
8
+ >
9
+ <button type="submit">Backup database</button>
10
+ <p data-style-color="$status === 200 ? 'green' : 'red'" data-text="$message || '&nbsp;'"></p>
11
+ </form>
12
+ </article>`
13
+ }