1
1
import { Hono } from "hono" ;
2
2
import { v4 as uuidv4 } from "uuid" ;
3
+ import { z } from "zod" ;
3
4
import { authMiddleware , type AuthContext } from "./middleware.ts" ;
4
5
import { type Env } from "./types.ts" ;
5
6
@@ -10,6 +11,19 @@ import {
10
11
validateProviderConfig ,
11
12
} from "./providers/api-key-factory.ts" ;
12
13
14
+ // Import notebook utilities
15
+ import { createNotebookId } from "./utils/notebook-id.ts" ;
16
+ import {
17
+ createNotebook ,
18
+ getNotebookById ,
19
+ createTag ,
20
+ getTagByName ,
21
+ assignTagToNotebook ,
22
+ getNotebookTags ,
23
+ } from "./trpc/db.ts" ;
24
+ import { createPermissionsProvider } from "./notebook-permissions/factory.ts" ;
25
+ import type { TagColor } from "./trpc/types.ts" ;
26
+
13
27
const api = new Hono < { Bindings : Env ; Variables : AuthContext } > ( ) ;
14
28
15
29
// Health endpoint - no auth required
@@ -52,6 +66,218 @@ api.get("/me", authMiddleware, (c) => {
52
66
} ) ;
53
67
} ) ;
54
68
69
+ // Request body validation schema for notebook creation
70
+ const createNotebookSchema = z . object ( {
71
+ title : z . string ( ) . min ( 1 ) . max ( 255 ) . trim ( ) ,
72
+ tags : z . array ( z . string ( ) . min ( 1 ) . max ( 50 ) . trim ( ) ) . optional ( ) ,
73
+ } ) ;
74
+
75
+ /**
76
+ * POST /notebooks - Create a new notebook
77
+ *
78
+ * Creates a new notebook with the authenticated user as owner.
79
+ * Designed for external clients using API keys.
80
+ *
81
+ * @param title - Required notebook title (1-255 characters)
82
+ * @param tags - Optional array of tag names to assign to notebook
83
+ * @returns Created notebook with ID, title, owner, and timestamps
84
+ */
85
+ api . post ( "/notebooks" , authMiddleware , async ( c ) => {
86
+ const passport = c . get ( "passport" ) ;
87
+ if ( ! passport ) {
88
+ return c . json ( { error : "Authentication failed" } , 401 ) ;
89
+ }
90
+
91
+ try {
92
+ const body = await c . req . json ( ) ;
93
+
94
+ // Validate request body with Zod
95
+ const parseResult = createNotebookSchema . safeParse ( body ) ;
96
+ if ( ! parseResult . success ) {
97
+ return c . json (
98
+ {
99
+ error : "Bad Request" ,
100
+ message : "Invalid request body" ,
101
+ details : parseResult . error . format ( ) ,
102
+ } ,
103
+ 400
104
+ ) ;
105
+ }
106
+
107
+ const { title, tags } = parseResult . data ;
108
+
109
+ // Generate notebook ID
110
+ const notebookId = createNotebookId ( ) ;
111
+
112
+ // Create notebook in database - ownership is established through owner_id field
113
+ const success = await createNotebook ( c . env . DB , {
114
+ id : notebookId ,
115
+ ownerId : passport . user . id ,
116
+ title : title ,
117
+ } ) ;
118
+
119
+ if ( ! success ) {
120
+ return c . json (
121
+ {
122
+ error : "Internal Server Error" ,
123
+ message : "Failed to create notebook" ,
124
+ } ,
125
+ 500
126
+ ) ;
127
+ }
128
+
129
+ // Retrieve the created notebook
130
+ const notebook = await getNotebookById ( c . env . DB , notebookId ) ;
131
+
132
+ if ( ! notebook ) {
133
+ return c . json (
134
+ {
135
+ error : "Internal Server Error" ,
136
+ message : "Notebook created but could not be retrieved" ,
137
+ } ,
138
+ 500
139
+ ) ;
140
+ }
141
+
142
+ // Handle tag assignment if tags were provided
143
+ if ( tags && tags . length > 0 ) {
144
+ try {
145
+ for ( const tagName of tags ) {
146
+ // Check if tag already exists for this user
147
+ let tag = await getTagByName ( c . env . DB , tagName , passport . user . id ) ;
148
+
149
+ // Create tag if it doesn't exist
150
+ if ( ! tag ) {
151
+ tag = await createTag ( c . env . DB , {
152
+ name : tagName ,
153
+ color : "#3B82F6" as TagColor , // Default blue color
154
+ user_id : passport . user . id ,
155
+ } ) ;
156
+ }
157
+
158
+ // Assign tag to notebook if creation was successful
159
+ if ( tag ) {
160
+ await assignTagToNotebook ( c . env . DB , notebookId , tag . id ) ;
161
+ }
162
+ }
163
+ } catch ( tagError ) {
164
+ console . warn ( "❌ Tag assignment failed:" , tagError ) ;
165
+ // Don't fail the entire request if tag assignment fails
166
+ }
167
+ }
168
+
169
+ return c . json ( {
170
+ id : notebook . id ,
171
+ title : notebook . title ,
172
+ ownerId : notebook . owner_id ,
173
+ createdAt : notebook . created_at ,
174
+ updatedAt : notebook . updated_at ,
175
+ } ) ;
176
+ } catch ( error ) {
177
+ console . error ( "❌ Notebook creation failed:" , error ) ;
178
+
179
+ if ( error instanceof SyntaxError ) {
180
+ return c . json (
181
+ {
182
+ error : "Bad Request" ,
183
+ message : "Invalid JSON in request body" ,
184
+ } ,
185
+ 400
186
+ ) ;
187
+ }
188
+
189
+ return c . json (
190
+ {
191
+ error : "Internal Server Error" ,
192
+ message : "Failed to create notebook" ,
193
+ } ,
194
+ 500
195
+ ) ;
196
+ }
197
+ } ) ;
198
+
199
+ /**
200
+ * GET /notebooks/:id - Get specific notebook by ID
201
+ *
202
+ * Returns notebook details if the authenticated user has access.
203
+ * Designed for external clients using API keys.
204
+ *
205
+ * @param id - Notebook ID
206
+ * @returns Notebook details with metadata
207
+ */
208
+ api . get ( "/notebooks/:id" , authMiddleware , async ( c ) => {
209
+ const passport = c . get ( "passport" ) ;
210
+ if ( ! passport ) {
211
+ return c . json ( { error : "Authentication failed" } , 401 ) ;
212
+ }
213
+
214
+ const notebookId = c . req . param ( "id" ) ;
215
+ if ( ! notebookId ) {
216
+ return c . json (
217
+ {
218
+ error : "Bad Request" ,
219
+ message : "Notebook ID is required" ,
220
+ } ,
221
+ 400
222
+ ) ;
223
+ }
224
+
225
+ try {
226
+ // Create permissions provider
227
+ const permissionsProvider = createPermissionsProvider ( c . env ) ;
228
+
229
+ // Check if user has access to this notebook
230
+ const permissionResult = await permissionsProvider . checkPermission (
231
+ passport . user . id ,
232
+ notebookId
233
+ ) ;
234
+
235
+ if ( ! permissionResult . hasAccess ) {
236
+ return c . json (
237
+ {
238
+ error : "Not Found" ,
239
+ message : "Notebook not found or access denied" ,
240
+ } ,
241
+ 404
242
+ ) ;
243
+ }
244
+
245
+ // Get notebook from database
246
+ const notebook = await getNotebookById ( c . env . DB , notebookId ) ;
247
+
248
+ if ( ! notebook ) {
249
+ return c . json (
250
+ {
251
+ error : "Not Found" ,
252
+ message : "Notebook not found" ,
253
+ } ,
254
+ 404
255
+ ) ;
256
+ }
257
+
258
+ // Get notebook tags
259
+ const tags = await getNotebookTags ( c . env . DB , notebookId , passport . user . id ) ;
260
+
261
+ return c . json ( {
262
+ id : notebook . id ,
263
+ title : notebook . title ,
264
+ ownerId : notebook . owner_id ,
265
+ createdAt : notebook . created_at ,
266
+ updatedAt : notebook . updated_at ,
267
+ tags : tags ,
268
+ } ) ;
269
+ } catch ( error ) {
270
+ console . error ( "❌ Failed to get notebook:" , error ) ;
271
+ return c . json (
272
+ {
273
+ error : "Internal Server Error" ,
274
+ message : "Failed to retrieve notebook" ,
275
+ } ,
276
+ 500
277
+ ) ;
278
+ }
279
+ } ) ;
280
+
55
281
// Mount unified API key routes
56
282
api . route ( "/api-keys" , apiKeyRoutes ) ;
57
283
0 commit comments