@avleon/core 0.0.43 → 0.0.45
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.js +7 -17
- package/dist/params.js +28 -26
- package/dist/queue.d.ts +27 -36
- package/dist/queue.js +67 -99
- package/package.json +2 -1
- package/dist/queue.test.d.ts +0 -1
- package/dist/queue.test.js +0 -79
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.js
CHANGED
|
@@ -503,29 +503,25 @@ class AvleonApplication {
|
|
|
503
503
|
// Initialize args array with correct length
|
|
504
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;
|
|
505
505
|
const args = new Array(maxIndex).fill(undefined);
|
|
506
|
-
// Map route parameters
|
|
507
506
|
meta.params.forEach((p) => {
|
|
508
|
-
|
|
509
|
-
|
|
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);
|
|
510
510
|
});
|
|
511
|
-
// Map query parameters
|
|
512
511
|
meta.query.forEach((q) => {
|
|
513
|
-
|
|
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);
|
|
514
514
|
});
|
|
515
|
-
// Map body data (including form data)
|
|
516
515
|
meta.body.forEach((body) => {
|
|
517
516
|
args[body.index] = { ...req.body, ...req.formData };
|
|
518
517
|
});
|
|
519
|
-
// Map current user
|
|
520
518
|
meta.currentUser.forEach((user) => {
|
|
521
519
|
args[user.index] = req.user;
|
|
522
520
|
});
|
|
523
|
-
// Map headers
|
|
524
521
|
meta.headers.forEach((header) => {
|
|
525
522
|
args[header.index] =
|
|
526
523
|
header.key === "all" ? { ...req.headers } : req.headers[header.key];
|
|
527
524
|
});
|
|
528
|
-
// Map request object
|
|
529
525
|
if (meta.request && meta.request.length > 0) {
|
|
530
526
|
meta.request.forEach((r) => {
|
|
531
527
|
args[r.index] = req;
|
|
@@ -538,11 +534,9 @@ class AvleonApplication {
|
|
|
538
534
|
((_d = req.headers["content-type"]) === null || _d === void 0 ? void 0 : _d.startsWith("multipart/form-data"))) {
|
|
539
535
|
const files = await req.saveRequestFiles();
|
|
540
536
|
if (!files || files.length === 0) {
|
|
541
|
-
// Only throw error if files are explicitly required
|
|
542
537
|
if (meta.files && meta.files.length > 0) {
|
|
543
538
|
throw new exceptions_1.BadRequestException({ error: "No files uploaded" });
|
|
544
539
|
}
|
|
545
|
-
// For single file (@File()), set to null
|
|
546
540
|
if (meta.file && meta.file.length > 0) {
|
|
547
541
|
meta.file.forEach((f) => {
|
|
548
542
|
args[f.index] = null;
|
|
@@ -550,7 +544,6 @@ class AvleonApplication {
|
|
|
550
544
|
}
|
|
551
545
|
}
|
|
552
546
|
else {
|
|
553
|
-
// Create file info objects
|
|
554
547
|
const fileInfo = files.map((file) => ({
|
|
555
548
|
type: file.type,
|
|
556
549
|
filepath: file.filepath,
|
|
@@ -559,16 +552,14 @@ class AvleonApplication {
|
|
|
559
552
|
encoding: file.encoding,
|
|
560
553
|
mimetype: file.mimetype,
|
|
561
554
|
fields: file.fields,
|
|
555
|
+
toBuffer: file.toBuffer,
|
|
562
556
|
}));
|
|
563
|
-
// Handle single file decorator (@File())
|
|
564
557
|
if (meta.file && meta.file.length > 0) {
|
|
565
558
|
meta.file.forEach((f) => {
|
|
566
559
|
if (f.fieldName === "all") {
|
|
567
|
-
// Return first file if "all" is specified
|
|
568
560
|
args[f.index] = fileInfo[0] || null;
|
|
569
561
|
}
|
|
570
562
|
else {
|
|
571
|
-
// Find specific file by fieldname
|
|
572
563
|
const file = fileInfo.find((x) => x.fieldname === f.fieldName);
|
|
573
564
|
if (!file) {
|
|
574
565
|
throw new exceptions_1.BadRequestException(`File field "${f.fieldName}" not found in uploaded files`);
|
|
@@ -594,12 +585,10 @@ class AvleonApplication {
|
|
|
594
585
|
}
|
|
595
586
|
}
|
|
596
587
|
else if (needsFiles) {
|
|
597
|
-
// Files expected but request is not multipart/form-data
|
|
598
588
|
throw new exceptions_1.BadRequestException({
|
|
599
589
|
error: "Invalid content type. Expected multipart/form-data for file uploads",
|
|
600
590
|
});
|
|
601
591
|
}
|
|
602
|
-
// Cache the result
|
|
603
592
|
cache.set(cacheKey, args);
|
|
604
593
|
return args;
|
|
605
594
|
}
|
|
@@ -830,6 +819,7 @@ class AvleonApplication {
|
|
|
830
819
|
return reply.status(500).send({
|
|
831
820
|
code: 500,
|
|
832
821
|
message: error.message || "Internal Server Error",
|
|
822
|
+
...(process.env.NODE_ENV === "development" && { stack: error.stack }),
|
|
833
823
|
});
|
|
834
824
|
});
|
|
835
825
|
await this.app.ready();
|
package/dist/params.js
CHANGED
|
@@ -13,45 +13,47 @@ 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
|
-
const parameterTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey) || [];
|
|
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
|
+
let metaKey;
|
|
36
18
|
switch (type) {
|
|
37
19
|
case "route:param":
|
|
38
|
-
|
|
20
|
+
metaKey = container_1.PARAM_META_KEY;
|
|
39
21
|
break;
|
|
40
22
|
case "route:query":
|
|
41
|
-
|
|
23
|
+
metaKey = container_1.QUERY_META_KEY;
|
|
42
24
|
break;
|
|
43
25
|
case "route:body":
|
|
44
|
-
|
|
26
|
+
metaKey = container_1.REQUEST_BODY_META_KEY;
|
|
45
27
|
break;
|
|
46
28
|
case "route:user":
|
|
47
|
-
|
|
29
|
+
metaKey = container_1.REQUEST_USER_META_KEY;
|
|
48
30
|
break;
|
|
49
31
|
case "route:header":
|
|
50
|
-
|
|
32
|
+
metaKey = container_1.REQUEST_HEADER_META_KEY;
|
|
51
33
|
break;
|
|
52
34
|
default:
|
|
53
|
-
|
|
35
|
+
throw new Error(`Unknown param decorator type: ${String(type)}`);
|
|
54
36
|
}
|
|
37
|
+
const existingParams = Reflect.getMetadata(metaKey, target, propertyKey) || [];
|
|
38
|
+
// Get parameter names (fallback safe)
|
|
39
|
+
const functionSource = target[propertyKey].toString();
|
|
40
|
+
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())) || [];
|
|
41
|
+
const parameterTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey) || [];
|
|
42
|
+
const paramDataType = parameterTypes[parameterIndex];
|
|
43
|
+
existingParams.push({
|
|
44
|
+
index: parameterIndex,
|
|
45
|
+
key: typeof key === "string" ? key : "all",
|
|
46
|
+
name: paramNames[parameterIndex],
|
|
47
|
+
required: (_c = options.required) !== null && _c !== void 0 ? _c : true,
|
|
48
|
+
validate: (_d = options.validate) !== null && _d !== void 0 ? _d : true,
|
|
49
|
+
dataType: (0, helpers_1.getDataType)(paramDataType),
|
|
50
|
+
validatorClass: (0, helpers_1.isClassValidatorClass)(paramDataType),
|
|
51
|
+
schema: (0, helpers_1.isClassValidatorClass)(paramDataType)
|
|
52
|
+
? (0, swagger_schema_1.generateSwaggerSchema)(paramDataType)
|
|
53
|
+
: null,
|
|
54
|
+
type,
|
|
55
|
+
});
|
|
56
|
+
Reflect.defineMetadata(metaKey, existingParams, target, propertyKey);
|
|
55
57
|
};
|
|
56
58
|
};
|
|
57
59
|
}
|
package/dist/queue.d.ts
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
interface
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import Bull, { Queue as BullQueue, Job, JobOptions } from 'bull';
|
|
2
|
+
export interface QueueConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
adapter?: any;
|
|
5
|
+
handler?: (job: Job) => Promise<any>;
|
|
6
|
+
options?: Bull.QueueOptions;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export declare class AvleonQueue<T = any> {
|
|
9
|
+
protected name?: string | undefined;
|
|
10
|
+
protected adapter?: any | undefined;
|
|
11
|
+
protected queue: BullQueue<T>;
|
|
12
|
+
protected handlerFn?: (job: Job<T>) => Promise<any>;
|
|
13
|
+
constructor(name?: string | undefined, adapter?: any | undefined, handler?: (job: Job<T>) => Promise<any>);
|
|
14
|
+
handler?(job: Job<T>): Promise<any> | any;
|
|
15
|
+
add(data: T, options?: JobOptions): Promise<Bull.Job<T>>;
|
|
16
|
+
delay(data: T, delayMs: number, options?: JobOptions): Promise<Bull.Job<T>>;
|
|
17
|
+
process(handler: (job: Job<T>) => Promise<any>): void;
|
|
18
|
+
processConcurrent(concurrency: number, handler: (job: Job<T>) => Promise<any>): void;
|
|
19
|
+
getQueue(): BullQueue<T>;
|
|
20
|
+
clean(grace: number, status?: 'completed' | 'wait' | 'active' | 'delayed' | 'failed'): Promise<Job[]>;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
pause(): Promise<void>;
|
|
23
|
+
resume(): Promise<void>;
|
|
24
|
+
getJob(jobId: string): Promise<Job<T> | null>;
|
|
25
|
+
getJobs(types: Array<'completed' | 'waiting' | 'active' | 'delayed' | 'failed' | 'paused'>, start?: number, end?: number): Promise<Job<T>[]>;
|
|
11
26
|
}
|
|
12
|
-
export declare
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
loadJobs(): Promise<Job[]>;
|
|
16
|
-
saveJobs(jobs: Job[]): Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
export declare class AvleonQueue extends EventEmitter {
|
|
19
|
-
private name;
|
|
20
|
-
private processing;
|
|
21
|
-
private stopped;
|
|
22
|
-
private jobHandler;
|
|
23
|
-
private adapter;
|
|
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>;
|
|
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>;
|
|
34
|
-
}
|
|
35
|
-
export declare class QueueManager {
|
|
36
|
-
from(name: string, jobHandler?: (job: Job) => Promise<void>): Promise<AvleonQueue>;
|
|
37
|
-
}
|
|
38
|
-
export {};
|
|
27
|
+
export declare function Queue(config: QueueConfig): <T extends {
|
|
28
|
+
new (...args: any[]): AvleonQueue;
|
|
29
|
+
}>(target: T) => T;
|
package/dist/queue.js
CHANGED
|
@@ -1,116 +1,84 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
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;
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
4
|
};
|
|
8
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
6
|
+
exports.AvleonQueue = void 0;
|
|
7
|
+
exports.Queue = Queue;
|
|
8
|
+
const bull_1 = __importDefault(require("bull"));
|
|
9
|
+
const typedi_1 = require("typedi");
|
|
10
|
+
class AvleonQueue {
|
|
11
|
+
constructor(name, adapter, handler) {
|
|
12
|
+
this.name = name;
|
|
13
|
+
this.adapter = adapter;
|
|
14
|
+
// Initialize queue with adapter or default Redis connection
|
|
15
|
+
this.queue = new bull_1.default(name || 'default', adapter);
|
|
16
|
+
this.handlerFn = handler;
|
|
17
|
+
// Check if the instance has a handler method defined
|
|
18
|
+
// This allows subclasses to define handler as a method
|
|
19
|
+
if (typeof this.handler === 'function' && !this.handlerFn) {
|
|
20
|
+
this.handlerFn = (job) => this.handler(job);
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// If handler is provided (from decorator or class method), set up processing
|
|
23
|
+
if (this.handlerFn) {
|
|
24
|
+
this.queue.process(this.handlerFn);
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// Add job to queue
|
|
28
|
+
add(data, options) {
|
|
29
|
+
return this.queue.add(data, options);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
constructor(name, adapter, jobHandler) {
|
|
35
|
-
super();
|
|
36
|
-
this.processing = false;
|
|
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);
|
|
31
|
+
// Add job with delay
|
|
32
|
+
delay(data, delayMs, options) {
|
|
33
|
+
return this.queue.add(data, { ...options, delay: delayMs });
|
|
42
34
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
// Process jobs (can be called manually if not using handler)
|
|
36
|
+
process(handler) {
|
|
37
|
+
this.handlerFn = handler;
|
|
38
|
+
this.queue.process(handler);
|
|
47
39
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
runAt: Date.now() + ((options === null || options === void 0 ? void 0 : options.delay) || 0),
|
|
53
|
-
status: "pending",
|
|
54
|
-
};
|
|
55
|
-
const jobs = await this.adapter.loadJobs();
|
|
56
|
-
jobs.push(job);
|
|
57
|
-
await this.adapter.saveJobs(jobs);
|
|
58
|
-
if (!this.processing)
|
|
59
|
-
this.processNext();
|
|
40
|
+
// Process with concurrency
|
|
41
|
+
processConcurrent(concurrency, handler) {
|
|
42
|
+
this.handlerFn = handler;
|
|
43
|
+
this.queue.process(concurrency, handler);
|
|
60
44
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const nextJob = jobs.find((j) => j.status === "pending");
|
|
68
|
-
if (!nextJob) {
|
|
69
|
-
this.processing = false;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
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));
|
|
76
|
-
}
|
|
77
|
-
nextJob.status = "running";
|
|
78
|
-
await this.adapter.saveJobs(jobs);
|
|
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));
|
|
90
|
-
}
|
|
91
|
-
this.processing = false;
|
|
45
|
+
// Get the underlying Bull queue
|
|
46
|
+
getQueue() {
|
|
47
|
+
return this.queue;
|
|
48
|
+
}
|
|
49
|
+
async clean(grace, status) {
|
|
50
|
+
return this.queue.clean(grace, status);
|
|
92
51
|
}
|
|
93
|
-
async
|
|
94
|
-
this.
|
|
52
|
+
async close() {
|
|
53
|
+
await this.queue.close();
|
|
95
54
|
}
|
|
96
|
-
async
|
|
97
|
-
this.
|
|
55
|
+
async pause() {
|
|
56
|
+
await this.queue.pause();
|
|
98
57
|
}
|
|
99
|
-
async
|
|
100
|
-
|
|
58
|
+
async resume() {
|
|
59
|
+
await this.queue.resume();
|
|
101
60
|
}
|
|
102
|
-
async
|
|
103
|
-
this.
|
|
61
|
+
async getJob(jobId) {
|
|
62
|
+
return this.queue.getJob(jobId);
|
|
63
|
+
}
|
|
64
|
+
async getJobs(types, start, end) {
|
|
65
|
+
return this.queue.getJobs(types, start, end);
|
|
104
66
|
}
|
|
105
67
|
}
|
|
106
68
|
exports.AvleonQueue = AvleonQueue;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
69
|
+
function Queue(config) {
|
|
70
|
+
return function (target) {
|
|
71
|
+
// Create a new class that extends the target
|
|
72
|
+
const DecoratedClass = class extends target {
|
|
73
|
+
constructor(...args) {
|
|
74
|
+
super(config.name, config.adapter, config.handler);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
Object.defineProperty(DecoratedClass, 'name', {
|
|
78
|
+
value: target.name,
|
|
79
|
+
writable: false
|
|
80
|
+
});
|
|
81
|
+
(0, typedi_1.Service)()(DecoratedClass);
|
|
82
|
+
return DecoratedClass;
|
|
83
|
+
};
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avleon/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.45",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"keywords": [
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test": "."
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"bull": "^4.16.5",
|
|
26
27
|
"class-transformer": "^0.5.1",
|
|
27
28
|
"class-validator": "^0.14.2",
|
|
28
29
|
"fastify": "^5.1.0",
|
package/dist/queue.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/queue.test.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const queue_1 = require("./queue");
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
|
-
const path_1 = require("path");
|
|
6
|
-
jest.mock("fs", () => ({
|
|
7
|
-
promises: {
|
|
8
|
-
readFile: jest.fn(),
|
|
9
|
-
writeFile: jest.fn(),
|
|
10
|
-
},
|
|
11
|
-
}));
|
|
12
|
-
const mockQueueFile = (0, path_1.join)(__dirname, "testqueue.json");
|
|
13
|
-
describe("FileQueueAdapter", () => {
|
|
14
|
-
const jobs = [{ id: "1", data: "foo" }, { id: "2", data: "bar" }];
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
jest.clearAllMocks();
|
|
17
|
-
});
|
|
18
|
-
it("should load jobs from file", async () => {
|
|
19
|
-
fs_1.promises.readFile.mockResolvedValue(JSON.stringify(jobs));
|
|
20
|
-
const adapter = new queue_1.FileQueueAdapter("testqueue");
|
|
21
|
-
const loaded = await adapter.loadJobs();
|
|
22
|
-
expect(loaded).toEqual(jobs);
|
|
23
|
-
expect(fs_1.promises.readFile).toHaveBeenCalledWith(mockQueueFile, "utf-8");
|
|
24
|
-
});
|
|
25
|
-
it("should return empty array if file does not exist", async () => {
|
|
26
|
-
fs_1.promises.readFile.mockRejectedValue(new Error("not found"));
|
|
27
|
-
const adapter = new queue_1.FileQueueAdapter("testqueue");
|
|
28
|
-
const loaded = await adapter.loadJobs();
|
|
29
|
-
expect(loaded).toEqual([]);
|
|
30
|
-
});
|
|
31
|
-
it("should save jobs to file", async () => {
|
|
32
|
-
const adapter = new queue_1.FileQueueAdapter("testqueue");
|
|
33
|
-
await adapter.saveJobs(jobs);
|
|
34
|
-
expect(fs_1.promises.writeFile).toHaveBeenCalledWith(mockQueueFile, JSON.stringify(jobs, null, 2), "utf-8");
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
describe("QueueManager and SimpleQueue", () => {
|
|
38
|
-
let adapter;
|
|
39
|
-
// let queueManager: QueueManager;
|
|
40
|
-
let handler;
|
|
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
|
-
// it("should process jobs using handler", async () => {
|
|
56
|
-
// (fs.readFile as jest.Mock)
|
|
57
|
-
// .mockResolvedValueOnce("[]")
|
|
58
|
-
// .mockResolvedValueOnce(JSON.stringify([{ id: "1", data: "baz" }]))
|
|
59
|
-
// .mockResolvedValueOnce("[]");
|
|
60
|
-
// const queue = queueManager.createQueue(handler);
|
|
61
|
-
// await queue.addJob("baz");
|
|
62
|
-
// expect(handler).toHaveBeenCalled();
|
|
63
|
-
// });
|
|
64
|
-
// it("should requeue job if handler throws", async () => {
|
|
65
|
-
// handler.mockRejectedValueOnce(new Error("fail"));
|
|
66
|
-
// (fs.readFile as jest.Mock)
|
|
67
|
-
// .mockResolvedValueOnce("[]")
|
|
68
|
-
// .mockResolvedValueOnce(JSON.stringify([{ id: "1", data: "baz" }]))
|
|
69
|
-
// .mockResolvedValueOnce(JSON.stringify([{ id: "1", data: "baz" }]));
|
|
70
|
-
// const queue = queueManager.createQueue(handler);
|
|
71
|
-
// await queue.addJob("baz");
|
|
72
|
-
// expect(handler).toHaveBeenCalled();
|
|
73
|
-
// expect(fs.writeFile).toHaveBeenCalledTimes(2);
|
|
74
|
-
// });
|
|
75
|
-
// it("QueueManager should be singleton", () => {
|
|
76
|
-
// const another = QueueManager.getInstance(adapter);
|
|
77
|
-
// expect(another).toBe(queueManager);
|
|
78
|
-
// });
|
|
79
|
-
});
|