balda-js 0.0.1 → 0.0.3
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/package.json +1 -6
- package/.husky/pre-commit +0 -19
- package/.nvmrc +0 -1
- package/docs/README.md +0 -135
- package/docs/blog/authors.yml +0 -6
- package/docs/blog/tags.yml +0 -4
- package/docs/cli.md +0 -109
- package/docs/docs/core-concepts/controllers.md +0 -393
- package/docs/docs/core-concepts/middleware.md +0 -302
- package/docs/docs/core-concepts/request-response.md +0 -486
- package/docs/docs/core-concepts/routing.md +0 -388
- package/docs/docs/core-concepts/server.md +0 -332
- package/docs/docs/cron/overview.md +0 -70
- package/docs/docs/examples/rest-api.md +0 -595
- package/docs/docs/getting-started/configuration.md +0 -168
- package/docs/docs/getting-started/installation.md +0 -125
- package/docs/docs/getting-started/quick-start.md +0 -273
- package/docs/docs/intro.md +0 -46
- package/docs/docs/plugins/cookie.md +0 -424
- package/docs/docs/plugins/cors.md +0 -295
- package/docs/docs/plugins/file.md +0 -382
- package/docs/docs/plugins/helmet.md +0 -388
- package/docs/docs/plugins/json.md +0 -338
- package/docs/docs/plugins/log.md +0 -592
- package/docs/docs/plugins/overview.md +0 -390
- package/docs/docs/plugins/rate-limiter.md +0 -347
- package/docs/docs/plugins/static.md +0 -352
- package/docs/docs/plugins/swagger.md +0 -411
- package/docs/docs/plugins/urlencoded.md +0 -76
- package/docs/docs/testing/examples.md +0 -384
- package/docs/docs/testing/mock-server.md +0 -311
- package/docs/docs/testing/overview.md +0 -76
- package/docs/docusaurus.config.ts +0 -144
- package/docs/intro.md +0 -78
- package/docs/package.json +0 -46
- package/docs/sidebars.ts +0 -72
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.svg +0 -1
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -37
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/docs/tsconfig.json +0 -8
- package/speed_test.sh +0 -3
- package/test/benchmark/index.ts +0 -17
- package/test/cli/cli.ts +0 -7
- package/test/commands/test.ts +0 -42
- package/test/controllers/file_upload.ts +0 -29
- package/test/controllers/urlencoded.ts +0 -13
- package/test/controllers/users.ts +0 -111
- package/test/cron/index.ts +0 -6
- package/test/cron/test_cron.ts +0 -8
- package/test/cron/test_cron_imported.ts +0 -8
- package/test/native_env.ts +0 -16
- package/test/resources/test.txt +0 -1
- package/test/server/index.ts +0 -3
- package/test/server/instance.ts +0 -63
- package/test/suite/upload.test.ts +0 -23
- package/test/suite/urlencoded.test.ts +0 -23
- package/test/suite/users.test.ts +0 -76
- package/todo.md +0 -9
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -17
@@ -1,70 +0,0 @@
|
|
1
|
-
---
|
2
|
-
sidebar_position: 6
|
3
|
-
---
|
4
|
-
|
5
|
-
# Overview
|
6
|
-
|
7
|
-
Balda.js includes a **cron job decorator** that lets you schedule background tasks directly from your controllers or services.
|
8
|
-
|
9
|
-
> **Peer dependency** — Balda.js intentionally leaves the scheduling engine to the community-standard [`node-cron`](https://www.npmjs.com/package/node-cron). To enable cron support you must install it yourself:
|
10
|
-
>
|
11
|
-
> ```bash
|
12
|
-
> # with npm
|
13
|
-
> npm install node-cron
|
14
|
-
>
|
15
|
-
> # or with yarn / pnpm / bun
|
16
|
-
> yarn add node-cron
|
17
|
-
> pnpm add node-cron
|
18
|
-
> bun add node-cron
|
19
|
-
> ```
|
20
|
-
|
21
|
-
## Basic Usage
|
22
|
-
|
23
|
-
```typescript
|
24
|
-
import { cron } from 'balda-js';
|
25
|
-
|
26
|
-
class TickCron {
|
27
|
-
// Run every minute
|
28
|
-
@cron('* * * * *')
|
29
|
-
async handle() {
|
30
|
-
console.log('Cron executed!');
|
31
|
-
}
|
32
|
-
}
|
33
|
-
```
|
34
|
-
|
35
|
-
The decorator accepts any valid [cron expression](https://crontab.guru/) supported by `node-cron`.
|
36
|
-
|
37
|
-
## Advanced Options
|
38
|
-
|
39
|
-
You can pass an **options object** as the second argument to control timezone, concurrency, etc.
|
40
|
-
|
41
|
-
```typescript
|
42
|
-
@cron('0 0 * * *', { timezone: 'UTC' })
|
43
|
-
async handle() {
|
44
|
-
await this.cleanupService.run();
|
45
|
-
}
|
46
|
-
```
|
47
|
-
|
48
|
-
## Error Handling
|
49
|
-
|
50
|
-
By default, if the decorated method throws, Balda.js will log the error (using the configured logger) but will **not** crash your server.
|
51
|
-
|
52
|
-
You can set a global cron error handler to handle the errors:
|
53
|
-
|
54
|
-
```typescript
|
55
|
-
server.setGlobalCronErrorHandler((error) => {
|
56
|
-
console.error(error);
|
57
|
-
});
|
58
|
-
```
|
59
|
-
|
60
|
-
## Starting Cron Jobs
|
61
|
-
|
62
|
-
You can import registered cron jobs classes that have one or more `@cron` decorated methods or directly import the `@cron` decorator and use it to decorate your own methods.
|
63
|
-
|
64
|
-
```typescript
|
65
|
-
import './cron/specific_cron_job';
|
66
|
-
|
67
|
-
server.startRegisteredCrons(['./cron/cron_jobs.{ts,js}'], () => {
|
68
|
-
console.log('Cron jobs started');
|
69
|
-
});
|
70
|
-
```
|
@@ -1,595 +0,0 @@
|
|
1
|
-
---
|
2
|
-
sidebar_position: 1
|
3
|
-
---
|
4
|
-
|
5
|
-
# REST API Example
|
6
|
-
|
7
|
-
A complete REST API example using Balda.js with validation, serialization, and error handling.
|
8
|
-
|
9
|
-
## Project Structure
|
10
|
-
|
11
|
-
```
|
12
|
-
src/
|
13
|
-
├── controllers/
|
14
|
-
│ ├── users.controller.ts
|
15
|
-
│ ├── posts.controller.ts
|
16
|
-
│ └── auth.controller.ts
|
17
|
-
├── middleware/
|
18
|
-
│ ├── auth.middleware.ts
|
19
|
-
│ └── validation.middleware.ts
|
20
|
-
├── schemas/
|
21
|
-
│ ├── user.schema.ts
|
22
|
-
│ └── post.schema.ts
|
23
|
-
├── services/
|
24
|
-
│ ├── user.service.ts
|
25
|
-
│ └── post.service.ts
|
26
|
-
└── server.ts
|
27
|
-
```
|
28
|
-
|
29
|
-
## Server Setup
|
30
|
-
|
31
|
-
```typescript
|
32
|
-
// src/server.ts
|
33
|
-
import { Server } from 'balda-js';
|
34
|
-
|
35
|
-
const server = new Server({
|
36
|
-
port: 3000,
|
37
|
-
host: 'localhost',
|
38
|
-
controllerPatterns: ['./src/controllers/**/*.ts'],
|
39
|
-
plugins: {
|
40
|
-
cors: {
|
41
|
-
origin: ['http://localhost:3000', 'http://localhost:3001'],
|
42
|
-
credentials: true
|
43
|
-
},
|
44
|
-
json: {
|
45
|
-
sizeLimit: '10mb',
|
46
|
-
strict: true
|
47
|
-
},
|
48
|
-
cookie: {
|
49
|
-
secret: process.env.COOKIE_SECRET || 'dev-secret',
|
50
|
-
secure: false,
|
51
|
-
httpOnly: true
|
52
|
-
},
|
53
|
-
helmet: {
|
54
|
-
contentSecurityPolicy: false
|
55
|
-
},
|
56
|
-
log: {
|
57
|
-
logRequest: true,
|
58
|
-
logResponse: true
|
59
|
-
}
|
60
|
-
},
|
61
|
-
swagger: {
|
62
|
-
type: 'standard',
|
63
|
-
models: {
|
64
|
-
User: {
|
65
|
-
type: 'object',
|
66
|
-
properties: {
|
67
|
-
id: { type: 'number' },
|
68
|
-
email: { type: 'string' },
|
69
|
-
name: { type: 'string' },
|
70
|
-
createdAt: { type: 'string', format: 'date-time' }
|
71
|
-
}
|
72
|
-
},
|
73
|
-
Post: {
|
74
|
-
type: 'object',
|
75
|
-
properties: {
|
76
|
-
id: { type: 'number' },
|
77
|
-
title: { type: 'string' },
|
78
|
-
content: { type: 'string' },
|
79
|
-
authorId: { type: 'number' },
|
80
|
-
createdAt: { type: 'string', format: 'date-time' }
|
81
|
-
}
|
82
|
-
}
|
83
|
-
}
|
84
|
-
}
|
85
|
-
});
|
86
|
-
|
87
|
-
// Global error handler
|
88
|
-
server.setErrorHandler((req, res, next, error) => {
|
89
|
-
console.error('Error:', error);
|
90
|
-
|
91
|
-
if (error.name === 'ValidationError') {
|
92
|
-
return res.badRequest({
|
93
|
-
error: 'Validation failed',
|
94
|
-
details: error.message
|
95
|
-
});
|
96
|
-
}
|
97
|
-
|
98
|
-
if (error.name === 'UnauthorizedError') {
|
99
|
-
return res.unauthorized({ error: 'Authentication required' });
|
100
|
-
}
|
101
|
-
|
102
|
-
res.internalServerError({ error: 'Internal server error' });
|
103
|
-
});
|
104
|
-
|
105
|
-
server.listen(({ port, host }) => {
|
106
|
-
console.log(`🚀 Server running on http://${host}:${port}`);
|
107
|
-
console.log(`📚 API Documentation: http://${host}:${port}/docs`);
|
108
|
-
});
|
109
|
-
```
|
110
|
-
|
111
|
-
## Data Schemas
|
112
|
-
|
113
|
-
```typescript
|
114
|
-
// src/schemas/user.schema.ts
|
115
|
-
import { Type } from '@sinclair/typebox';
|
116
|
-
|
117
|
-
export const CreateUserSchema = Type.Object({
|
118
|
-
email: Type.String({ format: 'email' }),
|
119
|
-
password: Type.String({ minLength: 8 }),
|
120
|
-
name: Type.String({ minLength: 1, maxLength: 100 })
|
121
|
-
});
|
122
|
-
|
123
|
-
export const UpdateUserSchema = Type.Partial(CreateUserSchema);
|
124
|
-
|
125
|
-
export const UserSchema = Type.Object({
|
126
|
-
id: Type.Number(),
|
127
|
-
email: Type.String({ format: 'email' }),
|
128
|
-
name: Type.String(),
|
129
|
-
createdAt: Type.String({ format: 'date-time' }),
|
130
|
-
updatedAt: Type.String({ format: 'date-time' })
|
131
|
-
});
|
132
|
-
|
133
|
-
export const LoginSchema = Type.Object({
|
134
|
-
email: Type.String({ format: 'email' }),
|
135
|
-
password: Type.String()
|
136
|
-
});
|
137
|
-
|
138
|
-
export type CreateUser = Static<typeof CreateUserSchema>;
|
139
|
-
export type UpdateUser = Static<typeof UpdateUserSchema>;
|
140
|
-
export type User = Static<typeof UserSchema>;
|
141
|
-
export type Login = Static<typeof LoginSchema>;
|
142
|
-
```
|
143
|
-
|
144
|
-
```typescript
|
145
|
-
// src/schemas/post.schema.ts
|
146
|
-
import { Type } from '@sinclair/typebox';
|
147
|
-
|
148
|
-
export const CreatePostSchema = Type.Object({
|
149
|
-
title: Type.String({ minLength: 1, maxLength: 200 }),
|
150
|
-
content: Type.String({ minLength: 1 }),
|
151
|
-
published: Type.Optional(Type.Boolean())
|
152
|
-
});
|
153
|
-
|
154
|
-
export const UpdatePostSchema = Type.Partial(CreatePostSchema);
|
155
|
-
|
156
|
-
export const PostSchema = Type.Object({
|
157
|
-
id: Type.Number(),
|
158
|
-
title: Type.String(),
|
159
|
-
content: Type.String(),
|
160
|
-
published: Type.Boolean(),
|
161
|
-
authorId: Type.Number(),
|
162
|
-
createdAt: Type.String({ format: 'date-time' }),
|
163
|
-
updatedAt: Type.String({ format: 'date-time' })
|
164
|
-
});
|
165
|
-
|
166
|
-
export type CreatePost = Static<typeof CreatePostSchema>;
|
167
|
-
export type UpdatePost = Static<typeof UpdatePostSchema>;
|
168
|
-
export type Post = Static<typeof PostSchema>;
|
169
|
-
```
|
170
|
-
|
171
|
-
## Services
|
172
|
-
|
173
|
-
```typescript
|
174
|
-
// src/services/user.service.ts
|
175
|
-
import { User, CreateUser, UpdateUser } from '../schemas/user.schema';
|
176
|
-
|
177
|
-
export class UserService {
|
178
|
-
private users: User[] = [];
|
179
|
-
private nextId = 1;
|
180
|
-
|
181
|
-
async findAll(): Promise<User[]> {
|
182
|
-
return this.users;
|
183
|
-
}
|
184
|
-
|
185
|
-
async findById(id: number): Promise<User | null> {
|
186
|
-
return this.users.find(user => user.id === id) || null;
|
187
|
-
}
|
188
|
-
|
189
|
-
async findByEmail(email: string): Promise<User | null> {
|
190
|
-
return this.users.find(user => user.email === email) || null;
|
191
|
-
}
|
192
|
-
|
193
|
-
async create(data: CreateUser): Promise<User> {
|
194
|
-
const existingUser = await this.findByEmail(data.email);
|
195
|
-
if (existingUser) {
|
196
|
-
throw new Error('User with this email already exists');
|
197
|
-
}
|
198
|
-
|
199
|
-
const user: User = {
|
200
|
-
id: this.nextId++,
|
201
|
-
email: data.email,
|
202
|
-
name: data.name,
|
203
|
-
createdAt: new Date().toISOString(),
|
204
|
-
updatedAt: new Date().toISOString()
|
205
|
-
};
|
206
|
-
|
207
|
-
this.users.push(user);
|
208
|
-
return user;
|
209
|
-
}
|
210
|
-
|
211
|
-
async update(id: number, data: UpdateUser): Promise<User | null> {
|
212
|
-
const userIndex = this.users.findIndex(user => user.id === id);
|
213
|
-
if (userIndex === -1) {
|
214
|
-
return null;
|
215
|
-
}
|
216
|
-
|
217
|
-
const updatedUser = {
|
218
|
-
...this.users[userIndex],
|
219
|
-
...data,
|
220
|
-
updatedAt: new Date().toISOString()
|
221
|
-
};
|
222
|
-
|
223
|
-
this.users[userIndex] = updatedUser;
|
224
|
-
return updatedUser;
|
225
|
-
}
|
226
|
-
|
227
|
-
async delete(id: number): Promise<boolean> {
|
228
|
-
const userIndex = this.users.findIndex(user => user.id === id);
|
229
|
-
if (userIndex === -1) {
|
230
|
-
return false;
|
231
|
-
}
|
232
|
-
|
233
|
-
this.users.splice(userIndex, 1);
|
234
|
-
return true;
|
235
|
-
}
|
236
|
-
}
|
237
|
-
|
238
|
-
export const userService = new UserService();
|
239
|
-
```
|
240
|
-
|
241
|
-
```typescript
|
242
|
-
// src/services/post.service.ts
|
243
|
-
import { Post, CreatePost, UpdatePost } from '../schemas/post.schema';
|
244
|
-
|
245
|
-
export class PostService {
|
246
|
-
private posts: Post[] = [];
|
247
|
-
private nextId = 1;
|
248
|
-
|
249
|
-
async findAll(published?: boolean): Promise<Post[]> {
|
250
|
-
if (published !== undefined) {
|
251
|
-
return this.posts.filter(post => post.published === published);
|
252
|
-
}
|
253
|
-
return this.posts;
|
254
|
-
}
|
255
|
-
|
256
|
-
async findById(id: number): Promise<Post | null> {
|
257
|
-
return this.posts.find(post => post.id === id) || null;
|
258
|
-
}
|
259
|
-
|
260
|
-
async findByAuthor(authorId: number): Promise<Post[]> {
|
261
|
-
return this.posts.filter(post => post.authorId === authorId);
|
262
|
-
}
|
263
|
-
|
264
|
-
async create(data: CreatePost, authorId: number): Promise<Post> {
|
265
|
-
const post: Post = {
|
266
|
-
id: this.nextId++,
|
267
|
-
title: data.title,
|
268
|
-
content: data.content,
|
269
|
-
published: data.published || false,
|
270
|
-
authorId,
|
271
|
-
createdAt: new Date().toISOString(),
|
272
|
-
updatedAt: new Date().toISOString()
|
273
|
-
};
|
274
|
-
|
275
|
-
this.posts.push(post);
|
276
|
-
return post;
|
277
|
-
}
|
278
|
-
|
279
|
-
async update(id: number, data: UpdatePost): Promise<Post | null> {
|
280
|
-
const postIndex = this.posts.findIndex(post => post.id === id);
|
281
|
-
if (postIndex === -1) {
|
282
|
-
return null;
|
283
|
-
}
|
284
|
-
|
285
|
-
const updatedPost = {
|
286
|
-
...this.posts[postIndex],
|
287
|
-
...data,
|
288
|
-
updatedAt: new Date().toISOString()
|
289
|
-
};
|
290
|
-
|
291
|
-
this.posts[postIndex] = updatedPost;
|
292
|
-
return updatedPost;
|
293
|
-
}
|
294
|
-
|
295
|
-
async delete(id: number): Promise<boolean> {
|
296
|
-
const postIndex = this.posts.findIndex(post => post.id === id);
|
297
|
-
if (postIndex === -1) {
|
298
|
-
return false;
|
299
|
-
}
|
300
|
-
|
301
|
-
this.posts.splice(postIndex, 1);
|
302
|
-
return true;
|
303
|
-
}
|
304
|
-
}
|
305
|
-
|
306
|
-
export const postService = new PostService();
|
307
|
-
```
|
308
|
-
|
309
|
-
## Middleware
|
310
|
-
|
311
|
-
```typescript
|
312
|
-
// src/middleware/auth.middleware.ts
|
313
|
-
import { Request, Response, NextFunction } from 'balda-js';
|
314
|
-
import { userService } from '../services/user.service';
|
315
|
-
|
316
|
-
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
317
|
-
const token = req.headers.authorization?.replace('Bearer ', '');
|
318
|
-
|
319
|
-
if (!token) {
|
320
|
-
return res.unauthorized({ error: 'Authentication token required' });
|
321
|
-
}
|
322
|
-
|
323
|
-
try {
|
324
|
-
// In a real app, you'd verify the JWT token here
|
325
|
-
// For this example, we'll use a simple user ID lookup
|
326
|
-
const userId = parseInt(token);
|
327
|
-
const user = await userService.findById(userId);
|
328
|
-
|
329
|
-
if (!user) {
|
330
|
-
return res.unauthorized({ error: 'Invalid authentication token' });
|
331
|
-
}
|
332
|
-
|
333
|
-
req.user = user;
|
334
|
-
next();
|
335
|
-
} catch (error) {
|
336
|
-
return res.unauthorized({ error: 'Invalid authentication token' });
|
337
|
-
}
|
338
|
-
};
|
339
|
-
|
340
|
-
export const optionalAuthMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
341
|
-
const token = req.headers.authorization?.replace('Bearer ', '');
|
342
|
-
|
343
|
-
if (token) {
|
344
|
-
try {
|
345
|
-
const userId = parseInt(token);
|
346
|
-
const user = await userService.findById(userId);
|
347
|
-
if (user) {
|
348
|
-
req.user = user;
|
349
|
-
}
|
350
|
-
} catch (error) {
|
351
|
-
// Ignore auth errors for optional auth
|
352
|
-
}
|
353
|
-
}
|
354
|
-
|
355
|
-
next();
|
356
|
-
};
|
357
|
-
```
|
358
|
-
|
359
|
-
## Controllers
|
360
|
-
|
361
|
-
```typescript
|
362
|
-
// src/controllers/users.controller.ts
|
363
|
-
import {
|
364
|
-
controller,
|
365
|
-
get,
|
366
|
-
post,
|
367
|
-
put,
|
368
|
-
del,
|
369
|
-
validate,
|
370
|
-
serialize,
|
371
|
-
middleware
|
372
|
-
} from 'balda-js';
|
373
|
-
import { Request, Response } from 'balda-js';
|
374
|
-
import {
|
375
|
-
CreateUserSchema,
|
376
|
-
UpdateUserSchema,
|
377
|
-
UserSchema,
|
378
|
-
LoginSchema
|
379
|
-
} from '../schemas/user.schema';
|
380
|
-
import { userService } from '../services/user.service';
|
381
|
-
import { authMiddleware } from '../middleware/auth.middleware';
|
382
|
-
|
383
|
-
@controller('/users')
|
384
|
-
export class UsersController {
|
385
|
-
@get('/')
|
386
|
-
@serialize(Type.Array(UserSchema))
|
387
|
-
async getAllUsers(req: Request, res: Response) {
|
388
|
-
const users = await userService.findAll();
|
389
|
-
res.json(users);
|
390
|
-
}
|
391
|
-
|
392
|
-
@get('/:id')
|
393
|
-
@serialize(UserSchema)
|
394
|
-
async getUserById(req: Request, res: Response) {
|
395
|
-
const id = parseInt(req.params.id);
|
396
|
-
const user = await userService.findById(id);
|
397
|
-
|
398
|
-
if (!user) {
|
399
|
-
return res.notFound({ error: 'User not found' });
|
400
|
-
}
|
401
|
-
|
402
|
-
res.json(user);
|
403
|
-
}
|
404
|
-
|
405
|
-
@post('/')
|
406
|
-
@validate.body(CreateUserSchema)
|
407
|
-
@serialize(UserSchema)
|
408
|
-
async createUser(req: Request, res: Response, body: CreateUser) {
|
409
|
-
try {
|
410
|
-
const user = await userService.create(body);
|
411
|
-
res.created(user);
|
412
|
-
} catch (error) {
|
413
|
-
return res.conflict({ error: error.message });
|
414
|
-
}
|
415
|
-
}
|
416
|
-
|
417
|
-
@put('/:id')
|
418
|
-
@middleware(authMiddleware)
|
419
|
-
@validate.body(UpdateUserSchema)
|
420
|
-
@serialize(UserSchema)
|
421
|
-
async updateUser(req: Request, res: Response, body: UpdateUser) {
|
422
|
-
const id = parseInt(req.params.id);
|
423
|
-
|
424
|
-
// Ensure users can only update their own profile
|
425
|
-
if (req.user.id !== id) {
|
426
|
-
return res.forbidden({ error: 'Cannot update other users' });
|
427
|
-
}
|
428
|
-
|
429
|
-
const user = await userService.update(id, body);
|
430
|
-
|
431
|
-
if (!user) {
|
432
|
-
return res.notFound({ error: 'User not found' });
|
433
|
-
}
|
434
|
-
|
435
|
-
res.json(user);
|
436
|
-
}
|
437
|
-
|
438
|
-
@del('/:id')
|
439
|
-
@middleware(authMiddleware)
|
440
|
-
async deleteUser(req: Request, res: Response) {
|
441
|
-
const id = parseInt(req.params.id);
|
442
|
-
|
443
|
-
// Ensure users can only delete their own account
|
444
|
-
if (req.user.id !== id) {
|
445
|
-
return res.forbidden({ error: 'Cannot delete other users' });
|
446
|
-
}
|
447
|
-
|
448
|
-
const deleted = await userService.delete(id);
|
449
|
-
|
450
|
-
if (!deleted) {
|
451
|
-
return res.notFound({ error: 'User not found' });
|
452
|
-
}
|
453
|
-
|
454
|
-
res.noContent();
|
455
|
-
}
|
456
|
-
|
457
|
-
@post('/login')
|
458
|
-
@validate.body(LoginSchema)
|
459
|
-
async login(req: Request, res: Response, body: Login) {
|
460
|
-
const user = await userService.findByEmail(body.email);
|
461
|
-
|
462
|
-
if (!user) {
|
463
|
-
return res.unauthorized({ error: 'Invalid credentials' });
|
464
|
-
}
|
465
|
-
|
466
|
-
// In a real app, you'd verify the password hash here
|
467
|
-
// For this example, we'll just return the user ID as a token
|
468
|
-
res.json({
|
469
|
-
token: user.id.toString(),
|
470
|
-
user: {
|
471
|
-
id: user.id,
|
472
|
-
email: user.email,
|
473
|
-
name: user.name
|
474
|
-
}
|
475
|
-
});
|
476
|
-
}
|
477
|
-
}
|
478
|
-
```
|
479
|
-
|
480
|
-
```typescript
|
481
|
-
// src/controllers/posts.controller.ts
|
482
|
-
import {
|
483
|
-
controller,
|
484
|
-
get,
|
485
|
-
post,
|
486
|
-
put,
|
487
|
-
del,
|
488
|
-
validate,
|
489
|
-
serialize,
|
490
|
-
middleware,
|
491
|
-
Request,
|
492
|
-
Response
|
493
|
-
} from 'balda-js';
|
494
|
-
import { Type, Static } from '@sinclair/typebox';
|
495
|
-
import {
|
496
|
-
CreatePostSchema,
|
497
|
-
UpdatePostSchema,
|
498
|
-
PostSchema
|
499
|
-
} from '../schemas/post.schema';
|
500
|
-
import { postService } from '../services/post.service';
|
501
|
-
import { authMiddleware, optionalAuthMiddleware } from '../middleware/auth.middleware';
|
502
|
-
|
503
|
-
@controller('/posts')
|
504
|
-
export class PostsController {
|
505
|
-
@get('/')
|
506
|
-
@middleware(optionalAuthMiddleware)
|
507
|
-
@serialize(Type.Array(PostSchema))
|
508
|
-
async getAllPosts(req: Request, res: Response) {
|
509
|
-
const published = req.query.published === 'true';
|
510
|
-
const posts = await postService.findAll(published);
|
511
|
-
res.json(posts);
|
512
|
-
}
|
513
|
-
|
514
|
-
@get('/:id')
|
515
|
-
@serialize(PostSchema)
|
516
|
-
async getPostById(req: Request, res: Response) {
|
517
|
-
const id = parseInt(req.params.id);
|
518
|
-
const post = await postService.findById(id);
|
519
|
-
|
520
|
-
if (!post) {
|
521
|
-
return res.notFound({ error: 'Post not found' });
|
522
|
-
}
|
523
|
-
|
524
|
-
// Only show unpublished posts to their authors
|
525
|
-
if (!post.published && (!req.user || req.user.id !== post.authorId)) {
|
526
|
-
return res.notFound({ error: 'Post not found' });
|
527
|
-
}
|
528
|
-
|
529
|
-
res.json(post);
|
530
|
-
}
|
531
|
-
|
532
|
-
@post('/')
|
533
|
-
@middleware(authMiddleware)
|
534
|
-
@validate.body(CreatePostSchema)
|
535
|
-
@serialize(PostSchema)
|
536
|
-
async createPost(req: Request, res: Response, body: Static<CreatePostSchema>) {
|
537
|
-
const post = await postService.create(body, req.user.id);
|
538
|
-
res.created(post);
|
539
|
-
}
|
540
|
-
|
541
|
-
@put('/:id')
|
542
|
-
@middleware(authMiddleware)
|
543
|
-
@validate.body(UpdatePostSchema)
|
544
|
-
@serialize(PostSchema)
|
545
|
-
async updatePost(req: Request, res: Response, body: UpdatePost) {
|
546
|
-
const id = parseInt(req.params.id);
|
547
|
-
const existingPost = await postService.findById(id);
|
548
|
-
|
549
|
-
if (!existingPost) {
|
550
|
-
return res.notFound({ error: 'Post not found' });
|
551
|
-
}
|
552
|
-
|
553
|
-
// Ensure users can only update their own posts
|
554
|
-
if (req.user.id !== existingPost.authorId) {
|
555
|
-
return res.forbidden({ error: 'Cannot update other users\' posts' });
|
556
|
-
}
|
557
|
-
|
558
|
-
const post = await postService.update(id, body);
|
559
|
-
res.json(post);
|
560
|
-
}
|
561
|
-
|
562
|
-
@del('/:id')
|
563
|
-
@middleware(authMiddleware)
|
564
|
-
async deletePost(req: Request, res: Response) {
|
565
|
-
const id = parseInt(req.params.id);
|
566
|
-
const existingPost = await postService.findById(id);
|
567
|
-
|
568
|
-
if (!existingPost) {
|
569
|
-
return res.notFound({ error: 'Post not found' });
|
570
|
-
}
|
571
|
-
|
572
|
-
// Ensure users can only delete their own posts
|
573
|
-
if (req.user.id !== existingPost.authorId) {
|
574
|
-
return res.forbidden({ error: 'Cannot delete other users\' posts' });
|
575
|
-
}
|
576
|
-
|
577
|
-
const deleted = await postService.delete(id);
|
578
|
-
res.noContent();
|
579
|
-
}
|
580
|
-
|
581
|
-
@get('/author/:authorId')
|
582
|
-
@serialize(Type.Array(PostSchema))
|
583
|
-
async getPostsByAuthor(req: Request, res: Response) {
|
584
|
-
const authorId = parseInt(req.params.authorId);
|
585
|
-
const posts = await postService.findByAuthor(authorId);
|
586
|
-
|
587
|
-
// Filter out unpublished posts unless the user is the author
|
588
|
-
const filteredPosts = posts.filter(post =>
|
589
|
-
post.published || (req.user && req.user.id === authorId)
|
590
|
-
);
|
591
|
-
|
592
|
-
res.json(filteredPosts);
|
593
|
-
}
|
594
|
-
}
|
595
|
-
```
|