@avleon/core 0.0.43 → 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.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
- args[p.index] =
509
- 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);
510
510
  });
511
- // Map query parameters
512
511
  meta.query.forEach((q) => {
513
- 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);
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,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avleon/core",
3
- "version": "0.0.43",
3
+ "version": "0.0.44",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "keywords": [