bxo 0.0.5-dev.6 โ 0.0.5-dev.61
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/README.md +99 -2
- package/index.ts +4 -787
- package/package.json +11 -5
- package/plugins/README.md +160 -0
- package/plugins/cors.ts +81 -57
- package/plugins/index.ts +4 -6
- package/plugins/ratelimit.ts +55 -59
- package/src/core/bxo.ts +438 -0
- package/src/handlers/request-handler.ts +229 -0
- package/src/index.ts +59 -0
- package/src/types/index.ts +164 -0
- package/src/utils/context-factory.ts +158 -0
- package/src/utils/helpers.ts +40 -0
- package/src/utils/index.ts +377 -0
- package/src/utils/response-handler.ts +286 -0
- package/src/utils/route-matcher.ts +191 -0
- package/tests/README.md +359 -0
- package/tests/integration/bxo.test.ts +598 -0
- package/tests/run-tests.ts +44 -0
- package/tests/unit/context-factory.test.ts +386 -0
- package/tests/unit/helpers.test.ts +253 -0
- package/tests/unit/response-handler.test.ts +301 -0
- package/tests/unit/route-matcher.test.ts +181 -0
- package/tests/unit/utils.test.ts +433 -0
- package/example.ts +0 -183
- package/plugins/auth.ts +0 -119
- package/plugins/logger.ts +0 -109
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
parseQuery,
|
|
4
|
+
parseHeaders,
|
|
5
|
+
parseCookies,
|
|
6
|
+
validateData,
|
|
7
|
+
validateResponse,
|
|
8
|
+
parseRequestBody,
|
|
9
|
+
cookiesToHeaders,
|
|
10
|
+
mergeHeadersWithCookies,
|
|
11
|
+
createRedirectResponse,
|
|
12
|
+
isFileUpload,
|
|
13
|
+
getFileFromUpload,
|
|
14
|
+
getFileInfo,
|
|
15
|
+
getFileUploads,
|
|
16
|
+
getFormFields
|
|
17
|
+
} from '../../src/utils';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
describe('Utility Functions', () => {
|
|
21
|
+
describe('parseQuery', () => {
|
|
22
|
+
it('should parse URLSearchParams correctly', () => {
|
|
23
|
+
const searchParams = new URLSearchParams('name=john&age=25&city=new%20york');
|
|
24
|
+
const result = parseQuery(searchParams);
|
|
25
|
+
|
|
26
|
+
expect(result).toEqual({
|
|
27
|
+
name: 'john',
|
|
28
|
+
age: '25',
|
|
29
|
+
city: 'new york'
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle empty search params', () => {
|
|
34
|
+
const searchParams = new URLSearchParams('');
|
|
35
|
+
const result = parseQuery(searchParams);
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('parseHeaders', () => {
|
|
42
|
+
it('should parse Headers object correctly', () => {
|
|
43
|
+
const headers = new Headers();
|
|
44
|
+
headers.set('content-type', 'application/json');
|
|
45
|
+
headers.set('authorization', 'Bearer token123');
|
|
46
|
+
|
|
47
|
+
const result = parseHeaders(headers);
|
|
48
|
+
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
'content-type': 'application/json',
|
|
51
|
+
'authorization': 'Bearer token123'
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('parseCookies', () => {
|
|
57
|
+
it('should parse cookie header correctly', () => {
|
|
58
|
+
const cookieHeader = 'name=john; age=25; city=new%20york';
|
|
59
|
+
const result = parseCookies(cookieHeader);
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
name: 'john',
|
|
63
|
+
age: '25',
|
|
64
|
+
city: 'new york'
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle null cookie header', () => {
|
|
69
|
+
const result = parseCookies(null);
|
|
70
|
+
expect(result).toEqual({});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle empty cookie header', () => {
|
|
74
|
+
const result = parseCookies('');
|
|
75
|
+
expect(result).toEqual({});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('validateData', () => {
|
|
80
|
+
it('should validate data with schema', () => {
|
|
81
|
+
const schema = z.object({
|
|
82
|
+
name: z.string(),
|
|
83
|
+
age: z.number()
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const data = { name: 'john', age: 25 };
|
|
87
|
+
const result = validateData(schema, data);
|
|
88
|
+
|
|
89
|
+
expect(result).toEqual(data);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return data without validation when no schema', () => {
|
|
93
|
+
const data = { name: 'john', age: 25 };
|
|
94
|
+
const result = validateData(undefined, data);
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual(data);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should throw error for invalid data', () => {
|
|
100
|
+
const schema = z.object({
|
|
101
|
+
name: z.string(),
|
|
102
|
+
age: z.number()
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const data = { name: 'john', age: 'invalid' };
|
|
106
|
+
|
|
107
|
+
expect(() => validateData(schema, data)).toThrow();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('validateResponse', () => {
|
|
112
|
+
it('should validate response with simple schema', () => {
|
|
113
|
+
const schema = z.object({
|
|
114
|
+
message: z.string()
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const data = { message: 'success' };
|
|
118
|
+
const result = validateResponse({
|
|
119
|
+
200: schema
|
|
120
|
+
}, data, 200);
|
|
121
|
+
|
|
122
|
+
expect(result).toEqual(data);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should validate response with status-based schema', () => {
|
|
126
|
+
const schema = {
|
|
127
|
+
200: z.object({ message: z.string() }),
|
|
128
|
+
400: z.object({ error: z.string() })
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const data = { message: 'success' };
|
|
132
|
+
const result = validateResponse(schema, data, 200);
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual(data);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return data without validation when no schema', () => {
|
|
138
|
+
const data = { message: 'success' };
|
|
139
|
+
const result = validateResponse(undefined, data);
|
|
140
|
+
|
|
141
|
+
expect(result).toEqual(data);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('cookiesToHeaders', () => {
|
|
146
|
+
it('should convert internal cookies to header strings', () => {
|
|
147
|
+
const cookies = [
|
|
148
|
+
{
|
|
149
|
+
name: 'session',
|
|
150
|
+
value: 'abc123',
|
|
151
|
+
domain: 'example.com',
|
|
152
|
+
path: '/',
|
|
153
|
+
secure: true,
|
|
154
|
+
httpOnly: true
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const result = cookiesToHeaders(cookies);
|
|
159
|
+
|
|
160
|
+
expect(result).toHaveLength(1);
|
|
161
|
+
expect(result[0]).toContain('session=abc123');
|
|
162
|
+
expect(result[0]).toContain('Domain=example.com');
|
|
163
|
+
expect(result[0]).toContain('Path=/');
|
|
164
|
+
expect(result[0]).toContain('Secure');
|
|
165
|
+
expect(result[0]).toContain('HttpOnly');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('mergeHeadersWithCookies', () => {
|
|
170
|
+
it('should merge headers with cookies', () => {
|
|
171
|
+
const headers = { 'content-type': 'application/json' };
|
|
172
|
+
const cookies = [
|
|
173
|
+
{ name: 'session', value: 'abc123' }
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const result = mergeHeadersWithCookies(headers, cookies);
|
|
177
|
+
|
|
178
|
+
expect(result.get('content-type')).toBe('application/json');
|
|
179
|
+
expect(result.get('set-cookie')).toBe('session=abc123');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('createRedirectResponse', () => {
|
|
184
|
+
it('should create redirect response with default status', () => {
|
|
185
|
+
const result = createRedirectResponse('/new-location');
|
|
186
|
+
|
|
187
|
+
expect(result.status).toBe(302);
|
|
188
|
+
expect(result.headers.get('location')).toBe('/new-location');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should create redirect response with custom status', () => {
|
|
192
|
+
const result = createRedirectResponse('/new-location', 301);
|
|
193
|
+
|
|
194
|
+
expect(result.status).toBe(301);
|
|
195
|
+
expect(result.headers.get('location')).toBe('/new-location');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should include additional headers', () => {
|
|
199
|
+
const headers = { 'x-custom': 'value' };
|
|
200
|
+
const result = createRedirectResponse('/new-location', 302, headers);
|
|
201
|
+
|
|
202
|
+
expect(result.headers.get('x-custom')).toBe('value');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('parseRequestBody', () => {
|
|
207
|
+
it('should parse JSON body correctly', async () => {
|
|
208
|
+
const jsonData = { name: 'john', age: 25 };
|
|
209
|
+
const request = new Request('http://localhost/test', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
body: JSON.stringify(jsonData)
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = await parseRequestBody(request);
|
|
216
|
+
expect(result).toEqual(jsonData);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should parse form data with arrays correctly', async () => {
|
|
220
|
+
const formData = new FormData();
|
|
221
|
+
formData.append('app', 'zodula');
|
|
222
|
+
formData.append('model', 'zodula_User');
|
|
223
|
+
formData.append('recordIds[]', 'asdfasdfdsa');
|
|
224
|
+
formData.append('recordIds[]', 'jarupak.sri@gmail.com');
|
|
225
|
+
formData.append('fields', '*');
|
|
226
|
+
|
|
227
|
+
const request = new Request('http://localhost/test', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
body: formData
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await parseRequestBody(request);
|
|
233
|
+
|
|
234
|
+
expect(result.app).toBe('zodula');
|
|
235
|
+
expect(result.model).toBe('zodula_User');
|
|
236
|
+
expect(Array.isArray(result.recordIds)).toBe(true);
|
|
237
|
+
expect(result.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
|
|
238
|
+
expect(result.fields).toBe('*');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should handle single form field correctly', async () => {
|
|
242
|
+
const formData = new FormData();
|
|
243
|
+
formData.append('name', 'john');
|
|
244
|
+
formData.append('email', 'john@example.com');
|
|
245
|
+
|
|
246
|
+
const request = new Request('http://localhost/test', {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: formData
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = await parseRequestBody(request);
|
|
252
|
+
|
|
253
|
+
expect(result.name).toBe('john');
|
|
254
|
+
expect(result.email).toBe('john@example.com');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle file uploads in form data', async () => {
|
|
258
|
+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
|
259
|
+
const formData = new FormData();
|
|
260
|
+
formData.append('name', 'john');
|
|
261
|
+
formData.append('file', file);
|
|
262
|
+
|
|
263
|
+
const request = new Request('http://localhost/test', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
body: formData
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await parseRequestBody(request);
|
|
269
|
+
|
|
270
|
+
expect(result.name).toBe('john');
|
|
271
|
+
expect(result.file).toBeInstanceOf(File);
|
|
272
|
+
expect(result.file.name).toBe('test.txt');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should parse nested object form data correctly', async () => {
|
|
276
|
+
const formData = new FormData();
|
|
277
|
+
formData.append('test[test]', 'test');
|
|
278
|
+
formData.append('test[new]', 'new');
|
|
279
|
+
formData.append('test[hi][hi]', 'hi');
|
|
280
|
+
|
|
281
|
+
const request = new Request('http://localhost/test', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
body: formData
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const result = await parseRequestBody(request);
|
|
287
|
+
|
|
288
|
+
expect(result.test).toEqual({
|
|
289
|
+
test: 'test',
|
|
290
|
+
new: 'new',
|
|
291
|
+
hi: {
|
|
292
|
+
hi: 'hi'
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should parse Axios-compatible multipart data with arrays and objects', async () => {
|
|
298
|
+
const formData = new FormData();
|
|
299
|
+
formData.append('x', '1');
|
|
300
|
+
formData.append('arr[]', '1');
|
|
301
|
+
formData.append('arr[]', '2');
|
|
302
|
+
formData.append('arr[]', '3');
|
|
303
|
+
formData.append('arr2[0]', '1');
|
|
304
|
+
formData.append('arr2[1][0]', '2');
|
|
305
|
+
formData.append('arr2[2]', '3');
|
|
306
|
+
formData.append('users[0][name]', 'Peter');
|
|
307
|
+
formData.append('users[0][surname]', 'Griffin');
|
|
308
|
+
formData.append('users[1][name]', 'Thomas');
|
|
309
|
+
formData.append('users[1][surname]', 'Anderson');
|
|
310
|
+
formData.append('obj2{}', '[{"x":1}]');
|
|
311
|
+
|
|
312
|
+
const request = new Request('http://localhost/test', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: formData
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const result = await parseRequestBody(request);
|
|
318
|
+
|
|
319
|
+
expect(result.x).toBe('1');
|
|
320
|
+
expect(result.arr).toEqual(['1', '2', '3']);
|
|
321
|
+
expect(result.arr2).toEqual(['1', ['2'], '3']);
|
|
322
|
+
expect(result.users).toEqual([
|
|
323
|
+
{ name: 'Peter', surname: 'Griffin' },
|
|
324
|
+
{ name: 'Thomas', surname: 'Anderson' }
|
|
325
|
+
]);
|
|
326
|
+
expect(result.obj2).toEqual([{ x: 1 }]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should handle JSON serialization with special endings', async () => {
|
|
330
|
+
const formData = new FormData();
|
|
331
|
+
formData.append('myObj{}', '{"x": 1, "s": "foo"}');
|
|
332
|
+
formData.append('settings{}', '{"theme": "dark", "notifications": true}');
|
|
333
|
+
|
|
334
|
+
const request = new Request('http://localhost/test', {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
body: formData
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const result = await parseRequestBody(request);
|
|
340
|
+
|
|
341
|
+
expect(result.myObj).toEqual({ x: 1, s: 'foo' });
|
|
342
|
+
expect(result.settings).toEqual({ theme: 'dark', notifications: true });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should handle mixed array indexing styles', async () => {
|
|
346
|
+
const formData = new FormData();
|
|
347
|
+
formData.append('tags[]', 'javascript');
|
|
348
|
+
formData.append('tags[]', 'typescript');
|
|
349
|
+
formData.append('scores[0]', '100');
|
|
350
|
+
formData.append('scores[1]', '95');
|
|
351
|
+
formData.append('scores[2]', '88');
|
|
352
|
+
|
|
353
|
+
const request = new Request('http://localhost/test', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
body: formData
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const result = await parseRequestBody(request);
|
|
359
|
+
|
|
360
|
+
expect(result.tags).toEqual(['javascript', 'typescript']);
|
|
361
|
+
expect(result.scores).toEqual(['100', '95', '88']);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('File Upload Utilities', () => {
|
|
366
|
+
it('should identify file uploads correctly', () => {
|
|
367
|
+
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
368
|
+
|
|
369
|
+
expect(isFileUpload(file)).toBe(true);
|
|
370
|
+
expect(isFileUpload('not a file')).toBe(false);
|
|
371
|
+
expect(isFileUpload({ type: 'text' })).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should extract file from upload', () => {
|
|
375
|
+
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
376
|
+
|
|
377
|
+
const result = getFileFromUpload(file);
|
|
378
|
+
expect(result).toBe(file);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should return null for non-file uploads', () => {
|
|
382
|
+
const result = getFileFromUpload('not a file');
|
|
383
|
+
expect(result).toBeNull();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should get file info', () => {
|
|
387
|
+
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
388
|
+
|
|
389
|
+
const result = getFileInfo(file);
|
|
390
|
+
expect(result).toEqual({
|
|
391
|
+
name: 'test.jpg',
|
|
392
|
+
size: 4,
|
|
393
|
+
mimetype: 'image/jpeg',
|
|
394
|
+
lastModified: expect.any(Number)
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should return null for non-file uploads when getting info', () => {
|
|
399
|
+
const result = getFileInfo('not a file');
|
|
400
|
+
expect(result).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should get all file uploads from form data', () => {
|
|
404
|
+
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
405
|
+
const formData = {
|
|
406
|
+
name: 'john',
|
|
407
|
+
avatar: file,
|
|
408
|
+
email: 'john@example.com'
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const result = getFileUploads(formData);
|
|
412
|
+
|
|
413
|
+
expect(Object.keys(result)).toHaveLength(1);
|
|
414
|
+
expect(result.avatar).toBeInstanceOf(File);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should get all non-file fields from form data', () => {
|
|
418
|
+
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
419
|
+
const formData = {
|
|
420
|
+
name: 'john',
|
|
421
|
+
avatar: file,
|
|
422
|
+
email: 'john@example.com'
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const result = getFormFields(formData);
|
|
426
|
+
|
|
427
|
+
expect(result).toEqual({
|
|
428
|
+
name: 'john',
|
|
429
|
+
email: 'john@example.com'
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
package/example.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import BXO, { z } from './index';
|
|
2
|
-
import { cors, logger, auth, rateLimit, createJWT } from './plugins';
|
|
3
|
-
|
|
4
|
-
// Create the app instance
|
|
5
|
-
const app = new BXO();
|
|
6
|
-
|
|
7
|
-
// Enable hot reload
|
|
8
|
-
app.enableHotReload(['./']); // Watch current directory
|
|
9
|
-
|
|
10
|
-
// Add plugins
|
|
11
|
-
app
|
|
12
|
-
.use(logger({ format: 'simple' }))
|
|
13
|
-
.use(cors({
|
|
14
|
-
origin: ['http://localhost:3000', 'https://example.com'],
|
|
15
|
-
credentials: true
|
|
16
|
-
}))
|
|
17
|
-
.use(rateLimit({
|
|
18
|
-
max: 100,
|
|
19
|
-
window: 60, // 1 minute
|
|
20
|
-
exclude: ['/health']
|
|
21
|
-
}))
|
|
22
|
-
.use(auth({
|
|
23
|
-
type: 'jwt',
|
|
24
|
-
secret: 'your-secret-key',
|
|
25
|
-
exclude: ['/', '/login', '/health']
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
// Add simplified lifecycle hooks
|
|
29
|
-
app
|
|
30
|
-
.onBeforeStart(() => {
|
|
31
|
-
console.log('๐ง Preparing to start server...');
|
|
32
|
-
})
|
|
33
|
-
.onAfterStart(() => {
|
|
34
|
-
console.log('โ
Server fully started and ready!');
|
|
35
|
-
})
|
|
36
|
-
.onBeforeStop(() => {
|
|
37
|
-
console.log('๐ง Preparing to stop server...');
|
|
38
|
-
})
|
|
39
|
-
.onAfterStop(() => {
|
|
40
|
-
console.log('โ
Server fully stopped!');
|
|
41
|
-
})
|
|
42
|
-
.onBeforeRestart(() => {
|
|
43
|
-
console.log('๐ง Preparing to restart server...');
|
|
44
|
-
})
|
|
45
|
-
.onAfterRestart(() => {
|
|
46
|
-
console.log('โ
Server restart completed!');
|
|
47
|
-
})
|
|
48
|
-
.onRequest((ctx) => {
|
|
49
|
-
console.log(`๐จ Processing ${ctx.request.method} ${ctx.request.url}`);
|
|
50
|
-
})
|
|
51
|
-
.onResponse((ctx, response) => {
|
|
52
|
-
console.log(`๐ค Response sent for ${ctx.request.method} ${ctx.request.url}`);
|
|
53
|
-
return response;
|
|
54
|
-
})
|
|
55
|
-
.onError((ctx, error) => {
|
|
56
|
-
console.error(`๐ฅ Error in ${ctx.request.method} ${ctx.request.url}:`, error.message);
|
|
57
|
-
return { error: 'Something went wrong', timestamp: new Date().toISOString() };
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Routes exactly like your example
|
|
61
|
-
app
|
|
62
|
-
// Two arguments: path, handler
|
|
63
|
-
.get('/simple', async (ctx) => {
|
|
64
|
-
return { message: 'Hello World' };
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// Three arguments: path, handler, config
|
|
68
|
-
.get('/users/:id', async (ctx) => {
|
|
69
|
-
// ctx.params.id is fully typed as string (UUID)
|
|
70
|
-
// ctx.query.include is typed as string | undefined
|
|
71
|
-
return { user: { id: ctx.params.id, include: ctx.query.include } };
|
|
72
|
-
}, {
|
|
73
|
-
params: z.object({ id: z.string().uuid() }),
|
|
74
|
-
query: z.object({ include: z.string().optional() })
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
.post('/users', async (ctx) => {
|
|
78
|
-
// ctx.body is fully typed with name: string, email: string
|
|
79
|
-
return { created: ctx.body };
|
|
80
|
-
}, {
|
|
81
|
-
body: z.object({
|
|
82
|
-
name: z.string(),
|
|
83
|
-
email: z.string().email()
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// Additional examples
|
|
88
|
-
.get('/health', async (ctx) => {
|
|
89
|
-
return {
|
|
90
|
-
status: 'ok',
|
|
91
|
-
timestamp: new Date().toISOString(),
|
|
92
|
-
server: app.getServerInfo()
|
|
93
|
-
};
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
.post('/login', async (ctx) => {
|
|
97
|
-
const { username, password } = ctx.body;
|
|
98
|
-
|
|
99
|
-
// Simple auth check (in production, verify against database)
|
|
100
|
-
if (username === 'admin' && password === 'password') {
|
|
101
|
-
const token = createJWT({ username, role: 'admin' }, 'your-secret-key', 3600);
|
|
102
|
-
return { token, user: { username, role: 'admin' } };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
ctx.set.status = 401;
|
|
106
|
-
return { error: 'Invalid credentials' };
|
|
107
|
-
}, {
|
|
108
|
-
body: z.object({
|
|
109
|
-
username: z.string(),
|
|
110
|
-
password: z.string()
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
.get('/protected', async (ctx) => {
|
|
115
|
-
// ctx.user is available here because of auth plugin
|
|
116
|
-
return { message: 'This is protected', user: ctx.user };
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
// Server control endpoints
|
|
120
|
-
.post('/restart', async (ctx) => {
|
|
121
|
-
// Restart the server
|
|
122
|
-
setTimeout(() => app.restart(3000), 100);
|
|
123
|
-
return { message: 'Server restart initiated' };
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
.get('/status', async (ctx) => {
|
|
127
|
-
return {
|
|
128
|
-
...app.getServerInfo(),
|
|
129
|
-
uptime: process.uptime(),
|
|
130
|
-
memory: process.memoryUsage()
|
|
131
|
-
};
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
.put('/users/:id', async (ctx) => {
|
|
135
|
-
return {
|
|
136
|
-
updated: ctx.body,
|
|
137
|
-
id: ctx.params.id,
|
|
138
|
-
version: ctx.headers['if-match']
|
|
139
|
-
};
|
|
140
|
-
}, {
|
|
141
|
-
params: z.object({ id: z.string().uuid() }),
|
|
142
|
-
body: z.object({
|
|
143
|
-
name: z.string().optional(),
|
|
144
|
-
email: z.string().email().optional()
|
|
145
|
-
}),
|
|
146
|
-
headers: z.object({
|
|
147
|
-
'if-match': z.string()
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
.delete('/users/:id', async (ctx) => {
|
|
152
|
-
ctx.set.status = 204;
|
|
153
|
-
return null;
|
|
154
|
-
}, {
|
|
155
|
-
params: z.object({ id: z.string().uuid() })
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Start the server (with hot reload enabled)
|
|
159
|
-
app.start(3000, 'localhost');
|
|
160
|
-
|
|
161
|
-
console.log(`
|
|
162
|
-
๐ฆ BXO Framework with Hot Reload
|
|
163
|
-
|
|
164
|
-
โจ Features Enabled:
|
|
165
|
-
- ๐ Hot reload (edit any .ts/.js file to restart)
|
|
166
|
-
- ๐ฃ Full lifecycle hooks (before/after pattern)
|
|
167
|
-
- ๐ JWT authentication
|
|
168
|
-
- ๐ Rate limiting
|
|
169
|
-
- ๐ CORS support
|
|
170
|
-
- ๐ Request logging
|
|
171
|
-
|
|
172
|
-
๐งช Try these endpoints:
|
|
173
|
-
- GET /simple
|
|
174
|
-
- GET /users/123e4567-e89b-12d3-a456-426614174000?include=profile
|
|
175
|
-
- POST /users (with JSON body: {"name": "John", "email": "john@example.com"})
|
|
176
|
-
- GET /health (shows server info)
|
|
177
|
-
- POST /login (with JSON body: {"username": "admin", "password": "password"})
|
|
178
|
-
- GET /protected (requires Bearer token from /login)
|
|
179
|
-
- GET /status (server statistics)
|
|
180
|
-
- POST /restart (restart server programmatically)
|
|
181
|
-
|
|
182
|
-
๐ก Edit this file and save to see hot reload in action!
|
|
183
|
-
`);
|