@avleon/core 0.0.42-rc0.1 → 0.0.44
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 +25 -22
- package/dist/helpers.d.ts +10 -0
- package/dist/helpers.js +127 -0
- package/dist/icore.d.ts +1 -0
- package/dist/icore.js +46 -34
- package/dist/params.js +33 -26
- package/dist/queue.d.ts +16 -14
- package/dist/queue.js +76 -63
- package/dist/queue.test.js +19 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ Avleon is a powerful, TypeScript-based web framework built on top of Fastify, de
|
|
|
38
38
|
- [mapPut](#mapput)
|
|
39
39
|
- [mapDelete](#mapdelete)
|
|
40
40
|
- [Testing](#testing)
|
|
41
|
+
- [WebSocket](#websocket-intregation-socketio)
|
|
41
42
|
|
|
42
43
|
## Features
|
|
43
44
|
|
|
@@ -68,7 +69,6 @@ pnpm dlx @avleon/cli new myapp
|
|
|
68
69
|
## Quick Start
|
|
69
70
|
|
|
70
71
|
### Minimal
|
|
71
|
-
|
|
72
72
|
```typescript
|
|
73
73
|
import { Avleon } from "@avleon/core";
|
|
74
74
|
|
|
@@ -78,7 +78,6 @@ app.run(); // or app.run(3000);
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### Controller Based
|
|
81
|
-
|
|
82
81
|
```typescript
|
|
83
82
|
import { Avleon, ApiController, Get, Results } from "@avleon/core";
|
|
84
83
|
|
|
@@ -100,7 +99,6 @@ app.run();
|
|
|
100
99
|
## Core Concepts
|
|
101
100
|
|
|
102
101
|
### Application Creation
|
|
103
|
-
|
|
104
102
|
Avleon provides a builder pattern for creating applications:
|
|
105
103
|
|
|
106
104
|
```typescript
|
|
@@ -118,7 +116,6 @@ app.run(); // or app.run(port)
|
|
|
118
116
|
```
|
|
119
117
|
|
|
120
118
|
### Controllers
|
|
121
|
-
|
|
122
119
|
Controllers are the entry points for your API requests. They are defined using the `@ApiController` decorator:
|
|
123
120
|
|
|
124
121
|
```typescript
|
|
@@ -129,7 +126,6 @@ class UserController {
|
|
|
129
126
|
```
|
|
130
127
|
|
|
131
128
|
### Route Methods
|
|
132
|
-
|
|
133
129
|
Define HTTP methods using decorators:
|
|
134
130
|
|
|
135
131
|
```typescript
|
|
@@ -155,7 +151,6 @@ async deleteUser(@Param('id') id: string) {
|
|
|
155
151
|
```
|
|
156
152
|
|
|
157
153
|
### Parameter Decorators
|
|
158
|
-
|
|
159
154
|
Extract data from requests using parameter decorators:
|
|
160
155
|
|
|
161
156
|
```typescript
|
|
@@ -190,7 +185,6 @@ async uploadFiles(
|
|
|
190
185
|
``` -->
|
|
191
186
|
|
|
192
187
|
### Error Handling
|
|
193
|
-
|
|
194
188
|
Return standardized responses using the `HttpResponse` and `HttpExceptions` class:
|
|
195
189
|
|
|
196
190
|
```typescript
|
|
@@ -207,7 +201,6 @@ async getUser(@Param('id') id: string) {
|
|
|
207
201
|
```
|
|
208
202
|
|
|
209
203
|
### Middleware
|
|
210
|
-
|
|
211
204
|
Create and apply middleware for cross-cutting concerns:
|
|
212
205
|
|
|
213
206
|
```typescript
|
|
@@ -240,7 +233,6 @@ class UserController {
|
|
|
240
233
|
```
|
|
241
234
|
|
|
242
235
|
### Authentication & Authorization
|
|
243
|
-
|
|
244
236
|
Secure your API with authentication and authorization:
|
|
245
237
|
|
|
246
238
|
```typescript
|
|
@@ -293,7 +285,6 @@ class AdminController {
|
|
|
293
285
|
```
|
|
294
286
|
|
|
295
287
|
### Validation
|
|
296
|
-
|
|
297
288
|
Validate request data using class-validator:
|
|
298
289
|
|
|
299
290
|
```typescript
|
|
@@ -340,7 +331,6 @@ class UserDto {
|
|
|
340
331
|
```
|
|
341
332
|
|
|
342
333
|
### OpenAPI Documentation
|
|
343
|
-
|
|
344
334
|
Generate API documentation automatically:
|
|
345
335
|
|
|
346
336
|
```typescript
|
|
@@ -376,7 +366,6 @@ app.useOpenApi(OpenApiConfig, (config) => {
|
|
|
376
366
|
### Database Integration
|
|
377
367
|
|
|
378
368
|
## 1. Knex
|
|
379
|
-
|
|
380
369
|
```typescript
|
|
381
370
|
const app = Avleon.createApplication();
|
|
382
371
|
app.useKnex({
|
|
@@ -417,7 +406,6 @@ app.useKenx(KnexConfig)
|
|
|
417
406
|
```
|
|
418
407
|
|
|
419
408
|
### Exmaple uses
|
|
420
|
-
|
|
421
409
|
```typescript
|
|
422
410
|
import { DB, AppService } from "@avleon/core";
|
|
423
411
|
|
|
@@ -505,7 +493,6 @@ export class UserService {
|
|
|
505
493
|
```
|
|
506
494
|
|
|
507
495
|
### File Uploads & File Storage
|
|
508
|
-
|
|
509
496
|
Handle file uploads with multipart support:
|
|
510
497
|
|
|
511
498
|
```typescript
|
|
@@ -555,7 +542,6 @@ async uploadSingleFile(@UploadFile('file') file: MultipartFile) {
|
|
|
555
542
|
```
|
|
556
543
|
|
|
557
544
|
### Static Files
|
|
558
|
-
|
|
559
545
|
Serve static files:
|
|
560
546
|
|
|
561
547
|
```typescript
|
|
@@ -569,15 +555,12 @@ app.useStaticFiles({
|
|
|
569
555
|
```
|
|
570
556
|
|
|
571
557
|
## Configuration
|
|
572
|
-
|
|
573
558
|
Coming soon...
|
|
574
559
|
|
|
575
560
|
## Route Mapping
|
|
576
|
-
|
|
577
561
|
Avleon provides several methods for mapping routes in your application:
|
|
578
562
|
|
|
579
563
|
### mapGet
|
|
580
|
-
|
|
581
564
|
The `mapGet` method is used to define GET routes in your application. It takes a path string and a handler function as parameters.
|
|
582
565
|
|
|
583
566
|
```typescript
|
|
@@ -588,7 +571,6 @@ app.mapGet("/users", async (req, res) => {
|
|
|
588
571
|
```
|
|
589
572
|
|
|
590
573
|
### mapPost
|
|
591
|
-
|
|
592
574
|
The `mapPost` method is used to define POST routes in your application. It takes a path string and a handler function as parameters.
|
|
593
575
|
|
|
594
576
|
```typescript
|
|
@@ -601,7 +583,6 @@ app.mapPost("/users", async (req, res) => {
|
|
|
601
583
|
```
|
|
602
584
|
|
|
603
585
|
### mapPut
|
|
604
|
-
|
|
605
586
|
The `mapPut` method is used to define PUT routes in your application. It takes a path string and a handler function as parameters.
|
|
606
587
|
|
|
607
588
|
```typescript
|
|
@@ -615,7 +596,6 @@ app.mapPut("/users/:id", async (req, res) => {
|
|
|
615
596
|
```
|
|
616
597
|
|
|
617
598
|
### mapDelete
|
|
618
|
-
|
|
619
599
|
The `mapDelete` method is used to define DELETE routes in your application. It takes a path string and a handler function as parameters.
|
|
620
600
|
|
|
621
601
|
```typescript
|
|
@@ -628,7 +608,6 @@ app.mapDelete("/users/:id", async (req, res) => {
|
|
|
628
608
|
```
|
|
629
609
|
|
|
630
610
|
### Add openapi and middleware support for inline route
|
|
631
|
-
|
|
632
611
|
Each of these methods returns a route object that can be used to add middleware or Swagger documentation to the route.
|
|
633
612
|
|
|
634
613
|
```typescript
|
|
@@ -662,6 +641,30 @@ app
|
|
|
662
641
|
},
|
|
663
642
|
});
|
|
664
643
|
```
|
|
644
|
+
### Websocket Intregation (Socket.io)
|
|
645
|
+
```typescript
|
|
646
|
+
app.useSocketIO({
|
|
647
|
+
cors:{origin:"*"}
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
Now in controller or service use EventDispatcher
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
export class UserService{
|
|
655
|
+
constructor(
|
|
656
|
+
private readonly dispatcher: EventDispatcher
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
async create(){
|
|
660
|
+
...rest code
|
|
661
|
+
|
|
662
|
+
await this.dispatcher.dispatch("users:notifications",{created:true, userId: newUser.Id})
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
665
668
|
|
|
666
669
|
### Testing
|
|
667
670
|
|
package/dist/helpers.d.ts
CHANGED
|
@@ -31,4 +31,14 @@ type ValidationError = {
|
|
|
31
31
|
export declare function validateRequestBody(target: Constructor, value: object, options?: "object" | "array"): ValidationError;
|
|
32
32
|
export declare function pick<T extends object>(obj: T, paths: string[]): Partial<T>;
|
|
33
33
|
export declare function exclude<T extends object>(obj: T | T[], paths: string[]): Partial<T> | Partial<T>[];
|
|
34
|
+
export declare function autoCast(value: any, typeHint?: any, schema?: any): any;
|
|
35
|
+
/**
|
|
36
|
+
* Deeply normalizes query strings into nested JS objects.
|
|
37
|
+
* Supports:
|
|
38
|
+
* - filter[name]=john
|
|
39
|
+
* - filter[user][age]=25
|
|
40
|
+
* - filter[tags][]=a&filter[tags][]=b
|
|
41
|
+
* - filter=name&filter=sorna
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizeQueryDeep(query: Record<string, any>): Record<string, any>;
|
|
34
44
|
export {};
|
package/dist/helpers.js
CHANGED
|
@@ -21,6 +21,8 @@ exports.validateObjectByInstance = validateObjectByInstance;
|
|
|
21
21
|
exports.validateRequestBody = validateRequestBody;
|
|
22
22
|
exports.pick = pick;
|
|
23
23
|
exports.exclude = exclude;
|
|
24
|
+
exports.autoCast = autoCast;
|
|
25
|
+
exports.normalizeQueryDeep = normalizeQueryDeep;
|
|
24
26
|
/**
|
|
25
27
|
* @copyright 2024
|
|
26
28
|
* @author Tareq Hossain
|
|
@@ -290,3 +292,128 @@ function exclude(obj, paths) {
|
|
|
290
292
|
}
|
|
291
293
|
return clone;
|
|
292
294
|
}
|
|
295
|
+
function autoCast(value, typeHint, schema) {
|
|
296
|
+
var _a, _b;
|
|
297
|
+
if (value === null || value === undefined)
|
|
298
|
+
return value;
|
|
299
|
+
if (Array.isArray(value)) {
|
|
300
|
+
const elementType = Array.isArray(typeHint) ? typeHint[0] : undefined;
|
|
301
|
+
return value.map((v) => autoCast(v, elementType));
|
|
302
|
+
}
|
|
303
|
+
if (typeof value === "object" && !(value instanceof Date)) {
|
|
304
|
+
const result = {};
|
|
305
|
+
for (const [key, val] of Object.entries(value)) {
|
|
306
|
+
let fieldType = undefined;
|
|
307
|
+
if ((_b = (_a = schema === null || schema === void 0 ? void 0 : schema.properties) === null || _a === void 0 ? void 0 : _a[key]) === null || _b === void 0 ? void 0 : _b.type) {
|
|
308
|
+
const t = schema.properties[key].type;
|
|
309
|
+
fieldType =
|
|
310
|
+
t === "integer" || t === "number"
|
|
311
|
+
? Number
|
|
312
|
+
: t === "boolean"
|
|
313
|
+
? Boolean
|
|
314
|
+
: t === "array"
|
|
315
|
+
? Array
|
|
316
|
+
: t === "object"
|
|
317
|
+
? Object
|
|
318
|
+
: String;
|
|
319
|
+
}
|
|
320
|
+
result[key] = autoCast(val, fieldType);
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
if (typeof value !== "string")
|
|
325
|
+
return value;
|
|
326
|
+
const trimmed = value.trim();
|
|
327
|
+
if (typeHint === Boolean || trimmed.toLowerCase() === "true")
|
|
328
|
+
return true;
|
|
329
|
+
if (trimmed.toLowerCase() === "false")
|
|
330
|
+
return false;
|
|
331
|
+
if (typeHint === Number || (!isNaN(Number(trimmed)) && trimmed !== "")) {
|
|
332
|
+
const n = Number(trimmed);
|
|
333
|
+
if (!isNaN(n))
|
|
334
|
+
return n;
|
|
335
|
+
}
|
|
336
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
337
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(trimmed);
|
|
340
|
+
return autoCast(parsed, typeHint, schema);
|
|
341
|
+
}
|
|
342
|
+
catch (_c) {
|
|
343
|
+
return trimmed;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (typeHint === Date ||
|
|
347
|
+
/^\d{4}-\d{2}-\d{2}([Tt]\d{2}:\d{2})?/.test(trimmed)) {
|
|
348
|
+
const d = new Date(trimmed);
|
|
349
|
+
if (!isNaN(d.getTime()))
|
|
350
|
+
return d;
|
|
351
|
+
}
|
|
352
|
+
return trimmed;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Deeply normalizes query strings into nested JS objects.
|
|
356
|
+
* Supports:
|
|
357
|
+
* - filter[name]=john
|
|
358
|
+
* - filter[user][age]=25
|
|
359
|
+
* - filter[tags][]=a&filter[tags][]=b
|
|
360
|
+
* - filter=name&filter=sorna
|
|
361
|
+
*/
|
|
362
|
+
function normalizeQueryDeep(query) {
|
|
363
|
+
const result = {};
|
|
364
|
+
const setDeep = (obj, path, value) => {
|
|
365
|
+
let current = obj;
|
|
366
|
+
for (let i = 0; i < path.length; i++) {
|
|
367
|
+
const key = path[i];
|
|
368
|
+
const nextKey = path[i + 1];
|
|
369
|
+
if (i === path.length - 1) {
|
|
370
|
+
if (key === "") {
|
|
371
|
+
if (!Array.isArray(current))
|
|
372
|
+
current = [];
|
|
373
|
+
current.push(value);
|
|
374
|
+
}
|
|
375
|
+
else if (Array.isArray(current[key])) {
|
|
376
|
+
current[key].push(value);
|
|
377
|
+
}
|
|
378
|
+
else if (current[key] !== undefined) {
|
|
379
|
+
current[key] = [current[key], value];
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
current[key] = value;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
if (!current[key]) {
|
|
387
|
+
current[key] = nextKey === "" || /^\d+$/.test(nextKey) ? [] : {};
|
|
388
|
+
}
|
|
389
|
+
current = current[key];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
394
|
+
const path = [];
|
|
395
|
+
const regex = /([^\[\]]+)|(\[\])/g;
|
|
396
|
+
let match;
|
|
397
|
+
while ((match = regex.exec(rawKey)) !== null) {
|
|
398
|
+
if (match[1])
|
|
399
|
+
path.push(match[1]);
|
|
400
|
+
else if (match[2])
|
|
401
|
+
path.push("");
|
|
402
|
+
}
|
|
403
|
+
if (path.length === 0) {
|
|
404
|
+
if (result[rawKey]) {
|
|
405
|
+
if (Array.isArray(result[rawKey]))
|
|
406
|
+
result[rawKey].push(rawValue);
|
|
407
|
+
else
|
|
408
|
+
result[rawKey] = [result[rawKey], rawValue];
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
result[rawKey] = rawValue;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
setDeep(result, path, rawValue);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
package/dist/icore.d.ts
CHANGED
|
@@ -148,6 +148,7 @@ export declare class AvleonApplication {
|
|
|
148
148
|
private _isConfigClass;
|
|
149
149
|
useCors<T = FastifyCorsOptions>(corsOptions?: ConfigInput<T>): void;
|
|
150
150
|
useWebSocket<T = Partial<ServerOptions>>(socketOptions: ConfigInput<T>): void;
|
|
151
|
+
useSocketIO<T = Partial<ServerOptions>>(socketOptions: ConfigInput<T>): void;
|
|
151
152
|
private _initWebSocket;
|
|
152
153
|
useOpenApi<T = OpenApiUiOptions>(configOrClass: OpenApiConfigInput<T>): void;
|
|
153
154
|
useMultipart<T extends MultipartOptions>(options: ConfigInput<T>): void;
|
package/dist/icore.js
CHANGED
|
@@ -59,6 +59,7 @@ const multipart_1 = __importDefault(require("@fastify/multipart"));
|
|
|
59
59
|
const validation_1 = require("./validation");
|
|
60
60
|
const utils_1 = require("./utils");
|
|
61
61
|
const socket_io_1 = require("socket.io");
|
|
62
|
+
const event_dispatcher_1 = require("./event-dispatcher");
|
|
62
63
|
const event_subscriber_1 = require("./event-subscriber");
|
|
63
64
|
const stream_1 = __importDefault(require("stream"));
|
|
64
65
|
const kenx_provider_1 = require("./kenx-provider");
|
|
@@ -191,14 +192,16 @@ class AvleonApplication {
|
|
|
191
192
|
this._hasWebsocket = true;
|
|
192
193
|
this._initWebSocket(socketOptions);
|
|
193
194
|
}
|
|
195
|
+
useSocketIO(socketOptions) {
|
|
196
|
+
this._hasWebsocket = true;
|
|
197
|
+
this._initWebSocket(socketOptions);
|
|
198
|
+
}
|
|
194
199
|
async _initWebSocket(options) {
|
|
195
200
|
const fsSocketIO = (0, utils_1.optionalRequire)("fastify-socket.io", {
|
|
196
201
|
failOnMissing: true,
|
|
197
202
|
customMessage: 'Install "fastify-socket.io" to enable socket.io.\n\n run pnpm install fastify-socket.io',
|
|
198
203
|
});
|
|
199
|
-
|
|
200
|
-
const socketIO = await this.app.io;
|
|
201
|
-
typedi_1.default.set(socket_io_1.Server, socketIO);
|
|
204
|
+
this.app.register(fsSocketIO, options);
|
|
202
205
|
}
|
|
203
206
|
useOpenApi(configOrClass) {
|
|
204
207
|
let openApiConfig;
|
|
@@ -500,29 +503,25 @@ class AvleonApplication {
|
|
|
500
503
|
// Initialize args array with correct length
|
|
501
504
|
const maxIndex = Math.max(...meta.params.map((p) => p.index || 0), ...meta.query.map((q) => q.index), ...meta.body.map((b) => b.index), ...meta.currentUser.map((u) => u.index), ...meta.headers.map((h) => h.index), ...(((_a = meta.request) === null || _a === void 0 ? void 0 : _a.map((r) => r.index)) || []), ...(((_b = meta.file) === null || _b === void 0 ? void 0 : _b.map((f) => f.index)) || []), ...(((_c = meta.files) === null || _c === void 0 ? void 0 : _c.map((f) => f.index)) || []), -1) + 1;
|
|
502
505
|
const args = new Array(maxIndex).fill(undefined);
|
|
503
|
-
// Map route parameters
|
|
504
506
|
meta.params.forEach((p) => {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
+
var _a;
|
|
508
|
+
const raw = p.key === "all" ? { ...req.params } : ((_a = req.params[p.key]) !== null && _a !== void 0 ? _a : null);
|
|
509
|
+
args[p.index] = (0, helpers_1.autoCast)(raw, p.dataType, p.schema);
|
|
507
510
|
});
|
|
508
|
-
// Map query parameters
|
|
509
511
|
meta.query.forEach((q) => {
|
|
510
|
-
|
|
512
|
+
const raw = q.key === "all" ? (0, helpers_1.normalizeQueryDeep)({ ...req.query }) : req.query[q.key];
|
|
513
|
+
args[q.index] = (0, helpers_1.autoCast)(raw, q.dataType, q.schema);
|
|
511
514
|
});
|
|
512
|
-
// Map body data (including form data)
|
|
513
515
|
meta.body.forEach((body) => {
|
|
514
516
|
args[body.index] = { ...req.body, ...req.formData };
|
|
515
517
|
});
|
|
516
|
-
// Map current user
|
|
517
518
|
meta.currentUser.forEach((user) => {
|
|
518
519
|
args[user.index] = req.user;
|
|
519
520
|
});
|
|
520
|
-
// Map headers
|
|
521
521
|
meta.headers.forEach((header) => {
|
|
522
522
|
args[header.index] =
|
|
523
523
|
header.key === "all" ? { ...req.headers } : req.headers[header.key];
|
|
524
524
|
});
|
|
525
|
-
// Map request object
|
|
526
525
|
if (meta.request && meta.request.length > 0) {
|
|
527
526
|
meta.request.forEach((r) => {
|
|
528
527
|
args[r.index] = req;
|
|
@@ -535,11 +534,9 @@ class AvleonApplication {
|
|
|
535
534
|
((_d = req.headers["content-type"]) === null || _d === void 0 ? void 0 : _d.startsWith("multipart/form-data"))) {
|
|
536
535
|
const files = await req.saveRequestFiles();
|
|
537
536
|
if (!files || files.length === 0) {
|
|
538
|
-
// Only throw error if files are explicitly required
|
|
539
537
|
if (meta.files && meta.files.length > 0) {
|
|
540
538
|
throw new exceptions_1.BadRequestException({ error: "No files uploaded" });
|
|
541
539
|
}
|
|
542
|
-
// For single file (@File()), set to null
|
|
543
540
|
if (meta.file && meta.file.length > 0) {
|
|
544
541
|
meta.file.forEach((f) => {
|
|
545
542
|
args[f.index] = null;
|
|
@@ -547,7 +544,6 @@ class AvleonApplication {
|
|
|
547
544
|
}
|
|
548
545
|
}
|
|
549
546
|
else {
|
|
550
|
-
// Create file info objects
|
|
551
547
|
const fileInfo = files.map((file) => ({
|
|
552
548
|
type: file.type,
|
|
553
549
|
filepath: file.filepath,
|
|
@@ -556,16 +552,14 @@ class AvleonApplication {
|
|
|
556
552
|
encoding: file.encoding,
|
|
557
553
|
mimetype: file.mimetype,
|
|
558
554
|
fields: file.fields,
|
|
555
|
+
toBuffer: file.toBuffer,
|
|
559
556
|
}));
|
|
560
|
-
// Handle single file decorator (@File())
|
|
561
557
|
if (meta.file && meta.file.length > 0) {
|
|
562
558
|
meta.file.forEach((f) => {
|
|
563
559
|
if (f.fieldName === "all") {
|
|
564
|
-
// Return first file if "all" is specified
|
|
565
560
|
args[f.index] = fileInfo[0] || null;
|
|
566
561
|
}
|
|
567
562
|
else {
|
|
568
|
-
// Find specific file by fieldname
|
|
569
563
|
const file = fileInfo.find((x) => x.fieldname === f.fieldName);
|
|
570
564
|
if (!file) {
|
|
571
565
|
throw new exceptions_1.BadRequestException(`File field "${f.fieldName}" not found in uploaded files`);
|
|
@@ -591,12 +585,10 @@ class AvleonApplication {
|
|
|
591
585
|
}
|
|
592
586
|
}
|
|
593
587
|
else if (needsFiles) {
|
|
594
|
-
// Files expected but request is not multipart/form-data
|
|
595
588
|
throw new exceptions_1.BadRequestException({
|
|
596
589
|
error: "Invalid content type. Expected multipart/form-data for file uploads",
|
|
597
590
|
});
|
|
598
591
|
}
|
|
599
|
-
// Cache the result
|
|
600
592
|
cache.set(cacheKey, args);
|
|
601
593
|
return args;
|
|
602
594
|
}
|
|
@@ -773,7 +765,15 @@ class AvleonApplication {
|
|
|
773
765
|
}
|
|
774
766
|
}
|
|
775
767
|
handleSocket(socket) {
|
|
768
|
+
const contextService = typedi_1.default.get(event_dispatcher_1.SocketContextService);
|
|
776
769
|
subscriberRegistry.register(socket);
|
|
770
|
+
// Wrap all future event handlers with context
|
|
771
|
+
const originalOn = socket.on.bind(socket);
|
|
772
|
+
socket.on = (event, handler) => {
|
|
773
|
+
return originalOn(event, (...args) => {
|
|
774
|
+
contextService.run(socket, () => handler(...args));
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
777
|
}
|
|
778
778
|
async run(port = 4000, fn) {
|
|
779
779
|
if (this.alreadyRun)
|
|
@@ -803,20 +803,6 @@ class AvleonApplication {
|
|
|
803
803
|
},
|
|
804
804
|
});
|
|
805
805
|
});
|
|
806
|
-
if (this._hasWebsocket) {
|
|
807
|
-
await this.app.io.on("connection", this.handleSocket);
|
|
808
|
-
await this.app.io.use((socket, next) => {
|
|
809
|
-
const token = socket.handshake.auth.token;
|
|
810
|
-
try {
|
|
811
|
-
const user = { id: 1, name: "tareq" };
|
|
812
|
-
socket.data.user = user; // this powers @AuthUser()
|
|
813
|
-
next();
|
|
814
|
-
}
|
|
815
|
-
catch (_a) {
|
|
816
|
-
next(new Error("Unauthorized"));
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
806
|
this.app.setErrorHandler((error, request, reply) => {
|
|
821
807
|
if (error instanceof exceptions_1.BaseHttpException) {
|
|
822
808
|
const response = {
|
|
@@ -833,9 +819,35 @@ class AvleonApplication {
|
|
|
833
819
|
return reply.status(500).send({
|
|
834
820
|
code: 500,
|
|
835
821
|
message: error.message || "Internal Server Error",
|
|
822
|
+
...(process.env.NODE_ENV === "development" && { stack: error.stack }),
|
|
836
823
|
});
|
|
837
824
|
});
|
|
838
825
|
await this.app.ready();
|
|
826
|
+
if (this._hasWebsocket) {
|
|
827
|
+
if (!this.app.io) {
|
|
828
|
+
throw new Error("Socket.IO not initialized. Make sure fastify-socket.io is registered correctly.");
|
|
829
|
+
}
|
|
830
|
+
// Register the io instance in Container
|
|
831
|
+
typedi_1.default.set(socket_io_1.Server, this.app.io);
|
|
832
|
+
// Register middleware first
|
|
833
|
+
// await this.app.io.use(
|
|
834
|
+
// (
|
|
835
|
+
// socket: { handshake: { auth: { token: any } }; data: { user: any } },
|
|
836
|
+
// next: any,
|
|
837
|
+
// ) => {
|
|
838
|
+
// const token = socket.handshake.auth.token;
|
|
839
|
+
// try {
|
|
840
|
+
// const user = { id: 1, name: "tareq" };
|
|
841
|
+
// socket.data.user = user; // this powers @AuthUser()
|
|
842
|
+
// next();
|
|
843
|
+
// } catch {
|
|
844
|
+
// next(new Error("Unauthorized"));
|
|
845
|
+
// }
|
|
846
|
+
// },
|
|
847
|
+
// );
|
|
848
|
+
// Then register connection handler
|
|
849
|
+
await this.app.io.on("connection", this.handleSocket.bind(this));
|
|
850
|
+
}
|
|
839
851
|
await this.app.listen({ port });
|
|
840
852
|
console.log(`Application running on http://127.0.0.1:${port}`);
|
|
841
853
|
}
|
package/dist/params.js
CHANGED
|
@@ -13,45 +13,52 @@ const swagger_schema_1 = require("./swagger-schema");
|
|
|
13
13
|
function createParamDecorator(type) {
|
|
14
14
|
return function (key, options = {}) {
|
|
15
15
|
return function (target, propertyKey, parameterIndex) {
|
|
16
|
-
var _a;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const functionSource = target[propertyKey].toString();
|
|
20
|
-
const paramNames = (_a = functionSource
|
|
21
|
-
.match(/\(([^)]*)\)/)) === null || _a === void 0 ? void 0 : _a[1].split(",").map((name) => name.trim());
|
|
22
|
-
const paramDataType = parameterTypes[parameterIndex];
|
|
23
|
-
existingParams.push({
|
|
24
|
-
index: parameterIndex,
|
|
25
|
-
key: key ? key : "all",
|
|
26
|
-
name: paramNames[parameterIndex],
|
|
27
|
-
required: options.required == undefined ? true : options.required,
|
|
28
|
-
validate: options.validate == undefined ? true : options.validate,
|
|
29
|
-
dataType: (0, helpers_1.getDataType)(paramDataType),
|
|
30
|
-
validatorClass: (0, helpers_1.isClassValidatorClass)(paramDataType),
|
|
31
|
-
schema: (0, helpers_1.isClassValidatorClass)(paramDataType)
|
|
32
|
-
? (0, swagger_schema_1.generateSwaggerSchema)(paramDataType)
|
|
33
|
-
: null,
|
|
34
|
-
type,
|
|
35
|
-
});
|
|
16
|
+
var _a, _b, _c, _d;
|
|
17
|
+
// Determine correct meta key
|
|
18
|
+
let metaKey;
|
|
36
19
|
switch (type) {
|
|
37
20
|
case "route:param":
|
|
38
|
-
|
|
21
|
+
metaKey = container_1.PARAM_META_KEY;
|
|
39
22
|
break;
|
|
40
23
|
case "route:query":
|
|
41
|
-
|
|
24
|
+
metaKey = container_1.QUERY_META_KEY;
|
|
42
25
|
break;
|
|
43
26
|
case "route:body":
|
|
44
|
-
|
|
27
|
+
metaKey = container_1.REQUEST_BODY_META_KEY;
|
|
45
28
|
break;
|
|
46
29
|
case "route:user":
|
|
47
|
-
|
|
30
|
+
metaKey = container_1.REQUEST_USER_META_KEY;
|
|
48
31
|
break;
|
|
49
32
|
case "route:header":
|
|
50
|
-
|
|
33
|
+
metaKey = container_1.REQUEST_HEADER_META_KEY;
|
|
51
34
|
break;
|
|
52
35
|
default:
|
|
53
|
-
|
|
36
|
+
throw new Error(`Unknown param decorator type: ${String(type)}`);
|
|
54
37
|
}
|
|
38
|
+
// Retrieve and preserve existing metadata
|
|
39
|
+
const existingParams = Reflect.getMetadata(metaKey, target, propertyKey) || [];
|
|
40
|
+
// Get parameter names (fallback safe)
|
|
41
|
+
const functionSource = target[propertyKey].toString();
|
|
42
|
+
const paramNames = ((_b = (_a = functionSource.match(/\(([^)]*)\)/)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.split(",").map((n) => n.trim())) || [];
|
|
43
|
+
// Determine the param type
|
|
44
|
+
const parameterTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey) || [];
|
|
45
|
+
const paramDataType = parameterTypes[parameterIndex];
|
|
46
|
+
// Append new parameter
|
|
47
|
+
existingParams.push({
|
|
48
|
+
index: parameterIndex,
|
|
49
|
+
key: typeof key === "string" ? key : "all",
|
|
50
|
+
name: paramNames[parameterIndex],
|
|
51
|
+
required: (_c = options.required) !== null && _c !== void 0 ? _c : true,
|
|
52
|
+
validate: (_d = options.validate) !== null && _d !== void 0 ? _d : true,
|
|
53
|
+
dataType: (0, helpers_1.getDataType)(paramDataType),
|
|
54
|
+
validatorClass: (0, helpers_1.isClassValidatorClass)(paramDataType),
|
|
55
|
+
schema: (0, helpers_1.isClassValidatorClass)(paramDataType)
|
|
56
|
+
? (0, swagger_schema_1.generateSwaggerSchema)(paramDataType)
|
|
57
|
+
: null,
|
|
58
|
+
type,
|
|
59
|
+
});
|
|
60
|
+
// Save back using the correct meta key
|
|
61
|
+
Reflect.defineMetadata(metaKey, existingParams, target, propertyKey);
|
|
55
62
|
};
|
|
56
63
|
};
|
|
57
64
|
}
|
package/dist/queue.d.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* @copyright 2024
|
|
3
|
-
* @author Tareq Hossain
|
|
4
|
-
* @email xtrinsic96@gmail.com
|
|
5
|
-
* @url https://github.com/xtareq
|
|
6
|
-
*/
|
|
1
|
+
import { EventEmitter } from "events";
|
|
7
2
|
interface Job {
|
|
8
3
|
id: string;
|
|
9
4
|
data: any;
|
|
5
|
+
runAt?: number;
|
|
6
|
+
status?: "pending" | "running" | "failed" | "completed";
|
|
10
7
|
}
|
|
11
8
|
interface QueueAdapter {
|
|
12
9
|
loadJobs(): Promise<Job[]>;
|
|
@@ -18,19 +15,24 @@ export declare class FileQueueAdapter implements QueueAdapter {
|
|
|
18
15
|
loadJobs(): Promise<Job[]>;
|
|
19
16
|
saveJobs(jobs: Job[]): Promise<void>;
|
|
20
17
|
}
|
|
21
|
-
declare class
|
|
18
|
+
export declare class AvleonQueue extends EventEmitter {
|
|
19
|
+
private name;
|
|
22
20
|
private processing;
|
|
21
|
+
private stopped;
|
|
23
22
|
private jobHandler;
|
|
24
23
|
private adapter;
|
|
25
|
-
constructor(
|
|
26
|
-
|
|
24
|
+
constructor(name: string, adapter?: QueueAdapter, jobHandler?: (job: Job) => Promise<void>);
|
|
25
|
+
private defaultHandler;
|
|
26
|
+
addJob(data: any, options?: {
|
|
27
|
+
delay?: number;
|
|
28
|
+
}): Promise<void>;
|
|
27
29
|
private processNext;
|
|
30
|
+
onDone(cb: (job: Job) => void): Promise<void>;
|
|
31
|
+
onFailed(cb: (error: any, job: Job) => void): Promise<void>;
|
|
32
|
+
getJobs(): Promise<Job[]>;
|
|
33
|
+
stop(): Promise<void>;
|
|
28
34
|
}
|
|
29
35
|
export declare class QueueManager {
|
|
30
|
-
|
|
31
|
-
private adapter;
|
|
32
|
-
private constructor();
|
|
33
|
-
static getInstance(adapter: QueueAdapter): QueueManager;
|
|
34
|
-
createQueue(jobHandler: (job: Job) => Promise<void>): SimpleQueue;
|
|
36
|
+
from(name: string, jobHandler?: (job: Job) => Promise<void>): Promise<AvleonQueue>;
|
|
35
37
|
}
|
|
36
38
|
export {};
|
package/dist/queue.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.QueueManager = exports.FileQueueAdapter = void 0;
|
|
9
|
+
exports.QueueManager = exports.AvleonQueue = exports.FileQueueAdapter = void 0;
|
|
10
10
|
const fs_1 = require("fs");
|
|
11
11
|
const path_1 = require("path");
|
|
12
|
-
const
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const events_1 = require("events");
|
|
14
|
+
const decorators_1 = require("./decorators");
|
|
13
15
|
class FileQueueAdapter {
|
|
14
16
|
constructor(queueName) {
|
|
15
17
|
this.queueFile = (0, path_1.join)(__dirname, `${queueName}.json`);
|
|
@@ -19,7 +21,7 @@ class FileQueueAdapter {
|
|
|
19
21
|
const data = await fs_1.promises.readFile(this.queueFile, "utf-8");
|
|
20
22
|
return JSON.parse(data);
|
|
21
23
|
}
|
|
22
|
-
catch (
|
|
24
|
+
catch (_a) {
|
|
23
25
|
return [];
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -28,76 +30,87 @@ class FileQueueAdapter {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
exports.FileQueueAdapter = FileQueueAdapter;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
// constructor(queueName: string) {
|
|
36
|
-
// this.queueKey = `queue:${queueName}`;
|
|
37
|
-
// this.client.connect();
|
|
38
|
-
// }
|
|
39
|
-
//
|
|
40
|
-
// async loadJobs(): Promise<Job[]> {
|
|
41
|
-
// const jobs = await this.client.lRange(this.queueKey, 0, -1);
|
|
42
|
-
// return jobs.map((job) => JSON.parse(job));
|
|
43
|
-
// }
|
|
44
|
-
//
|
|
45
|
-
// async saveJobs(jobs: Job[]) {
|
|
46
|
-
// await this.client.del(this.queueKey);
|
|
47
|
-
// if (jobs.length > 0) {
|
|
48
|
-
// await this.client.rPush(this.queueKey, ...jobs.map(job => JSON.stringify(job)));
|
|
49
|
-
// }
|
|
50
|
-
// }
|
|
51
|
-
// }
|
|
52
|
-
class SimpleQueue {
|
|
53
|
-
constructor(adapter, jobHandler) {
|
|
33
|
+
class AvleonQueue extends events_1.EventEmitter {
|
|
34
|
+
constructor(name, adapter, jobHandler) {
|
|
35
|
+
super();
|
|
54
36
|
this.processing = false;
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
37
|
+
this.stopped = false;
|
|
38
|
+
this.name = name;
|
|
39
|
+
this.adapter = adapter ? adapter : new FileQueueAdapter(name);
|
|
40
|
+
this.jobHandler = jobHandler || this.defaultHandler.bind(this);
|
|
41
|
+
this.setMaxListeners(10);
|
|
57
42
|
}
|
|
58
|
-
async
|
|
59
|
-
|
|
43
|
+
async defaultHandler(job) {
|
|
44
|
+
if (typeof job.data === "function") {
|
|
45
|
+
await job.data();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async addJob(data, options) {
|
|
49
|
+
const job = {
|
|
50
|
+
id: (0, crypto_1.randomUUID)(),
|
|
51
|
+
data,
|
|
52
|
+
runAt: Date.now() + ((options === null || options === void 0 ? void 0 : options.delay) || 0),
|
|
53
|
+
status: "pending",
|
|
54
|
+
};
|
|
60
55
|
const jobs = await this.adapter.loadJobs();
|
|
61
56
|
jobs.push(job);
|
|
62
57
|
await this.adapter.saveJobs(jobs);
|
|
63
|
-
this.
|
|
58
|
+
if (!this.processing)
|
|
59
|
+
this.processNext();
|
|
64
60
|
}
|
|
65
61
|
async processNext() {
|
|
66
|
-
if (this.processing)
|
|
62
|
+
if (this.processing || this.stopped)
|
|
67
63
|
return;
|
|
68
64
|
this.processing = true;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (job) {
|
|
76
|
-
try {
|
|
77
|
-
await this.jobHandler(job);
|
|
65
|
+
while (!this.stopped) {
|
|
66
|
+
const jobs = await this.adapter.loadJobs();
|
|
67
|
+
const nextJob = jobs.find((j) => j.status === "pending");
|
|
68
|
+
if (!nextJob) {
|
|
69
|
+
this.processing = false;
|
|
70
|
+
return;
|
|
78
71
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (nextJob.runAt && nextJob.runAt > now) {
|
|
74
|
+
const delay = nextJob.runAt - now;
|
|
75
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
82
76
|
}
|
|
77
|
+
nextJob.status = "running";
|
|
83
78
|
await this.adapter.saveJobs(jobs);
|
|
84
|
-
this.
|
|
85
|
-
|
|
79
|
+
this.emit("start", nextJob);
|
|
80
|
+
try {
|
|
81
|
+
await this.jobHandler(nextJob);
|
|
82
|
+
nextJob.status = "completed";
|
|
83
|
+
this.emit("done", nextJob);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
nextJob.status = "failed";
|
|
87
|
+
this.emit("failed", err, nextJob);
|
|
88
|
+
}
|
|
89
|
+
await this.adapter.saveJobs(jobs.filter((j) => j.id !== nextJob.id));
|
|
86
90
|
}
|
|
91
|
+
this.processing = false;
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
constructor(adapter) {
|
|
91
|
-
this.adapter = adapter;
|
|
93
|
+
async onDone(cb) {
|
|
94
|
+
this.on("done", cb);
|
|
92
95
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
QueueManager.instance = new QueueManager(adapter);
|
|
96
|
-
}
|
|
97
|
-
return QueueManager.instance;
|
|
96
|
+
async onFailed(cb) {
|
|
97
|
+
this.on("failed", cb);
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
return
|
|
99
|
+
async getJobs() {
|
|
100
|
+
return this.adapter.loadJobs();
|
|
101
|
+
}
|
|
102
|
+
async stop() {
|
|
103
|
+
this.stopped = true;
|
|
101
104
|
}
|
|
102
105
|
}
|
|
106
|
+
exports.AvleonQueue = AvleonQueue;
|
|
107
|
+
let QueueManager = class QueueManager {
|
|
108
|
+
async from(name, jobHandler) {
|
|
109
|
+
const q = new AvleonQueue(name, new FileQueueAdapter(name), jobHandler);
|
|
110
|
+
return q;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
103
113
|
exports.QueueManager = QueueManager;
|
|
114
|
+
exports.QueueManager = QueueManager = __decorate([
|
|
115
|
+
decorators_1.AppService
|
|
116
|
+
], QueueManager);
|
package/dist/queue.test.js
CHANGED
|
@@ -36,22 +36,22 @@ describe("FileQueueAdapter", () => {
|
|
|
36
36
|
});
|
|
37
37
|
describe("QueueManager and SimpleQueue", () => {
|
|
38
38
|
let adapter;
|
|
39
|
-
let queueManager;
|
|
39
|
+
// let queueManager: QueueManager;
|
|
40
40
|
let handler;
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
it("should create a queue and add a job", async () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
41
|
+
// beforeEach(() => {
|
|
42
|
+
// jest.clearAllMocks();
|
|
43
|
+
// adapter = new FileQueueAdapter("testqueue");
|
|
44
|
+
// queueManager = QueueManager.getInstance(adapter);
|
|
45
|
+
// handler = jest.fn().mockResolvedValue(undefined);
|
|
46
|
+
// (fs.readFile as jest.Mock).mockResolvedValue("[]");
|
|
47
|
+
// (fs.writeFile as jest.Mock).mockResolvedValue(undefined);
|
|
48
|
+
// });
|
|
49
|
+
// it("should create a queue and add a job", async () => {
|
|
50
|
+
// const queue = queueManager.createQueue(handler);
|
|
51
|
+
// await queue.addJob({ foo: "bar" });
|
|
52
|
+
// expect(fs.readFile).toHaveBeenCalled();
|
|
53
|
+
// expect(fs.writeFile).toHaveBeenCalled();
|
|
54
|
+
// });
|
|
55
55
|
// it("should process jobs using handler", async () => {
|
|
56
56
|
// (fs.readFile as jest.Mock)
|
|
57
57
|
// .mockResolvedValueOnce("[]")
|
|
@@ -72,8 +72,8 @@ describe("QueueManager and SimpleQueue", () => {
|
|
|
72
72
|
// expect(handler).toHaveBeenCalled();
|
|
73
73
|
// expect(fs.writeFile).toHaveBeenCalledTimes(2);
|
|
74
74
|
// });
|
|
75
|
-
it("QueueManager should be singleton", () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
75
|
+
// it("QueueManager should be singleton", () => {
|
|
76
|
+
// const another = QueueManager.getInstance(adapter);
|
|
77
|
+
// expect(another).toBe(queueManager);
|
|
78
|
+
// });
|
|
79
79
|
});
|