fragment-ts 1.0.33 → 1.0.35
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/API.md +248 -38
- package/DOCS.md +327 -63
- package/NewCLIGENERATECOMMANDS.txt +5 -0
- package/README.md +168 -3
- package/USAGE.md +395 -2
- package/dist/cli/commands/build.command.d.ts.map +1 -1
- package/dist/cli/commands/build.command.js +20 -8
- package/dist/cli/commands/build.command.js.map +1 -1
- package/dist/cli/commands/diagnostics.command.d.ts +1 -2
- package/dist/cli/commands/diagnostics.command.d.ts.map +1 -1
- package/dist/cli/commands/diagnostics.command.js +37 -23
- package/dist/cli/commands/diagnostics.command.js.map +1 -1
- package/dist/cli/commands/generate.command.d.ts +5 -1
- package/dist/cli/commands/generate.command.d.ts.map +1 -1
- package/dist/cli/commands/generate.command.js +171 -39
- package/dist/cli/commands/generate.command.js.map +1 -1
- package/dist/cli/commands/init.command.d.ts.map +1 -1
- package/dist/cli/commands/init.command.js +98 -28
- package/dist/cli/commands/init.command.js.map +1 -1
- package/dist/cli/commands/migrate.command.d.ts +10 -17
- package/dist/cli/commands/migrate.command.d.ts.map +1 -1
- package/dist/cli/commands/migrate.command.js +133 -170
- package/dist/cli/commands/migrate.command.js.map +1 -1
- package/dist/cli/commands/serve.command.d.ts.map +1 -1
- package/dist/cli/commands/serve.command.js +9 -4
- package/dist/cli/commands/serve.command.js.map +1 -1
- package/dist/cli/commands/test.command.d.ts.map +1 -1
- package/dist/cli/commands/test.command.js +24 -6
- package/dist/cli/commands/test.command.js.map +1 -1
- package/dist/core/decorators/exception-filter.decorator.d.ts.map +1 -1
- package/dist/core/decorators/exception-filter.decorator.js +11 -4
- package/dist/core/decorators/exception-filter.decorator.js.map +1 -1
- package/dist/core/decorators/guard.decorator.d.ts.map +1 -1
- package/dist/core/decorators/guard.decorator.js +11 -4
- package/dist/core/decorators/guard.decorator.js.map +1 -1
- package/dist/core/decorators/interceptor.decorator.d.ts.map +1 -1
- package/dist/core/decorators/interceptor.decorator.js +10 -4
- package/dist/core/decorators/interceptor.decorator.js.map +1 -1
- package/dist/core/decorators/middleware.decorator.d.ts.map +1 -1
- package/dist/core/decorators/middleware.decorator.js +8 -4
- package/dist/core/decorators/middleware.decorator.js.map +1 -1
- package/dist/core/scanner/component-scanner.d.ts +12 -0
- package/dist/core/scanner/component-scanner.d.ts.map +1 -1
- package/dist/core/scanner/component-scanner.js +72 -14
- package/dist/core/scanner/component-scanner.js.map +1 -1
- package/dist/shared/config.utils.d.ts +58 -0
- package/dist/shared/config.utils.d.ts.map +1 -0
- package/dist/shared/config.utils.js +137 -0
- package/dist/shared/config.utils.js.map +1 -0
- package/dist/shared/env.utils.d.ts +27 -0
- package/dist/shared/env.utils.d.ts.map +1 -0
- package/dist/shared/env.utils.js +68 -0
- package/dist/shared/env.utils.js.map +1 -0
- package/dist/shared/tsconfig.utils.d.ts +122 -0
- package/dist/shared/tsconfig.utils.d.ts.map +1 -0
- package/dist/shared/tsconfig.utils.js +305 -0
- package/dist/shared/tsconfig.utils.js.map +1 -0
- package/dist/testing/runner.d.ts +9 -1
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js +50 -10
- package/dist/testing/runner.js.map +1 -1
- package/dist/typeorm/typeorm-module.d.ts +1 -0
- package/dist/typeorm/typeorm-module.d.ts.map +1 -1
- package/dist/typeorm/typeorm-module.js +193 -85
- package/dist/typeorm/typeorm-module.js.map +1 -1
- package/dist/web/application.d.ts +0 -1
- package/dist/web/application.d.ts.map +1 -1
- package/dist/web/application.js +4 -26
- package/dist/web/application.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/build.command.ts +24 -9
- package/src/cli/commands/diagnostics.command.ts +42 -30
- package/src/cli/commands/generate.command.ts +212 -52
- package/src/cli/commands/init.command.ts +100 -29
- package/src/cli/commands/migrate.command.ts +145 -198
- package/src/cli/commands/serve.command.ts +181 -170
- package/src/cli/commands/test.command.ts +25 -11
- package/src/core/decorators/exception-filter.decorator.ts +29 -6
- package/src/core/decorators/guard.decorator.ts +28 -5
- package/src/core/decorators/interceptor.decorator.ts +32 -6
- package/src/core/decorators/middleware.decorator.ts +30 -6
- package/src/core/scanner/component-scanner.ts +100 -18
- package/src/shared/config.utils.ts +148 -0
- package/src/shared/env.utils.ts +72 -0
- package/src/shared/tsconfig.utils.ts +360 -0
- package/src/testing/runner.ts +62 -14
- package/src/typeorm/typeorm-module.ts +209 -86
- package/src/web/application.ts +4 -33
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
1
3
|
# Fragment TS - A Modern TypeScript Framework
|
|
2
4
|
|
|
3
5
|
[](https://www.npmjs.com/package/fragment-ts)
|
|
@@ -15,6 +17,7 @@ Fragment TS is a lightweight, dependency injection-based framework for building
|
|
|
15
17
|
- 🔧 **Auto-configuration**: Smart component scanning and registration
|
|
16
18
|
- 🧪 **Testable Design**: Easily mock dependencies for unit testing
|
|
17
19
|
- 📦 **Production Ready**: Works with compiled JavaScript and TypeScript source
|
|
20
|
+
- 🛡️ **Enhancement Pipeline**: Middleware, guards, interceptors, and exception filters for advanced request processing
|
|
18
21
|
|
|
19
22
|
## Installation
|
|
20
23
|
|
|
@@ -109,7 +112,7 @@ export class UserRepository {
|
|
|
109
112
|
|
|
110
113
|
### 5. Configure TypeORM:
|
|
111
114
|
|
|
112
|
-
```
|
|
115
|
+
```json
|
|
113
116
|
// fragment.json
|
|
114
117
|
{
|
|
115
118
|
"type": "mysql",
|
|
@@ -123,6 +126,169 @@ export class UserRepository {
|
|
|
123
126
|
}
|
|
124
127
|
```
|
|
125
128
|
|
|
129
|
+
## Enhancement Pipeline
|
|
130
|
+
|
|
131
|
+
Fragment TS provides a powerful request processing pipeline with four types of enhancements that can be applied at both **class** and **method** levels:
|
|
132
|
+
|
|
133
|
+
### Middleware (`@FragmentMiddleware`)
|
|
134
|
+
|
|
135
|
+
Apply Express-compatible middleware to log requests, handle authentication headers, or modify the request/response cycle.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// src/middlewares/logging.middleware.ts
|
|
139
|
+
import { Injectable } from 'fragment-ts';
|
|
140
|
+
import { Request, Response, NextFunction } from 'express';
|
|
141
|
+
|
|
142
|
+
@Injectable()
|
|
143
|
+
export class LoggingMiddleware {
|
|
144
|
+
use(req: Request, res: Response, next: NextFunction): void {
|
|
145
|
+
console.log(`📥 ${req.method} ${req.path}`);
|
|
146
|
+
next();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Usage
|
|
151
|
+
import { Controller, Get } from 'fragment-ts';
|
|
152
|
+
import { LoggingMiddleware } from '../middlewares/logging.middleware';
|
|
153
|
+
|
|
154
|
+
@FragmentMiddleware(LoggingMiddleware)
|
|
155
|
+
@Controller('/api')
|
|
156
|
+
export class AppController {
|
|
157
|
+
@Get('/health')
|
|
158
|
+
health() {
|
|
159
|
+
return { status: 'OK' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Guards (`@FragmentGuard`)
|
|
165
|
+
|
|
166
|
+
Control access to routes with authorization logic. Guards run after middleware but before the route handler.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// src/guards/auth.guard.ts
|
|
170
|
+
import { Injectable } from 'fragment-ts';
|
|
171
|
+
import { Request } from 'express';
|
|
172
|
+
|
|
173
|
+
@Injectable()
|
|
174
|
+
export class AuthGuard {
|
|
175
|
+
canActivate(req: Request): boolean {
|
|
176
|
+
const token = req.headers.authorization;
|
|
177
|
+
return token === 'Bearer valid-token';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Usage
|
|
182
|
+
import { Controller, Get } from 'fragment-ts';
|
|
183
|
+
import { AuthGuard } from '../guards/auth.guard';
|
|
184
|
+
|
|
185
|
+
@FragmentGuard(AuthGuard)
|
|
186
|
+
@Controller('/secure')
|
|
187
|
+
export class SecureController {
|
|
188
|
+
@Get('/data')
|
|
189
|
+
getData() {
|
|
190
|
+
return { secret: 'Protected data' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Interceptors (`@FragmentInterceptor`)
|
|
196
|
+
|
|
197
|
+
Transform the response data before it's sent to the client. Interceptors run after the route handler executes.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// src/interceptors/transform.interceptor.ts
|
|
201
|
+
import { Injectable } from 'fragment-ts';
|
|
202
|
+
import { Request, Response } from 'express';
|
|
203
|
+
|
|
204
|
+
@Injectable()
|
|
205
|
+
export class TransformInterceptor {
|
|
206
|
+
intercept(req: Request, res: Response, result: any): any {
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
result,
|
|
210
|
+
timestamp: new Date().toISOString()
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Usage
|
|
216
|
+
import { Controller, Get } from 'fragment-ts';
|
|
217
|
+
import { TransformInterceptor } from '../interceptors/transform.interceptor';
|
|
218
|
+
|
|
219
|
+
@FragmentInterceptor(TransformInterceptor)
|
|
220
|
+
@Controller('/api')
|
|
221
|
+
export class DataController {
|
|
222
|
+
@Get('/info')
|
|
223
|
+
getInfo() {
|
|
224
|
+
return { message: 'Hello World' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Exception Filters (`@FragmentExceptionFilter`)
|
|
230
|
+
|
|
231
|
+
Handle errors gracefully with custom error responses. Filters are checked in method → controller → application order.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// src/filters/http-exception.filter.ts
|
|
235
|
+
import { Injectable } from 'fragment-ts';
|
|
236
|
+
import { Request, Response } from 'express';
|
|
237
|
+
|
|
238
|
+
@Injectable()
|
|
239
|
+
export class HttpExceptionFilter {
|
|
240
|
+
catch(exception: Error, req: Request, res: Response): void {
|
|
241
|
+
if (exception.name === 'ValidationError') {
|
|
242
|
+
res.status(400).json({ error: 'Validation failed', message: exception.message });
|
|
243
|
+
} else if (exception.name === 'NotFoundError') {
|
|
244
|
+
res.status(404).json({ error: 'Not found', message: exception.message });
|
|
245
|
+
} else {
|
|
246
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Usage
|
|
252
|
+
import { FragmentApplication } from 'fragment-ts';
|
|
253
|
+
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
|
254
|
+
|
|
255
|
+
@FragmentApplication({
|
|
256
|
+
port: 3000,
|
|
257
|
+
autoScan: true,
|
|
258
|
+
})
|
|
259
|
+
@FragmentExceptionFilter(HttpExceptionFilter)
|
|
260
|
+
export class Application {
|
|
261
|
+
constructor() {
|
|
262
|
+
console.log('Application starting...');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Combined Usage
|
|
268
|
+
|
|
269
|
+
All enhancement decorators can be combined and applied at different levels:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
@FragmentMiddleware(LoggingMiddleware)
|
|
273
|
+
@FragmentGuard(AuthGuard)
|
|
274
|
+
@FragmentInterceptor(TransformInterceptor)
|
|
275
|
+
@Controller('/api')
|
|
276
|
+
export class ComplexController {
|
|
277
|
+
@Get('/public')
|
|
278
|
+
@FragmentGuard(PublicGuard) // Overrides class-level guard
|
|
279
|
+
publicData() {
|
|
280
|
+
return { message: 'Public data' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@Post('/admin')
|
|
284
|
+
@FragmentGuard(AdminGuard) // Additional guard
|
|
285
|
+
@FragmentInterceptor(CacheInterceptor) // Additional interceptor
|
|
286
|
+
createAdminData(@Body() data: any) {
|
|
287
|
+
return { created: true, data };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
126
292
|
## Running the Application
|
|
127
293
|
|
|
128
294
|
```bash
|
|
@@ -151,5 +317,4 @@ For issues and feature requests, please [open an issue](https://github.com/youru
|
|
|
151
317
|
|
|
152
318
|
## License
|
|
153
319
|
|
|
154
|
-
MIT © [
|
|
155
|
-
```
|
|
320
|
+
MIT © [Digitwhale Innovations](https://digitwhale.com)
|
package/USAGE.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
1
3
|
# Usage Guide
|
|
2
4
|
|
|
3
5
|
This guide demonstrates practical usage patterns for Fragment TS framework.
|
|
@@ -19,8 +21,16 @@ src/
|
|
|
19
21
|
│ └── user.repository.ts
|
|
20
22
|
├── entities/
|
|
21
23
|
│ └── user.entity.ts
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
├── dtos/
|
|
25
|
+
│ └── create-user.dto.ts
|
|
26
|
+
├── middlewares/
|
|
27
|
+
│ └── logging.middleware.ts
|
|
28
|
+
├── guards/
|
|
29
|
+
│ └── auth.guard.ts
|
|
30
|
+
├── interceptors/
|
|
31
|
+
│ └── transform.interceptor.ts
|
|
32
|
+
└── filters/
|
|
33
|
+
└── http-exception.filter.ts
|
|
24
34
|
```
|
|
25
35
|
|
|
26
36
|
## Dependency Injection
|
|
@@ -160,6 +170,282 @@ export class ProductController {
|
|
|
160
170
|
}
|
|
161
171
|
```
|
|
162
172
|
|
|
173
|
+
## Enhancement Pipeline
|
|
174
|
+
|
|
175
|
+
### Middleware Implementation
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// src/middlewares/request-id.middleware.ts
|
|
179
|
+
import { Injectable } from 'fragment-ts';
|
|
180
|
+
import { Request, Response, NextFunction } from 'express';
|
|
181
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
182
|
+
|
|
183
|
+
@Injectable()
|
|
184
|
+
export class RequestIdMiddleware {
|
|
185
|
+
use(req: Request, res: Response, next: NextFunction): void {
|
|
186
|
+
const requestId = req.headers['x-request-id'] || uuidv4();
|
|
187
|
+
req.headers['x-request-id'] = requestId as string;
|
|
188
|
+
res.setHeader('X-Request-ID', requestId);
|
|
189
|
+
next();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/middlewares/logging.middleware.ts
|
|
194
|
+
import { Injectable } from 'fragment-ts';
|
|
195
|
+
import { Request, Response, NextFunction } from 'express';
|
|
196
|
+
|
|
197
|
+
@Injectable()
|
|
198
|
+
export class LoggingMiddleware {
|
|
199
|
+
use(req: Request, res: Response, next: NextFunction): void {
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
console.log(`📥 [${req.method}] ${req.path} - ${req.ip || 'unknown'}`);
|
|
202
|
+
|
|
203
|
+
res.on('finish', () => {
|
|
204
|
+
const duration = Date.now() - start;
|
|
205
|
+
console.log(`📤 [${req.method}] ${req.path} → ${res.statusCode} (${duration}ms)`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
next();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Guard Implementation
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// src/guards/role.guard.ts
|
|
217
|
+
import { Injectable } from 'fragment-ts';
|
|
218
|
+
import { Request } from 'express';
|
|
219
|
+
|
|
220
|
+
@Injectable()
|
|
221
|
+
export class RoleGuard {
|
|
222
|
+
canActivate(req: Request): boolean {
|
|
223
|
+
const userRole = req.headers['x-user-role'];
|
|
224
|
+
const requiredRoles = ['admin', 'moderator'];
|
|
225
|
+
return requiredRoles.includes(userRole as string);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/guards/api-key.guard.ts
|
|
230
|
+
import { Injectable } from 'fragment-ts';
|
|
231
|
+
import { Request } from 'express';
|
|
232
|
+
|
|
233
|
+
@Injectable()
|
|
234
|
+
export class ApiKeyGuard {
|
|
235
|
+
canActivate(req: Request): boolean {
|
|
236
|
+
const apiKey = req.headers['x-api-key'];
|
|
237
|
+
const validKeys = process.env.VALID_API_KEYS?.split(',') || [];
|
|
238
|
+
return validKeys.includes(apiKey as string);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Interceptor Implementation
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// src/interceptors/cache.interceptor.ts
|
|
247
|
+
import { Injectable } from 'fragment-ts';
|
|
248
|
+
import { Request, Response } from 'express';
|
|
249
|
+
import { CacheService } from '../services/cache.service';
|
|
250
|
+
|
|
251
|
+
@Injectable()
|
|
252
|
+
export class CacheInterceptor {
|
|
253
|
+
constructor(private cacheService: CacheService) {}
|
|
254
|
+
|
|
255
|
+
async intercept(req: Request, res: Response, result: any): Promise<any> {
|
|
256
|
+
const cacheKey = `${req.method}:${req.path}`;
|
|
257
|
+
|
|
258
|
+
// Store in cache for GET requests
|
|
259
|
+
if (req.method === 'GET') {
|
|
260
|
+
await this.cacheService.set(cacheKey, result, 300); // 5 minutes
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/interceptors/validation.interceptor.ts
|
|
268
|
+
import { Injectable } from 'fragment-ts';
|
|
269
|
+
import { Request, Response } from 'express';
|
|
270
|
+
import { validate } from 'class-validator';
|
|
271
|
+
|
|
272
|
+
@Injectable()
|
|
273
|
+
export class ValidationInterceptor {
|
|
274
|
+
async intercept(req: Request, res: Response, result: any): Promise<any> {
|
|
275
|
+
// Validate response data if it's an object with validation decorators
|
|
276
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
277
|
+
const errors = await validate(result);
|
|
278
|
+
if (errors.length > 0) {
|
|
279
|
+
console.warn('Response validation failed:', errors);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Exception Filter Implementation
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// src/filters/validation-exception.filter.ts
|
|
291
|
+
import { Injectable } from 'fragment-ts';
|
|
292
|
+
import { Request, Response } from 'express';
|
|
293
|
+
import { ValidationError } from 'class-validator';
|
|
294
|
+
|
|
295
|
+
@Injectable()
|
|
296
|
+
export class ValidationExceptionFilter {
|
|
297
|
+
catch(exception: Error, req: Request, res: Response): void {
|
|
298
|
+
if (exception instanceof ValidationError) {
|
|
299
|
+
const errors = this.formatValidationErrors(exception);
|
|
300
|
+
res.status(400).json({
|
|
301
|
+
statusCode: 400,
|
|
302
|
+
error: 'Bad Request',
|
|
303
|
+
message: 'Validation failed',
|
|
304
|
+
details: errors
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private formatValidationErrors(error: ValidationError): Record<string, string[]> {
|
|
310
|
+
const errors: Record<string, string[]> = {};
|
|
311
|
+
for (const constraint of Object.values(error.constraints || {})) {
|
|
312
|
+
const property = error.property;
|
|
313
|
+
if (!errors[property]) errors[property] = [];
|
|
314
|
+
errors[property].push(constraint);
|
|
315
|
+
}
|
|
316
|
+
return errors;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/filters/database-exception.filter.ts
|
|
321
|
+
import { Injectable } from 'fragment-ts';
|
|
322
|
+
import { Request, Response } from 'express';
|
|
323
|
+
|
|
324
|
+
@Injectable()
|
|
325
|
+
export class DatabaseExceptionFilter {
|
|
326
|
+
catch(exception: Error, req: Request, res: Response): void {
|
|
327
|
+
if (exception.name === 'QueryFailedError' || exception.message.includes('database')) {
|
|
328
|
+
res.status(500).json({
|
|
329
|
+
statusCode: 500,
|
|
330
|
+
error: 'Database Error',
|
|
331
|
+
message: 'An error occurred while accessing the database'
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Applying Enhancements
|
|
339
|
+
|
|
340
|
+
#### Class-Level Enhancements
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import { Controller, Get } from 'fragment-ts';
|
|
344
|
+
import { LoggingMiddleware } from '../middlewares/logging.middleware';
|
|
345
|
+
import { AuthGuard } from '../guards/auth.guard';
|
|
346
|
+
import { TransformInterceptor } from '../interceptors/transform.interceptor';
|
|
347
|
+
import { HttpExceptionFilter } from '../filters/http-exception.filter';
|
|
348
|
+
|
|
349
|
+
@FragmentMiddleware(LoggingMiddleware)
|
|
350
|
+
@FragmentGuard(AuthGuard)
|
|
351
|
+
@FragmentInterceptor(TransformInterceptor)
|
|
352
|
+
@FragmentExceptionFilter(HttpExceptionFilter)
|
|
353
|
+
@Controller('/api/v1')
|
|
354
|
+
export class ApiController {
|
|
355
|
+
@Get('/users')
|
|
356
|
+
async getUsers() {
|
|
357
|
+
return [{ id: 1, name: 'John' }];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@Get('/status')
|
|
361
|
+
async getStatus() {
|
|
362
|
+
return { status: 'healthy' };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Method-Level Enhancements
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { Controller, Get, Post, Body } from 'fragment-ts';
|
|
371
|
+
import { ApiKeyGuard } from '../guards/api-key.guard';
|
|
372
|
+
import { CacheInterceptor } from '../interceptors/cache.interceptor';
|
|
373
|
+
import { ValidationExceptionFilter } from '../filters/validation-exception.filter';
|
|
374
|
+
import { CreateUserDto } from '../dtos/create-user.dto';
|
|
375
|
+
|
|
376
|
+
@Controller('/api')
|
|
377
|
+
export class UserController {
|
|
378
|
+
@Get('/public-data')
|
|
379
|
+
@FragmentGuard(ApiKeyGuard) // Only this method requires API key
|
|
380
|
+
getPublicData() {
|
|
381
|
+
return { data: 'public information' };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@Get('/users')
|
|
385
|
+
@FragmentInterceptor(CacheInterceptor) // Cache only this endpoint
|
|
386
|
+
async getUsers() {
|
|
387
|
+
// Fetch from database
|
|
388
|
+
return [{ id: 1, name: 'John' }];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@Post('/users')
|
|
392
|
+
@FragmentExceptionFilter(ValidationExceptionFilter) // Specific error handling
|
|
393
|
+
async createUser(@Body() userData: CreateUserDto) {
|
|
394
|
+
// Create user logic
|
|
395
|
+
return { id: 1, ...userData };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
#### Mixed Class and Method Enhancements
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
@FragmentMiddleware(LoggingMiddleware)
|
|
404
|
+
@FragmentGuard(AuthGuard) // Applied to all methods
|
|
405
|
+
@Controller('/admin')
|
|
406
|
+
export class AdminController {
|
|
407
|
+
@Get('/dashboard')
|
|
408
|
+
getDashboard() {
|
|
409
|
+
// Uses both LoggingMiddleware and AuthGuard
|
|
410
|
+
return { dashboard: 'data' };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@Get('/public-stats')
|
|
414
|
+
@FragmentGuard(PublicGuard) // Overrides class-level AuthGuard
|
|
415
|
+
getPublicStats() {
|
|
416
|
+
// Uses LoggingMiddleware and PublicGuard (not AuthGuard)
|
|
417
|
+
return { stats: 'public' };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@Post('/settings')
|
|
421
|
+
@FragmentInterceptor(AuditInterceptor) // Additional interceptor
|
|
422
|
+
updateSettings(@Body() settings: any) {
|
|
423
|
+
// Uses LoggingMiddleware, AuthGuard, and AuditInterceptor
|
|
424
|
+
return { updated: true };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### Application-Level Exception Filters
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { FragmentApplication } from 'fragment-ts';
|
|
433
|
+
import { GlobalExceptionFilter } from './filters/global-exception.filter';
|
|
434
|
+
import { DatabaseExceptionFilter } from './filters/database-exception.filter';
|
|
435
|
+
|
|
436
|
+
@FragmentApplication({
|
|
437
|
+
port: 3000,
|
|
438
|
+
autoScan: true
|
|
439
|
+
})
|
|
440
|
+
@FragmentExceptionFilter(DatabaseExceptionFilter)
|
|
441
|
+
@FragmentExceptionFilter(GlobalExceptionFilter)
|
|
442
|
+
export class Application {
|
|
443
|
+
constructor() {
|
|
444
|
+
console.log('Application starting...');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
163
449
|
## Repositories
|
|
164
450
|
|
|
165
451
|
### TypeORM Repository Injection
|
|
@@ -331,6 +617,101 @@ describe('UserService', () => {
|
|
|
331
617
|
});
|
|
332
618
|
```
|
|
333
619
|
|
|
620
|
+
### Testing Guards
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
import { AuthGuard } from '../guards/auth.guard';
|
|
624
|
+
|
|
625
|
+
describe('AuthGuard', () => {
|
|
626
|
+
let guard: AuthGuard;
|
|
627
|
+
|
|
628
|
+
beforeEach(() => {
|
|
629
|
+
guard = new AuthGuard();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should allow valid tokens', () => {
|
|
633
|
+
const mockReq = { headers: { authorization: 'Bearer valid-token' } } as any;
|
|
634
|
+
expect(guard.canActivate(mockReq)).toBe(true);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should reject invalid tokens', () => {
|
|
638
|
+
const mockReq = { headers: { authorization: 'Bearer invalid' } } as any;
|
|
639
|
+
expect(guard.canActivate(mockReq)).toBe(false);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('should reject missing tokens', () => {
|
|
643
|
+
const mockReq = { headers: {} } as any;
|
|
644
|
+
expect(guard.canActivate(mockReq)).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Testing Interceptors
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
import { TransformInterceptor } from '../interceptors/transform.interceptor';
|
|
653
|
+
|
|
654
|
+
describe('TransformInterceptor', () => {
|
|
655
|
+
let interceptor: TransformInterceptor;
|
|
656
|
+
let mockReq: any;
|
|
657
|
+
let mockRes: any;
|
|
658
|
+
|
|
659
|
+
beforeEach(() => {
|
|
660
|
+
interceptor = new TransformInterceptor();
|
|
661
|
+
mockReq = { path: '/test', method: 'GET' };
|
|
662
|
+
mockRes = {};
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should wrap result with metadata', () => {
|
|
666
|
+
const originalResult = { data: 'test' };
|
|
667
|
+
const result = interceptor.intercept(mockReq, mockRes, originalResult);
|
|
668
|
+
|
|
669
|
+
expect(result).toHaveProperty('success', true);
|
|
670
|
+
expect(result).toHaveProperty('data', originalResult);
|
|
671
|
+
expect(result).toHaveProperty('timestamp');
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Integration Testing with Enhancements
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
import request from 'supertest';
|
|
680
|
+
import { FragmentWebApplication } from 'fragment-ts';
|
|
681
|
+
import { Application } from '../app';
|
|
682
|
+
|
|
683
|
+
describe('API with Enhancements', () => {
|
|
684
|
+
let app: FragmentWebApplication;
|
|
685
|
+
|
|
686
|
+
beforeAll(async () => {
|
|
687
|
+
app = new FragmentWebApplication();
|
|
688
|
+
await app.bootstrap(Application);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should log requests with middleware', async () => {
|
|
692
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
693
|
+
|
|
694
|
+
await request(app.getExpressApp())
|
|
695
|
+
.get('/api/status')
|
|
696
|
+
.expect(200);
|
|
697
|
+
|
|
698
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('📥 [GET] /api/status'));
|
|
699
|
+
consoleSpy.mockRestore();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('should require authentication with guard', async () => {
|
|
703
|
+
await request(app.getExpressApp())
|
|
704
|
+
.get('/admin/dashboard')
|
|
705
|
+
.expect(403); // Assuming guard returns false without proper auth
|
|
706
|
+
|
|
707
|
+
await request(app.getExpressApp())
|
|
708
|
+
.get('/admin/dashboard')
|
|
709
|
+
.set('Authorization', 'Bearer valid-token')
|
|
710
|
+
.expect(200);
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
```
|
|
714
|
+
|
|
334
715
|
## Environment Configuration
|
|
335
716
|
|
|
336
717
|
### fragment.json Example
|
|
@@ -361,6 +742,7 @@ DB_HOST=production-db.example.com
|
|
|
361
742
|
DB_USER=app_user
|
|
362
743
|
DB_PASSWORD=secure_password
|
|
363
744
|
FEATURE_FLAG_NEW_UI=true
|
|
745
|
+
VALID_API_KEYS=key1,key2,key3
|
|
364
746
|
```
|
|
365
747
|
|
|
366
748
|
## Production Deployment
|
|
@@ -417,6 +799,11 @@ CMD ["node", "dist/app.js"]
|
|
|
417
799
|
- Check route methods have appropriate HTTP decorators (`@Get()`, `@Post()`, etc.)
|
|
418
800
|
- Ensure controller classes are exported and discoverable
|
|
419
801
|
|
|
802
|
+
4. **Enhancement decorators not working**:
|
|
803
|
+
- Ensure enhancement classes have `@Injectable()` or similar decorator
|
|
804
|
+
- Verify enhancement classes implement the correct interface
|
|
805
|
+
- Check that metadata is being stored as arrays (not single values)
|
|
806
|
+
|
|
420
807
|
### Debugging Tips
|
|
421
808
|
|
|
422
809
|
1. Enable detailed logging by setting environment variable:
|
|
@@ -440,3 +827,9 @@ CMD ["node", "dist/app.js"]
|
|
|
440
827
|
console.log('Discovered classes:', storage.getAllClasses());
|
|
441
828
|
console.log('Discovered methods:', storage.getAllMethods());
|
|
442
829
|
```
|
|
830
|
+
|
|
831
|
+
4. Debug enhancement pipeline execution:
|
|
832
|
+
```typescript
|
|
833
|
+
// Add console logs to your middleware/guards/interceptors
|
|
834
|
+
// to verify they're being called in the expected order
|
|
835
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.command.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"build.command.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,qBAAa,YAAY;IACvB,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;WAW1B,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA4DjD,OAAO,CAAC,MAAM,CAAC,OAAO;IA+BtB,OAAO,CAAC,MAAM,CAAC,YAAY;CAqC5B"}
|
|
@@ -42,6 +42,7 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const chalk_1 = __importDefault(require("chalk"));
|
|
44
44
|
const ora_1 = __importDefault(require("ora"));
|
|
45
|
+
const tsconfig_utils_1 = require("../../shared/tsconfig.utils");
|
|
45
46
|
class BuildCommand {
|
|
46
47
|
static register(program) {
|
|
47
48
|
program
|
|
@@ -64,13 +65,25 @@ class BuildCommand {
|
|
|
64
65
|
console.log(chalk_1.default.cyan(" npm install --save-dev typescript"));
|
|
65
66
|
process.exit(1);
|
|
66
67
|
}
|
|
67
|
-
// Check if tsconfig.json exists
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(tsconfigPath)) {
|
|
68
|
+
// Check if tsconfig.json exists using utility
|
|
69
|
+
if (!tsconfig_utils_1.TsConfigUtils.exists()) {
|
|
70
70
|
spinner.fail("tsconfig.json not found");
|
|
71
71
|
console.log(chalk_1.default.yellow("\nCreate a tsconfig.json file in your project root"));
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
|
+
// Validate required compiler options for Fragment
|
|
75
|
+
if (!tsconfig_utils_1.TsConfigUtils.hasDecoratorSupport()) {
|
|
76
|
+
spinner.fail("Required TypeScript compiler options not enabled");
|
|
77
|
+
console.log(chalk_1.default.yellow("\nYour tsconfig.json must include:"));
|
|
78
|
+
console.log(chalk_1.default.cyan(' "compilerOptions": {'));
|
|
79
|
+
console.log(chalk_1.default.cyan(' "experimentalDecorators": true,'));
|
|
80
|
+
console.log(chalk_1.default.cyan(' "emitDecoratorMetadata": true'));
|
|
81
|
+
console.log(chalk_1.default.cyan(" }"));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
// Get output directory from tsconfig instead of hardcoding
|
|
85
|
+
const outDir = tsconfig_utils_1.TsConfigUtils.getOutDir();
|
|
86
|
+
const relativeOutDir = path.relative(process.cwd(), outDir);
|
|
74
87
|
// Run TypeScript compiler
|
|
75
88
|
spinner.text = "Compiling TypeScript...";
|
|
76
89
|
(0, child_process_1.execSync)(`"${tscPath}"`, {
|
|
@@ -80,9 +93,9 @@ class BuildCommand {
|
|
|
80
93
|
spinner.succeed("Build completed successfully!");
|
|
81
94
|
if (options.analyze) {
|
|
82
95
|
console.log(chalk_1.default.blue("\n📊 Bundle Analysis:"));
|
|
83
|
-
this.analyzeBuild();
|
|
96
|
+
this.analyzeBuild(outDir); // Pass the actual output directory
|
|
84
97
|
}
|
|
85
|
-
console.log(chalk_1.default.green(
|
|
98
|
+
console.log(chalk_1.default.green(`\n✨ Build output: ${relativeOutDir}/`));
|
|
86
99
|
}
|
|
87
100
|
catch (error) {
|
|
88
101
|
spinner.fail("Build failed");
|
|
@@ -111,10 +124,9 @@ class BuildCommand {
|
|
|
111
124
|
}
|
|
112
125
|
return null;
|
|
113
126
|
}
|
|
114
|
-
static analyzeBuild() {
|
|
115
|
-
const distPath = path.join(process.cwd(), "dist");
|
|
127
|
+
static analyzeBuild(distPath) {
|
|
116
128
|
if (!fs.existsSync(distPath)) {
|
|
117
|
-
console.log(chalk_1.default.yellow(
|
|
129
|
+
console.log(chalk_1.default.yellow(` No ${path.basename(distPath)}/ directory found`));
|
|
118
130
|
return;
|
|
119
131
|
}
|
|
120
132
|
let totalSize = 0;
|