@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 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
- await this.app.register(fsSocketIO, options);
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
- args[p.index] =
506
- p.key == "all" ? { ...req.query } : req.params[p.key] || null;
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
- args[q.index] = q.key == "all" ? { ...req.query } : req.query[q.key];
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
- const existingParams = Reflect.getMetadata(type, target, propertyKey) || [];
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
+ // Determine correct meta key
18
+ let metaKey;
36
19
  switch (type) {
37
20
  case "route:param":
38
- Reflect.defineMetadata(container_1.PARAM_META_KEY, existingParams, target, propertyKey);
21
+ metaKey = container_1.PARAM_META_KEY;
39
22
  break;
40
23
  case "route:query":
41
- Reflect.defineMetadata(container_1.QUERY_META_KEY, existingParams, target, propertyKey);
24
+ metaKey = container_1.QUERY_META_KEY;
42
25
  break;
43
26
  case "route:body":
44
- Reflect.defineMetadata(container_1.REQUEST_BODY_META_KEY, existingParams, target, propertyKey);
27
+ metaKey = container_1.REQUEST_BODY_META_KEY;
45
28
  break;
46
29
  case "route:user":
47
- Reflect.defineMetadata(container_1.REQUEST_USER_META_KEY, existingParams, target, propertyKey);
30
+ metaKey = container_1.REQUEST_USER_META_KEY;
48
31
  break;
49
32
  case "route:header":
50
- Reflect.defineMetadata(container_1.REQUEST_HEADER_META_KEY, existingParams, target, propertyKey);
33
+ metaKey = container_1.REQUEST_HEADER_META_KEY;
51
34
  break;
52
35
  default:
53
- break;
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 SimpleQueue {
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(adapter: QueueAdapter, jobHandler: (job: Job) => Promise<void>);
26
- addJob(data: any): Promise<void>;
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
- private static instance;
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
- * @copyright 2024
4
- * @author Tareq Hossain
5
- * @email xtrinsic96@gmail.com
6
- * @url https://github.com/xtareq
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 helpers_1 = require("./helpers");
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 (error) {
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
- // class RedisQueueAdapter implements QueueAdapter {
32
- // private client = createClient();
33
- // private queueKey: string;
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.adapter = adapter;
56
- this.jobHandler = jobHandler;
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 addJob(data) {
59
- const job = { id: helpers_1.uuid, data };
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.processNext();
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
- const jobs = await this.adapter.loadJobs();
70
- if (jobs.length === 0) {
71
- this.processing = false;
72
- return;
73
- }
74
- const job = jobs.shift();
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
- catch (error) {
80
- console.error(`Error processing job ${job.id}:`, error);
81
- jobs.unshift(job);
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.processing = false;
85
- this.processNext();
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
- class QueueManager {
90
- constructor(adapter) {
91
- this.adapter = adapter;
93
+ async onDone(cb) {
94
+ this.on("done", cb);
92
95
  }
93
- static getInstance(adapter) {
94
- if (!QueueManager.instance) {
95
- QueueManager.instance = new QueueManager(adapter);
96
- }
97
- return QueueManager.instance;
96
+ async onFailed(cb) {
97
+ this.on("failed", cb);
98
98
  }
99
- createQueue(jobHandler) {
100
- return new SimpleQueue(this.adapter, jobHandler);
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);
@@ -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
- jest.clearAllMocks();
43
- adapter = new queue_1.FileQueueAdapter("testqueue");
44
- queueManager = queue_1.QueueManager.getInstance(adapter);
45
- handler = jest.fn().mockResolvedValue(undefined);
46
- fs_1.promises.readFile.mockResolvedValue("[]");
47
- fs_1.promises.writeFile.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_1.promises.readFile).toHaveBeenCalled();
53
- expect(fs_1.promises.writeFile).toHaveBeenCalled();
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
- const another = queue_1.QueueManager.getInstance(adapter);
77
- expect(another).toBe(queueManager);
78
- });
75
+ // it("QueueManager should be singleton", () => {
76
+ // const another = QueueManager.getInstance(adapter);
77
+ // expect(another).toBe(queueManager);
78
+ // });
79
79
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avleon/core",
3
- "version": "0.0.42-rc0.1",
3
+ "version": "0.0.44",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "keywords": [