@bbki.ng/backend 0.3.6 → 0.3.9
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/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/eslint.config.js +1 -4
- package/migrations/001_add_posts.sql +13 -0
- package/migrations/002_seed_test_data.sql +15 -0
- package/package.json +2 -2
- package/src/config/app.config.ts +6 -6
- package/src/controllers/comment/add.controller.ts +6 -6
- package/src/controllers/posts/add.controller.ts +90 -0
- package/src/controllers/posts/get.controller.ts +42 -0
- package/src/controllers/posts/list.controller.ts +25 -0
- package/src/controllers/posts/remove.controller.ts +47 -0
- package/src/controllers/posts/update.controller.ts +101 -0
- package/src/controllers/streaming/add.controller.ts +32 -27
- package/src/controllers/streaming/list.controller.ts +22 -13
- package/src/controllers/streaming/remove.controller.ts +24 -25
- package/src/index.ts +2 -0
- package/src/routes/comment.routes.ts +3 -3
- package/src/routes/posts.routes.ts +20 -0
- package/src/routes/streaming.routes.ts +7 -7
- package/src/types/index.ts +10 -1
- package/src/utils/auth.ts +40 -0
- package/tsconfig.json +2 -2
- package/worker-configuration.d.ts +10010 -8896
- package/wrangler.jsonc +31 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# backend
|
|
2
2
|
|
|
3
|
+
## 0.3.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 282af74: update ui components
|
|
8
|
+
- 558e40b: update compoennts
|
|
9
|
+
|
|
10
|
+
## 0.3.8
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 21296f0: update backend
|
|
15
|
+
|
|
16
|
+
## 0.3.7
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 796a37a: remove unused code
|
|
21
|
+
|
|
3
22
|
## 0.3.6
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
package/eslint.config.js
CHANGED
|
@@ -6,7 +6,4 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
8
|
|
|
9
|
-
export default [
|
|
10
|
-
includeIgnoreFile(path.resolve(__dirname, '.gitignore')),
|
|
11
|
-
...cloudflareConfig,
|
|
12
|
-
];
|
|
9
|
+
export default [includeIgnoreFile(path.resolve(__dirname, '.gitignore')), ...cloudflareConfig];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- posts table for blog articles
|
|
2
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
title TEXT NOT NULL UNIQUE,
|
|
5
|
+
content TEXT NOT NULL,
|
|
6
|
+
author TEXT DEFAULT 'bbki.ng',
|
|
7
|
+
created_at TEXT NOT NULL,
|
|
8
|
+
updated_at TEXT NOT NULL
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- indexes for common queries
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_posts_title ON posts(title);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Seed test data for local development
|
|
2
|
+
INSERT INTO posts (id, title, content, author, created_at, updated_at) VALUES
|
|
3
|
+
('test-post-001', 'Hello World', 'This is a test post for local development. Welcome to bbki.ng!', 'bbki.ng', datetime('now'), datetime('now')),
|
|
4
|
+
('test-post-002', 'Second Post', 'Another test post to verify the posts list is working correctly.', 'bbki.ng', datetime('now', '-1 day'), datetime('now', '-1 day')),
|
|
5
|
+
('test-post-003', 'Testing Markdown', '# Markdown Test
|
|
6
|
+
|
|
7
|
+
This post tests **bold** and *italic* text.
|
|
8
|
+
|
|
9
|
+
## Code Block
|
|
10
|
+
```javascript
|
|
11
|
+
const hello = "world";
|
|
12
|
+
console.log(hello);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> A blockquote for testing.', 'bbki.ng', datetime('now', '-2 days'), datetime('now', '-2 days'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/backend",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@simplewebauthn/server": "13.2.2",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"prettier": "^3.2.0",
|
|
21
21
|
"typescript": "^5.3.0",
|
|
22
22
|
"wrangler": "^4.58.0",
|
|
23
|
-
"@bbki.ng/config": "1.0.
|
|
23
|
+
"@bbki.ng/config": "1.0.6"
|
|
24
24
|
},
|
|
25
25
|
"prettier": "@bbki.ng/config/prettier",
|
|
26
26
|
"scripts": {
|
package/src/config/app.config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Hono } from
|
|
2
|
-
import type { D1Database } from
|
|
3
|
-
import { cors } from
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
3
|
+
import { cors } from 'hono/cors';
|
|
4
4
|
|
|
5
5
|
type Bindings = {
|
|
6
6
|
DB: D1Database;
|
|
@@ -10,10 +10,10 @@ type Bindings = {
|
|
|
10
10
|
const app = new Hono<{ Bindings: Bindings }>();
|
|
11
11
|
|
|
12
12
|
app.use(
|
|
13
|
-
|
|
13
|
+
'*',
|
|
14
14
|
cors({
|
|
15
|
-
origin: [
|
|
16
|
-
})
|
|
15
|
+
origin: ['https://bbki.ng'],
|
|
16
|
+
})
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
export default app;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { Context } from
|
|
1
|
+
import { Context } from 'hono';
|
|
2
2
|
|
|
3
3
|
export const addComment = async (c: Context) => {
|
|
4
4
|
const { articleId, author, content } = await c.req.json();
|
|
5
5
|
|
|
6
6
|
try {
|
|
7
7
|
let { results } = await c.env.DB.prepare(
|
|
8
|
-
|
|
8
|
+
'INSERT INTO comment (article_id, author, content, created_at) VALUES (?, ?, ?, ?);'
|
|
9
9
|
)
|
|
10
10
|
.bind(articleId, author, content, new Date().toISOString())
|
|
11
11
|
.run();
|
|
12
12
|
|
|
13
13
|
return c.json({
|
|
14
|
-
status:
|
|
15
|
-
message:
|
|
14
|
+
status: 'success',
|
|
15
|
+
message: 'Comment added successfully',
|
|
16
16
|
results,
|
|
17
17
|
});
|
|
18
18
|
} catch (error: any) {
|
|
19
19
|
return c.json({
|
|
20
|
-
status:
|
|
21
|
-
message:
|
|
20
|
+
status: 'error',
|
|
21
|
+
message: 'Failed to add comment',
|
|
22
22
|
error: error.message,
|
|
23
23
|
});
|
|
24
24
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { HTTPException } from 'hono/http-exception';
|
|
3
|
+
|
|
4
|
+
interface AddPostRequest {
|
|
5
|
+
title: string;
|
|
6
|
+
content: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_TITLE_LENGTH = 200;
|
|
11
|
+
const MAX_CONTENT_LENGTH = 100000;
|
|
12
|
+
|
|
13
|
+
export const addPost = async (c: Context) => {
|
|
14
|
+
try {
|
|
15
|
+
// Parse and validate request body
|
|
16
|
+
const body = await c.req.json<AddPostRequest>();
|
|
17
|
+
|
|
18
|
+
if (!body.title || typeof body.title !== 'string') {
|
|
19
|
+
throw new HTTPException(400, { message: 'Title is required' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!body.content || typeof body.content !== 'string') {
|
|
23
|
+
throw new HTTPException(400, { message: 'Content is required' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const title = body.title.trim();
|
|
27
|
+
const content = body.content.trim();
|
|
28
|
+
|
|
29
|
+
if (title.length === 0) {
|
|
30
|
+
throw new HTTPException(400, { message: 'Title cannot be empty' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (title.length > MAX_TITLE_LENGTH) {
|
|
34
|
+
throw new HTTPException(413, { message: `Title exceeds ${MAX_TITLE_LENGTH} chars` });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (content.length === 0) {
|
|
38
|
+
throw new HTTPException(400, { message: 'Content cannot be empty' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
42
|
+
throw new HTTPException(413, { message: `Content exceeds ${MAX_CONTENT_LENGTH} chars` });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const author = (body.author ?? 'bbki.ng').trim();
|
|
46
|
+
|
|
47
|
+
// Check database availability
|
|
48
|
+
if (!c.env?.DB) {
|
|
49
|
+
throw new HTTPException(503, { message: 'Database unavailable' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Insert post
|
|
53
|
+
const id = crypto.randomUUID();
|
|
54
|
+
const now = new Date().toISOString();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await c.env.DB.prepare(
|
|
58
|
+
'INSERT INTO posts (id, title, content, author, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
59
|
+
)
|
|
60
|
+
.bind(id, title, content, author, now, now)
|
|
61
|
+
.run();
|
|
62
|
+
} catch (dbError: any) {
|
|
63
|
+
if (dbError.message?.includes('UNIQUE')) {
|
|
64
|
+
throw new HTTPException(409, { message: 'Post with this title already exists' });
|
|
65
|
+
}
|
|
66
|
+
throw dbError;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return c.json(
|
|
70
|
+
{
|
|
71
|
+
status: 'success',
|
|
72
|
+
data: { id, title, content, author, createdAt: now, updatedAt: now },
|
|
73
|
+
},
|
|
74
|
+
201
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof HTTPException) {
|
|
78
|
+
return c.json({ status: 'error', message: error.message }, error.status);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.error('Unexpected error:', error);
|
|
82
|
+
return c.json(
|
|
83
|
+
{
|
|
84
|
+
status: 'error',
|
|
85
|
+
message: 'Internal server error',
|
|
86
|
+
},
|
|
87
|
+
500
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { HTTPException } from 'hono/http-exception';
|
|
3
|
+
|
|
4
|
+
export const getPost = async (c: Context) => {
|
|
5
|
+
try {
|
|
6
|
+
const title = c.req.param('title');
|
|
7
|
+
|
|
8
|
+
if (!title) {
|
|
9
|
+
throw new HTTPException(400, { message: 'Title is required' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const result = await c.env.DB.prepare(
|
|
13
|
+
`SELECT id, title, content, author, created_at as createdAt, updated_at as updatedAt
|
|
14
|
+
FROM posts
|
|
15
|
+
WHERE title = ?`
|
|
16
|
+
)
|
|
17
|
+
.bind(title)
|
|
18
|
+
.first();
|
|
19
|
+
|
|
20
|
+
if (!result) {
|
|
21
|
+
throw new HTTPException(404, { message: 'Post not found' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return c.json({
|
|
25
|
+
status: 'success',
|
|
26
|
+
data: result,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof HTTPException) {
|
|
30
|
+
return c.json({ status: 'error', message: error.message }, error.status);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.error('Unexpected error:', error);
|
|
34
|
+
return c.json(
|
|
35
|
+
{
|
|
36
|
+
status: 'error',
|
|
37
|
+
message: 'Internal server error',
|
|
38
|
+
},
|
|
39
|
+
500
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
|
|
3
|
+
export const listPosts = async (c: Context) => {
|
|
4
|
+
try {
|
|
5
|
+
const { results } = await c.env.DB.prepare(
|
|
6
|
+
`SELECT id, title, content, author, created_at as createdAt, updated_at as updatedAt
|
|
7
|
+
FROM posts
|
|
8
|
+
ORDER BY created_at DESC`
|
|
9
|
+
).all();
|
|
10
|
+
|
|
11
|
+
return c.json({
|
|
12
|
+
status: 'success',
|
|
13
|
+
data: results,
|
|
14
|
+
});
|
|
15
|
+
} catch (error: any) {
|
|
16
|
+
return c.json(
|
|
17
|
+
{
|
|
18
|
+
status: 'error',
|
|
19
|
+
message: 'Failed to fetch posts',
|
|
20
|
+
error: error.message,
|
|
21
|
+
},
|
|
22
|
+
500
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { HTTPException } from 'hono/http-exception';
|
|
3
|
+
|
|
4
|
+
export const removePost = async (c: Context) => {
|
|
5
|
+
try {
|
|
6
|
+
const id = c.req.param('id');
|
|
7
|
+
|
|
8
|
+
if (!id) {
|
|
9
|
+
throw new HTTPException(400, { message: 'Post ID is required' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Check database availability
|
|
13
|
+
if (!c.env?.DB) {
|
|
14
|
+
throw new HTTPException(503, { message: 'Database unavailable' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check if post exists
|
|
18
|
+
const existingPost = await c.env.DB.prepare('SELECT id FROM posts WHERE id = ?')
|
|
19
|
+
.bind(id)
|
|
20
|
+
.first();
|
|
21
|
+
|
|
22
|
+
if (!existingPost) {
|
|
23
|
+
throw new HTTPException(404, { message: 'Post not found' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Delete post
|
|
27
|
+
await c.env.DB.prepare('DELETE FROM posts WHERE id = ?').bind(id).run();
|
|
28
|
+
|
|
29
|
+
return c.json({
|
|
30
|
+
status: 'success',
|
|
31
|
+
message: 'Post deleted successfully',
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof HTTPException) {
|
|
35
|
+
return c.json({ status: 'error', message: error.message }, error.status);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.error('Unexpected error:', error);
|
|
39
|
+
return c.json(
|
|
40
|
+
{
|
|
41
|
+
status: 'error',
|
|
42
|
+
message: 'Internal server error',
|
|
43
|
+
},
|
|
44
|
+
500
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { HTTPException } from 'hono/http-exception';
|
|
3
|
+
|
|
4
|
+
interface UpdatePostRequest {
|
|
5
|
+
title: string;
|
|
6
|
+
content: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_TITLE_LENGTH = 200;
|
|
11
|
+
const MAX_CONTENT_LENGTH = 100000;
|
|
12
|
+
|
|
13
|
+
export const updatePost = async (c: Context) => {
|
|
14
|
+
try {
|
|
15
|
+
const id = c.req.param('id');
|
|
16
|
+
|
|
17
|
+
if (!id) {
|
|
18
|
+
throw new HTTPException(400, { message: 'Post ID is required' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Parse and validate request body
|
|
22
|
+
const body = await c.req.json<UpdatePostRequest>();
|
|
23
|
+
|
|
24
|
+
if (!body.title || typeof body.title !== 'string') {
|
|
25
|
+
throw new HTTPException(400, { message: 'Title is required' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!body.content || typeof body.content !== 'string') {
|
|
29
|
+
throw new HTTPException(400, { message: 'Content is required' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const title = body.title.trim();
|
|
33
|
+
const content = body.content.trim();
|
|
34
|
+
|
|
35
|
+
if (title.length === 0) {
|
|
36
|
+
throw new HTTPException(400, { message: 'Title cannot be empty' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (title.length > MAX_TITLE_LENGTH) {
|
|
40
|
+
throw new HTTPException(413, { message: `Title exceeds ${MAX_TITLE_LENGTH} chars` });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (content.length === 0) {
|
|
44
|
+
throw new HTTPException(400, { message: 'Content cannot be empty' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
48
|
+
throw new HTTPException(413, { message: `Content exceeds ${MAX_CONTENT_LENGTH} chars` });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const author = (body.author ?? 'bbki.ng').trim();
|
|
52
|
+
|
|
53
|
+
// Check database availability
|
|
54
|
+
if (!c.env?.DB) {
|
|
55
|
+
throw new HTTPException(503, { message: 'Database unavailable' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if post exists
|
|
59
|
+
const existingPost = await c.env.DB.prepare('SELECT id FROM posts WHERE id = ?')
|
|
60
|
+
.bind(id)
|
|
61
|
+
.first();
|
|
62
|
+
|
|
63
|
+
if (!existingPost) {
|
|
64
|
+
throw new HTTPException(404, { message: 'Post not found' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update post
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await c.env.DB.prepare(
|
|
72
|
+
'UPDATE posts SET title = ?, content = ?, author = ?, updated_at = ? WHERE id = ?'
|
|
73
|
+
)
|
|
74
|
+
.bind(title, content, author, now, id)
|
|
75
|
+
.run();
|
|
76
|
+
} catch (dbError: any) {
|
|
77
|
+
if (dbError.message?.includes('UNIQUE')) {
|
|
78
|
+
throw new HTTPException(409, { message: 'Post with this title already exists' });
|
|
79
|
+
}
|
|
80
|
+
throw dbError;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return c.json({
|
|
84
|
+
status: 'success',
|
|
85
|
+
data: { id, title, content, author, updatedAt: now },
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof HTTPException) {
|
|
89
|
+
return c.json({ status: 'error', message: error.message }, error.status);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.error('Unexpected error:', error);
|
|
93
|
+
return c.json(
|
|
94
|
+
{
|
|
95
|
+
status: 'error',
|
|
96
|
+
message: 'Internal server error',
|
|
97
|
+
},
|
|
98
|
+
500
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Context } from
|
|
2
|
-
import { HTTPException } from
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { HTTPException } from 'hono/http-exception';
|
|
3
3
|
|
|
4
4
|
interface AddStreamingRequest {
|
|
5
5
|
author?: string;
|
|
@@ -25,25 +25,25 @@ export const addStreaming = async (c: Context) => {
|
|
|
25
25
|
const streamApiKey = c.env?.STREAM_API_KEY;
|
|
26
26
|
if (!streamApiKey) {
|
|
27
27
|
console.error('STREAM_API_KEY not configured in environment');
|
|
28
|
-
throw new HTTPException(500, { message:
|
|
28
|
+
throw new HTTPException(500, { message: 'Server configuration error' });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// 1. 认证(Header 方式)
|
|
32
32
|
const apiKey = c.req.header('x-api-key');
|
|
33
33
|
if (!apiKey || !timingSafeEqual(apiKey, streamApiKey)) {
|
|
34
|
-
throw new HTTPException(401, { message:
|
|
34
|
+
throw new HTTPException(401, { message: 'Invalid API key' });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// 2. 解析与验证
|
|
38
38
|
const body = await c.req.json<AddStreamingRequest>();
|
|
39
|
-
|
|
40
|
-
if (!body.content || typeof body.content !==
|
|
41
|
-
throw new HTTPException(400, { message:
|
|
39
|
+
|
|
40
|
+
if (!body.content || typeof body.content !== 'string') {
|
|
41
|
+
throw new HTTPException(400, { message: 'Content is required' });
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
const content = body.content.trim();
|
|
45
45
|
if (content.length === 0) {
|
|
46
|
-
throw new HTTPException(400, { message:
|
|
46
|
+
throw new HTTPException(400, { message: 'Content cannot be empty' });
|
|
47
47
|
}
|
|
48
48
|
if (content.length > MAX_CONTENT_LENGTH) {
|
|
49
49
|
throw new HTTPException(413, { message: `Content exceeds ${MAX_CONTENT_LENGTH} chars` });
|
|
@@ -51,17 +51,17 @@ export const addStreaming = async (c: Context) => {
|
|
|
51
51
|
|
|
52
52
|
const author = (body.author ?? 'bbki.ng').trim();
|
|
53
53
|
if (author.length > 50) {
|
|
54
|
-
throw new HTTPException(400, { message:
|
|
54
|
+
throw new HTTPException(400, { message: 'Author too long' });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const type = body.type?.trim();
|
|
58
|
-
if (type && !ALLOWED_TYPES.includes(type as typeof ALLOWED_TYPES[number])) {
|
|
59
|
-
throw new HTTPException(400, { message:
|
|
58
|
+
if (type && !ALLOWED_TYPES.includes(type as (typeof ALLOWED_TYPES)[number])) {
|
|
59
|
+
throw new HTTPException(400, { message: 'Invalid type' });
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// 3. 数据库检查
|
|
63
63
|
if (!c.env?.DB) {
|
|
64
|
-
throw new HTTPException(503, { message:
|
|
64
|
+
throw new HTTPException(503, { message: 'Database unavailable' });
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 4. 写入
|
|
@@ -70,31 +70,36 @@ export const addStreaming = async (c: Context) => {
|
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
await c.env.DB.prepare(
|
|
73
|
-
|
|
73
|
+
'INSERT INTO streaming (id, author, content, type, created_at) VALUES (?, ?, ?, ?, ?)'
|
|
74
74
|
)
|
|
75
75
|
.bind(id, author, content, type || null, createdAt)
|
|
76
76
|
.run();
|
|
77
77
|
} catch (dbError) {
|
|
78
78
|
if (dbError instanceof Error && dbError.message.includes('UNIQUE')) {
|
|
79
|
-
throw new HTTPException(409, { message:
|
|
79
|
+
throw new HTTPException(409, { message: 'Duplicate entry' });
|
|
80
80
|
}
|
|
81
81
|
throw dbError;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
return c.json(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
return c.json(
|
|
85
|
+
{
|
|
86
|
+
status: 'success',
|
|
87
|
+
data: { id, author, content, type, createdAt },
|
|
88
|
+
},
|
|
89
|
+
201
|
|
90
|
+
);
|
|
89
91
|
} catch (error) {
|
|
90
92
|
if (error instanceof HTTPException) {
|
|
91
|
-
return c.json({ status:
|
|
93
|
+
return c.json({ status: 'error', message: error.message }, error.status);
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
console.error('Unexpected error:', error);
|
|
95
|
-
return c.json(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
return c.json(
|
|
98
|
+
{
|
|
99
|
+
status: 'error',
|
|
100
|
+
message: 'Internal server error',
|
|
101
|
+
},
|
|
102
|
+
500
|
|
103
|
+
);
|
|
99
104
|
}
|
|
100
|
-
};
|
|
105
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from
|
|
1
|
+
import { Context } from 'hono';
|
|
2
2
|
|
|
3
3
|
export const listStreaming = async (c: Context) => {
|
|
4
4
|
try {
|
|
@@ -6,9 +6,9 @@ export const listStreaming = async (c: Context) => {
|
|
|
6
6
|
// 'before' - fetch records older than this ID (for pagination / next page)
|
|
7
7
|
// 'after' - fetch records newer than this ID (for polling / new messages)
|
|
8
8
|
// 'offset' - number of records to fetch (default: 8, max: 100)
|
|
9
|
-
const before = c.req.query(
|
|
10
|
-
const after = c.req.query(
|
|
11
|
-
const offset = Math.min(parseInt(c.req.query(
|
|
9
|
+
const before = c.req.query('before');
|
|
10
|
+
const after = c.req.query('after');
|
|
11
|
+
const offset = Math.min(parseInt(c.req.query('offset') || '8', 10), 100);
|
|
12
12
|
|
|
13
13
|
let results;
|
|
14
14
|
|
|
@@ -20,7 +20,9 @@ export const listStreaming = async (c: Context) => {
|
|
|
20
20
|
WHERE created_at < (SELECT created_at FROM streaming WHERE id = ?)
|
|
21
21
|
ORDER BY created_at DESC
|
|
22
22
|
LIMIT ?`
|
|
23
|
-
)
|
|
23
|
+
)
|
|
24
|
+
.bind(before, offset)
|
|
25
|
+
.all();
|
|
24
26
|
results = cursorResults;
|
|
25
27
|
} else if (after) {
|
|
26
28
|
// Fetch records newer than the specified id (polling - new messages)
|
|
@@ -31,7 +33,9 @@ export const listStreaming = async (c: Context) => {
|
|
|
31
33
|
WHERE created_at > (SELECT created_at FROM streaming WHERE id = ?)
|
|
32
34
|
ORDER BY created_at ASC
|
|
33
35
|
LIMIT ?`
|
|
34
|
-
)
|
|
36
|
+
)
|
|
37
|
+
.bind(after, offset)
|
|
38
|
+
.all();
|
|
35
39
|
results = cursorResults;
|
|
36
40
|
} else {
|
|
37
41
|
// Fetch the most recent records
|
|
@@ -40,19 +44,24 @@ export const listStreaming = async (c: Context) => {
|
|
|
40
44
|
FROM streaming
|
|
41
45
|
ORDER BY created_at DESC
|
|
42
46
|
LIMIT ?`
|
|
43
|
-
)
|
|
47
|
+
)
|
|
48
|
+
.bind(offset)
|
|
49
|
+
.all();
|
|
44
50
|
results = recentResults;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
return c.json({
|
|
48
|
-
status:
|
|
54
|
+
status: 'success',
|
|
49
55
|
data: results,
|
|
50
56
|
});
|
|
51
57
|
} catch (error: any) {
|
|
52
|
-
return c.json(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
return c.json(
|
|
59
|
+
{
|
|
60
|
+
status: 'error',
|
|
61
|
+
message: 'Failed to fetch streaming',
|
|
62
|
+
error: error.message,
|
|
63
|
+
},
|
|
64
|
+
500
|
|
65
|
+
);
|
|
57
66
|
}
|
|
58
67
|
};
|