@alstar/studio 0.0.0-beta.10 → 0.0.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/api/api-key.ts +37 -49
  2. package/api/auth.ts +64 -0
  3. package/api/backup.ts +39 -28
  4. package/api/block.ts +80 -119
  5. package/api/index.ts +17 -10
  6. package/api/mcp.ts +39 -42
  7. package/components/AdminPanel.ts +29 -4
  8. package/components/BlockFieldRenderer.ts +20 -4
  9. package/components/Entries.ts +7 -3
  10. package/components/FieldRenderer.ts +1 -1
  11. package/components/Settings.ts +5 -93
  12. package/components/SiteLayout.ts +11 -9
  13. package/components/fields/Markdown.ts +1 -1
  14. package/components/fields/Slug.ts +122 -0
  15. package/components/fields/Text.ts +20 -4
  16. package/components/fields/index.ts +4 -1
  17. package/components/icons.ts +55 -0
  18. package/components/settings/ApiKeys.ts +122 -0
  19. package/components/settings/Backup.ts +82 -0
  20. package/components/settings/Users.ts +63 -0
  21. package/index.ts +51 -11
  22. package/package.json +6 -3
  23. package/pages/entry/[id].ts +1 -3
  24. package/pages/error.ts +14 -0
  25. package/pages/index.ts +1 -1
  26. package/pages/login.ts +21 -0
  27. package/pages/register.ts +33 -0
  28. package/pages/settings.ts +1 -3
  29. package/public/studio/css/settings.css +7 -15
  30. package/public/studio/js/markdown-editor.js +1 -1
  31. package/public/studio/js/sortable-list.js +1 -1
  32. package/public/studio/main.css +5 -1
  33. package/public/studio/main.js +13 -0
  34. package/queries/block-2.ts +339 -0
  35. package/queries/getBlockTrees-2.ts +71 -0
  36. package/queries/index.ts +1 -1
  37. package/readme.md +2 -2
  38. package/schema.sql +18 -0
  39. package/schemas.ts +11 -1
  40. package/types.ts +11 -0
  41. package/utils/auth.ts +54 -0
  42. package/utils/create-hash.ts +9 -0
  43. package/utils/define.ts +1 -3
  44. package/utils/renderSSE.ts +27 -0
  45. package/utils/slugify.ts +3 -1
  46. package/utils/startup-log.ts +15 -6
  47. package/components/Backup.ts +0 -10
  48. package/utils/build-structure-path.ts +0 -43
package/api/api-key.ts CHANGED
@@ -4,70 +4,58 @@ import { Hono } from 'hono'
4
4
  import { streamSSE } from 'hono/streaming'
5
5
  import { db } from '@alstar/db'
6
6
  import crypto from 'node:crypto'
7
-
8
- import { stripNewlines } from '../utils/strip-newlines.ts'
9
7
  import { sql } from '../utils/sql.ts'
10
- import Settings from '../components/Settings.ts'
11
-
12
- export default () => {
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())
8
+ import { createHash } from '../utils/create-hash.ts'
9
+ import { renderSSE } from '../utils/renderSSE.ts'
19
10
 
20
- if (!data) return
11
+ const app = new Hono<{ Bindings: HttpBindings }>()
21
12
 
22
- const apiKey = crypto.randomUUID()
23
- const hash = crypto.createHash('sha256')
13
+ app.post('/api-key', async (c) => {
14
+ return streamSSE(c, async (stream) => {
15
+ const formData = await c.req.formData()
16
+ const data = Object.fromEntries(formData.entries())
24
17
 
25
- hash.update(apiKey)
18
+ if (!data) return
26
19
 
27
- const digest = hash.digest().toString('base64')
20
+ const apiKey = crypto.randomUUID()
28
21
 
29
- const xs = (length: number) => '*'.repeat(length)
22
+ const hash = createHash(apiKey)
30
23
 
31
- db.insertInto('api_keys', {
32
- name: data.name?.toString(),
33
- value: digest,
34
- hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
35
- })
24
+ const xs = (length: number) => '*'.repeat(length)
36
25
 
37
- await stream.writeSSE({
38
- event: 'datastar-patch-signals',
39
- data: `signals { apiKey: '${apiKey}', name: '' }`,
40
- })
26
+ db.insertInto('api_keys', {
27
+ name: data.name?.toString(),
28
+ value: hash,
29
+ hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
30
+ })
41
31
 
42
- await stream.writeSSE({
43
- event: 'datastar-patch-elements',
44
- data: `elements ${stripNewlines(Settings())}`,
45
- })
32
+ await stream.writeSSE({
33
+ event: 'datastar-patch-signals',
34
+ data: `signals { apiKey: '${apiKey}', name: '' }`,
46
35
  })
36
+
37
+ await renderSSE(stream, c)
47
38
  })
39
+ })
48
40
 
49
- app.delete('/api-key', async (c) => {
50
- return streamSSE(c, async (stream) => {
51
- const formData = await c.req.formData()
41
+ app.delete('/api-key', async (c) => {
42
+ return streamSSE(c, async (stream) => {
43
+ const formData = await c.req.formData()
52
44
 
53
- const value = formData.get('value')?.toString()
45
+ const value = formData.get('value')?.toString()
54
46
 
55
- if (!value) return
47
+ if (!value) return
56
48
 
57
- db.database
58
- .prepare(sql`
59
- delete from api_keys
60
- where
61
- value = ?
62
- `)
63
- .run(value)
49
+ db.database
50
+ .prepare(sql`
51
+ delete from api_keys
52
+ where
53
+ value = ?
54
+ `)
55
+ .run(value)
64
56
 
65
- await stream.writeSSE({
66
- event: 'datastar-patch-elements',
67
- data: `elements ${stripNewlines(Settings())}`,
68
- })
69
- })
57
+ await renderSSE(stream, c)
70
58
  })
59
+ })
71
60
 
72
- return app
73
- }
61
+ export const apiKeyRoutes = app
package/api/auth.ts ADDED
@@ -0,0 +1,64 @@
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
+
64
+ export const authRoutes = app
package/api/backup.ts CHANGED
@@ -1,38 +1,49 @@
1
- import { type HttpBindings } from '@hono/node-server'
1
+ import fsp from 'node:fs/promises'
2
+ import { backup } from 'node:sqlite'
3
+
2
4
  import { Hono } from 'hono'
5
+ import { type HttpBindings } from '@hono/node-server'
3
6
  import { streamSSE } from 'hono/streaming'
4
- import { DatabaseSync } from 'node:sqlite'
5
-
6
- import { stripNewlines } from '../utils/strip-newlines.ts'
7
7
  import { db } from '@alstar/db'
8
+ import { stripNewlines } from '../utils/strip-newlines.ts'
9
+ import Settings from '../components/Settings.ts'
10
+ import path from 'node:path'
11
+ import { renderSSE } from '../utils/renderSSE.ts'
12
+
13
+ const app = new Hono<{ Bindings: HttpBindings }>()
14
+
15
+ app.post('/backup', async (c) => {
16
+ const date = new Date()
17
+ const name = `./backups/backup-${date.toISOString()}.db`
18
+
19
+ try {
20
+ fsp.mkdir('./backups', { recursive: true })
8
21
 
9
- export default () => {
10
- const app = new Hono<{ Bindings: HttpBindings }>()
22
+ await backup(db.database, name)
11
23
 
12
- app.post('/backup', async (c) => {
13
- // const totalPagesTransferred = await backup(db.database, './backups/backup.db', {
14
- // rate: 1, // Copy one page at a time.
15
- // progress: ({ totalPages, remainingPages }) => {
16
- // console.log('Backup in progress', { totalPages, remainingPages })
17
- // },
18
- // })
24
+ return streamSSE(c, async (stream) => await renderSSE(stream, c))
25
+ } catch (error) {
26
+ console.log(error)
27
+ return c.json({ status: 500, message: 'Something went wrong' })
28
+ }
29
+ })
19
30
 
20
- // console.log('Backup completed', totalPagesTransferred)
31
+ app.delete('/backup', async (c) => {
32
+ const formData = await c.req.formData()
33
+ const data = Object.fromEntries(formData.entries())
21
34
 
22
- return c.html('good')
35
+ if (!data.filename) {
36
+ return c.json({ status: 404, message: 'Need a filename to remove' })
37
+ }
23
38
 
24
- // return streamSSE(c, async (stream) => {
25
- // await stream.writeSSE({
26
- // event: 'datastar-patch-signals',
27
- // data: `signals {}`,
28
- // })
39
+ try {
40
+ await fsp.rm(path.join('./backups', data.filename.toString()))
29
41
 
30
- // await stream.writeSSE({
31
- // event: 'datastar-patch-elements',
32
- // data: `elements ${stripNewlines(Settings())}`,
33
- // })
34
- // })
35
- })
42
+ return streamSSE(c, async (stream) => await renderSSE(stream, c))
43
+ } catch (error) {
44
+ console.log(error)
45
+ return c.json({ status: 500, message: 'Something went wrong' })
46
+ }
47
+ })
36
48
 
37
- return app
38
- }
49
+ export const backupRoutes = app
package/api/block.ts CHANGED
@@ -2,103 +2,49 @@ 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
- import { stripNewlines } from '../utils/strip-newlines.ts'
6
5
  import { sql } from '../utils/sql.ts'
7
- import { type Structure } from '../types.ts'
8
6
  import { db } from '@alstar/db'
9
- import Entry from '../components/Entry.ts'
10
7
  import {
11
8
  blockWithChildren,
12
9
  deleteBlockWithChildren,
13
10
  } from '../queries/block-with-children.ts'
14
- import AdminPanel from '../components/AdminPanel.ts'
15
11
  import { query } from '../queries/index.ts'
12
+ import { renderSSE } from '../utils/renderSSE.ts'
16
13
 
17
- export default (structure: Structure) => {
18
- const app = new Hono<{ Bindings: HttpBindings }>()
14
+ const app = new Hono<{ Bindings: HttpBindings }>()
19
15
 
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())
16
+ app.post('/block', async (c) => {
17
+ return streamSSE(c, async (stream) => {
18
+ const formData = await c.req.formData()
19
+ const data = Object.fromEntries(formData.entries())
24
20
 
25
- const row = structure[data.name?.toString()]
21
+ const definedData = JSON.parse(
22
+ JSON.stringify({
23
+ type: data.type?.toString(),
24
+ name: data.name?.toString(),
25
+ label: data.label?.toString(),
26
+ parent_id: data.parent_id?.toString(),
27
+ sort_order: data.sort_order?.toString(),
28
+ }),
29
+ )
26
30
 
27
- if (!row) return
31
+ db.insertInto('blocks', definedData)
28
32
 
29
- db.insertInto('blocks', {
30
- name: data.name?.toString(),
31
- label: row.label,
32
- type: row.type,
33
- })
34
-
35
- await stream.writeSSE({
36
- event: 'datastar-patch-elements',
37
- data: `elements ${stripNewlines(AdminPanel())}`,
38
- })
39
- })
33
+ await renderSSE(stream, c)
40
34
  })
35
+ })
41
36
 
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
- })
54
-
55
- await stream.writeSSE({
56
- event: 'datastar-patch-elements',
57
- data: `elements ${stripNewlines(Entry({ entryId: parseInt(data.entry_id.toString()) }))}`,
58
- })
59
- })
60
- })
37
+ app.patch('/block', async (c) => {
38
+ return streamSSE(c, async (stream) => {
39
+ const formData = await c.req.formData()
61
40
 
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
- })
89
-
90
- if (rootBlock.type !== 'single') {
91
- await stream.writeSSE({
92
- event: 'datastar-patch-elements',
93
- data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
94
- })
95
- }
96
- }
97
- })
98
- })
41
+ const id = formData.get('id')?.toString()
42
+ const value = formData.get('value')?.toString()
43
+ // const name = formData.get('name')?.toString()
44
+ // const entryId = formData.get('entryId')?.toString()
45
+ // const parentId = formData.get('parentId')?.toString()
99
46
 
100
- app.patch('/value', async (c) => {
101
- const body = await c.req.json()
47
+ if (!id || !value) return
102
48
 
103
49
  const transaction = db.database.prepare(sql`
104
50
  update blocks
@@ -108,56 +54,71 @@ export default (structure: Structure) => {
108
54
  id = ?;
109
55
  `)
110
56
 
111
- transaction.run(body.value, body.id)
57
+ transaction.run(value, id)
112
58
 
113
- return c.json({ status: 200, message: 'success' })
59
+ await renderSSE(stream, c)
60
+
61
+ // if (entryId === parentId && name?.toString() === 'title') {
62
+ // const rootBlock = query.block({
63
+ // id: parentId?.toString() || null,
64
+ // })
65
+
66
+ // if (rootBlock.type !== 'single') {
67
+ // await stream.writeSSE({
68
+ // event: 'datastar-patch-elements',
69
+ // data: `elements <a href="/studio/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
70
+ // })
71
+ // }
72
+ // }
114
73
  })
74
+ })
75
+
76
+ app.patch('/value', async (c) => {
77
+ const body = await c.req.json()
115
78
 
116
- app.delete('/block', async (c) => {
117
- return streamSSE(c, async (stream) => {
118
- const formData = await c.req.formData()
79
+ const transaction = db.database.prepare(sql`
80
+ update blocks
81
+ set
82
+ value = ?
83
+ where
84
+ id = ?;
85
+ `)
119
86
 
120
- const id = formData.get('id')?.toString()
121
- const entryId = formData.get('entry_id')?.toString()
87
+ transaction.run(body.value, body.id)
122
88
 
123
- if (!id) return
89
+ return c.json({ status: 200, message: 'success' })
90
+ })
124
91
 
125
- const transaction = db.database.prepare(deleteBlockWithChildren)
92
+ app.delete('/block', async (c) => {
93
+ return streamSSE(c, async (stream) => {
94
+ const formData = await c.req.formData()
95
+ const id = formData.get('id')?.toString()
126
96
 
127
- transaction.all(id)
97
+ if (!id) return
128
98
 
129
- if (entryId) {
130
- await stream.writeSSE({
131
- event: 'datastar-patch-elements',
132
- data: `elements ${stripNewlines(Entry({ entryId: parseInt(entryId.toString()) }))}`,
133
- })
134
- } else {
135
- await stream.writeSSE({
136
- event: 'datastar-patch-elements',
137
- data: `elements ${stripNewlines(AdminPanel())}`,
138
- })
139
- }
140
- })
99
+ db.database.prepare(deleteBlockWithChildren).all(id)
100
+
101
+ await renderSSE(stream, c)
141
102
  })
103
+ })
142
104
 
143
- app.post('/sort-order', async (c) => {
144
- const id = c.req.query('id')
145
- const sortOrder = c.req.query('sort-order')
105
+ app.post('/sort-order', async (c) => {
106
+ const id = c.req.query('id')
107
+ const sortOrder = c.req.query('sort-order')
146
108
 
147
- if (!id || !sortOrder) return
109
+ if (!id || !sortOrder) return
148
110
 
149
- const transaction = db.database.prepare(sql`
150
- update blocks
151
- set
152
- sort_order = ?
153
- where
154
- id = ?
155
- `)
111
+ const transaction = db.database.prepare(sql`
112
+ update blocks
113
+ set
114
+ sort_order = ?
115
+ where
116
+ id = ?
117
+ `)
156
118
 
157
- transaction.run(sortOrder, id)
119
+ transaction.run(sortOrder, id)
158
120
 
159
- return c.json({ status: 200, message: 'success' })
160
- })
121
+ return c.json({ status: 200, message: 'success' })
122
+ })
161
123
 
162
- return app
163
- }
124
+ export const blockRoutes = app
package/api/index.ts CHANGED
@@ -1,13 +1,20 @@
1
- import block from './block.ts'
2
- import apiKey from './api-key.ts'
3
- import type { Structure } from '../types.ts'
4
- import backup from './backup.ts'
1
+ import { Hono } from 'hono'
5
2
 
6
- export const api = (structure: Structure) => {
7
- const app = block(structure)
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
8
 
9
- app.route('/', apiKey())
10
- app.route('/', backup())
9
+ const routes = new Hono()
11
10
 
12
- return app
13
- }
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 CHANGED
@@ -1,53 +1,50 @@
1
1
  import { type HttpBindings } from '@hono/node-server'
2
- import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
3
2
  import { Hono } from 'hono'
4
3
  import { sql } from '../utils/sql.ts'
5
4
  import { db } from '@alstar/db'
6
5
  import { bearerAuth } from 'hono/bearer-auth'
7
6
  import crypto from 'node:crypto'
8
7
 
9
- export default () => {
10
- const app = new Hono<{ Bindings: HttpBindings }>()
11
-
12
- app.use(
13
- '/*',
14
- bearerAuth({
15
- verifyToken: async (token, c) => {
16
- const hash = crypto.createHash('sha256')
17
-
18
- hash.update(token)
19
-
20
- const digest = hash.digest().toString('base64')
21
-
22
- const exists = db.database
23
- .prepare(sql`
24
- select
25
- value
26
- from
27
- api_keys
28
- where
29
- value = ?
30
- `)
31
- .get(digest)
32
-
33
- return !!exists
34
- },
35
- }),
36
- )
37
-
38
- app.get('/entry', async (c) => {
39
- return c.json({
40
- status: 'success',
41
- message: 'Response from MCP server!',
42
- })
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!',
43
40
  })
41
+ })
44
42
 
45
- app.post('/entry', async (c) => {
46
- return c.json({
47
- status: 'success',
48
- message: 'New entry created!',
49
- })
43
+ app.post('/entry', async (c) => {
44
+ return c.json({
45
+ status: 'success',
46
+ message: 'New entry created!',
50
47
  })
48
+ })
51
49
 
52
- return app
53
- }
50
+ export const mcpRoutes = app