adorn-api 1.0.31 → 1.0.33
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 +238 -3
- package/dist/adapter/express/coercion.d.ts +1 -1
- package/dist/adapter/express/coercion.js +33 -3
- package/dist/adapter/express/controllers.js +13 -2
- package/dist/adapter/express/response-serializer.d.ts +2 -0
- package/dist/adapter/express/response-serializer.js +117 -0
- package/dist/adapter/metal-orm/field-builder.d.ts +2 -1
- package/dist/adapter/metal-orm/field-builder.js +127 -3
- package/dist/adapter/metal-orm/index.d.ts +1 -1
- package/dist/adapter/metal-orm/index.js +9 -1
- package/dist/adapter/metal-orm/utils.d.ts +29 -0
- package/dist/adapter/metal-orm/utils.js +39 -0
- package/dist/core/validation/validators/string-validator.js +20 -0
- package/dist/core/validation/validators/validation-utils.d.ts +6 -0
- package/dist/core/validation/validators/validation-utils.js +16 -0
- package/dist/core/validation-errors.d.ts +1 -0
- package/dist/core/validation-errors.js +1 -0
- package/examples/metal-orm-postgres/app.ts +18 -0
- package/examples/metal-orm-postgres/db.ts +67 -0
- package/examples/metal-orm-postgres/index.ts +6 -0
- package/examples/metal-orm-postgres/post.controller.ts +209 -0
- package/examples/metal-orm-postgres/post.dtos.ts +78 -0
- package/examples/metal-orm-postgres/post.entity.ts +24 -0
- package/examples/metal-orm-postgres/user.controller.helpers.ts +305 -0
- package/examples/metal-orm-postgres/user.controller.ts +231 -0
- package/examples/metal-orm-postgres/user.dtos.ts +88 -0
- package/examples/metal-orm-postgres/user.entity.ts +21 -0
- package/package.json +4 -2
- package/src/adapter/express/coercion.ts +35 -4
- package/src/adapter/express/controllers.ts +17 -2
- package/src/adapter/express/response-serializer.ts +133 -0
- package/src/adapter/metal-orm/field-builder.ts +167 -6
- package/src/adapter/metal-orm/index.ts +55 -46
- package/src/adapter/metal-orm/utils.ts +52 -0
- package/src/core/validation/validators/string-validator.ts +45 -18
- package/src/core/validation/validators/validation-utils.ts +21 -5
- package/src/core/validation-errors.ts +4 -3
- package/tests/e2e/sqlserver-metal-orm.e2e.test.ts +449 -0
- package/tests/unit/metal-orm.test.ts +49 -13
- package/tests/unit/validation.test.ts +22 -9
package/README.md
CHANGED
|
@@ -9,10 +9,11 @@ A modern, decorator-first web framework built on Express with built-in OpenAPI 3
|
|
|
9
9
|
- 🔌 **Express Integration**: Built on top of Express for familiarity and extensibility
|
|
10
10
|
- 🎯 **Type-Safe Data Transfer Objects**: Define schemas with TypeScript for compile-time checks
|
|
11
11
|
- 🔄 **DTO Composition**: Reuse and compose DTOs with PickDto, OmitDto, PartialDto, and MergeDto
|
|
12
|
-
- 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs
|
|
12
|
+
- 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs, including transformer-aware schema generation
|
|
13
13
|
- 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
|
|
14
14
|
- 📝 **Request Validation**: Automatic validation of request bodies, params, query, and headers
|
|
15
|
-
-
|
|
15
|
+
- 🔧 **Transformers**: Custom field transformations with @Transform decorator and built-in transform functions
|
|
16
|
+
- **Error Handling**: Structured error responses with error DTO support
|
|
16
17
|
- 💾 **File Uploads**: Easy handling of file uploads with multipart form data
|
|
17
18
|
- 🌐 **CORS Support**: Built-in CORS configuration
|
|
18
19
|
- 🏗️ **Lifecycle Hooks**: Application bootstrap and shutdown lifecycle events
|
|
@@ -262,7 +263,8 @@ class UploadController {
|
|
|
262
263
|
|
|
263
264
|
## Metal ORM Integration
|
|
264
265
|
|
|
265
|
-
Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
|
|
266
|
+
Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
|
|
267
|
+
Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
|
|
266
268
|
|
|
267
269
|
### 1. Define Entities
|
|
268
270
|
|
|
@@ -384,6 +386,10 @@ createExpressApp({
|
|
|
384
386
|
cors: true, // Enable CORS with default options or configure
|
|
385
387
|
jsonBody: true, // Parse JSON bodies (default: true)
|
|
386
388
|
inputCoercion: "safe", // Input coercion mode ("safe" or "strict")
|
|
389
|
+
validation: { // Validation configuration
|
|
390
|
+
enabled: true, // Enable validation (default: true)
|
|
391
|
+
mode: "strict" // Validation mode: "strict" or "safe"
|
|
392
|
+
},
|
|
387
393
|
multipart: { // File upload configuration
|
|
388
394
|
dest: "./uploads",
|
|
389
395
|
limits: { fileSize: 50 * 1024 * 1024 }
|
|
@@ -483,6 +489,234 @@ import { lifecycleRegistry } from "adorn-api";
|
|
|
483
489
|
lifecycleRegistry.register(new DatabaseService());
|
|
484
490
|
```
|
|
485
491
|
|
|
492
|
+
## Validation
|
|
493
|
+
|
|
494
|
+
Adorn API provides automatic request validation and a comprehensive validation system for your DTOs and schemas.
|
|
495
|
+
|
|
496
|
+
### Validation Configuration
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
createExpressApp({
|
|
500
|
+
controllers: [UserController],
|
|
501
|
+
validation: {
|
|
502
|
+
enabled: true, // Enable validation (default: true)
|
|
503
|
+
mode: "strict" // Validation mode: "strict" or "safe"
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Validation Errors
|
|
509
|
+
|
|
510
|
+
Invalid requests automatically return structured validation errors:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// Example error response
|
|
514
|
+
{
|
|
515
|
+
"statusCode": 400,
|
|
516
|
+
"message": "Validation failed",
|
|
517
|
+
"errors": [
|
|
518
|
+
{
|
|
519
|
+
"field": "name",
|
|
520
|
+
"message": "must be at least 1 character long",
|
|
521
|
+
"value": "",
|
|
522
|
+
"code": "STRING_MIN_LENGTH"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"field": "email",
|
|
526
|
+
"message": "must be a valid email",
|
|
527
|
+
"value": "invalid-email",
|
|
528
|
+
"code": "FORMAT_EMAIL"
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Validation Error Codes
|
|
535
|
+
|
|
536
|
+
Adorn API provides machine-readable error codes for programmatic error handling:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { ValidationErrorCode } from "adorn-api";
|
|
540
|
+
|
|
541
|
+
console.log(ValidationErrorCode.FORMAT_EMAIL); // "FORMAT_EMAIL"
|
|
542
|
+
console.log(ValidationErrorCode.STRING_MIN_LENGTH); // "STRING_MIN_LENGTH"
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Manual Validation
|
|
546
|
+
|
|
547
|
+
You can also manually validate data using the `validate` function:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { validate, ValidationErrors, t } from "adorn-api";
|
|
551
|
+
|
|
552
|
+
const data = { name: "", email: "invalid" };
|
|
553
|
+
const errors = validate(data, t.object({
|
|
554
|
+
name: t.string({ minLength: 1 }),
|
|
555
|
+
email: t.string({ format: "email" })
|
|
556
|
+
}));
|
|
557
|
+
|
|
558
|
+
if (errors.length > 0) {
|
|
559
|
+
throw new ValidationErrors(errors);
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
## Transformers
|
|
564
|
+
|
|
565
|
+
Transform fields during serialization with custom transform functions or built-in transform utilities.
|
|
566
|
+
|
|
567
|
+
### Basic Transform
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
import { Dto, Field, Transform, t } from "adorn-api";
|
|
571
|
+
|
|
572
|
+
@Dto()
|
|
573
|
+
export class UserDto {
|
|
574
|
+
@Field(t.string())
|
|
575
|
+
@Transform((value) => value.toUpperCase())
|
|
576
|
+
name!: string;
|
|
577
|
+
|
|
578
|
+
@Field(t.dateTime())
|
|
579
|
+
@Transform((value) => value.toISOString())
|
|
580
|
+
createdAt!: Date;
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Built-in Transforms
|
|
585
|
+
|
|
586
|
+
Adorn API includes common transform functions:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
import { Dto, Field, Transform, Transforms, t } from "adorn-api";
|
|
590
|
+
|
|
591
|
+
@Dto()
|
|
592
|
+
export class UserDto {
|
|
593
|
+
@Field(t.string())
|
|
594
|
+
@Transform(Transforms.toLowerCase)
|
|
595
|
+
email!: string;
|
|
596
|
+
|
|
597
|
+
@Field(t.number())
|
|
598
|
+
@Transform(Transforms.round(2))
|
|
599
|
+
price!: number;
|
|
600
|
+
|
|
601
|
+
@Field(t.string())
|
|
602
|
+
@Transform(Transforms.mask(4)) // Mask all but last 4 characters
|
|
603
|
+
creditCard!: string;
|
|
604
|
+
|
|
605
|
+
@Field(t.dateTime())
|
|
606
|
+
@Transform(Transforms.toISOString)
|
|
607
|
+
birthDate!: Date;
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Conditional Transforms with Groups
|
|
612
|
+
|
|
613
|
+
Apply transforms only to specific serialization groups:
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
import { Dto, Field, Transform, Expose, t } from "adorn-api";
|
|
617
|
+
|
|
618
|
+
@Dto()
|
|
619
|
+
export class UserDto {
|
|
620
|
+
@Field(t.string())
|
|
621
|
+
name!: string;
|
|
622
|
+
|
|
623
|
+
@Field(t.string())
|
|
624
|
+
@Expose({ groups: ["admin"] })
|
|
625
|
+
@Transform((value) => Transforms.mask(2), { groups: ["admin"] })
|
|
626
|
+
phoneNumber!: string;
|
|
627
|
+
|
|
628
|
+
@Field(t.string())
|
|
629
|
+
@Expose({ groups: ["internal"] })
|
|
630
|
+
@Transform((value) => "[REDACTED]", { groups: ["external"] })
|
|
631
|
+
internalNote!: string;
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Custom Transform Functions
|
|
636
|
+
|
|
637
|
+
Create custom transform functions:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { Dto, Field, Transform, t } from "adorn-api";
|
|
641
|
+
|
|
642
|
+
const toCurrency = (value: number, currency: string = "USD") => {
|
|
643
|
+
return new Intl.NumberFormat("en-US", {
|
|
644
|
+
style: "currency",
|
|
645
|
+
currency
|
|
646
|
+
}).format(value);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
@Dto()
|
|
650
|
+
export class ProductDto {
|
|
651
|
+
@Field(t.string())
|
|
652
|
+
name!: string;
|
|
653
|
+
|
|
654
|
+
@Field(t.number())
|
|
655
|
+
@Transform(toCurrency)
|
|
656
|
+
price!: number;
|
|
657
|
+
|
|
658
|
+
@Field(t.number())
|
|
659
|
+
@Transform((value) => toCurrency(value, "EUR"))
|
|
660
|
+
priceEUR!: number;
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Serialization with Options
|
|
665
|
+
|
|
666
|
+
Control serialization with custom options:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
import { serialize, createSerializer } from "adorn-api";
|
|
670
|
+
import { UserDto } from "./user.dtos";
|
|
671
|
+
|
|
672
|
+
const user = new UserDto();
|
|
673
|
+
user.name = "John Doe";
|
|
674
|
+
user.phoneNumber = "123-456-7890";
|
|
675
|
+
user.internalNote = "This is an internal note";
|
|
676
|
+
|
|
677
|
+
// Basic serialization
|
|
678
|
+
const basic = serialize(user);
|
|
679
|
+
// Output: { name: "John Doe" }
|
|
680
|
+
|
|
681
|
+
// Admin group serialization
|
|
682
|
+
const admin = serialize(user, { groups: ["admin"] });
|
|
683
|
+
// Output: { name: "John Doe", phoneNumber: "********90" }
|
|
684
|
+
|
|
685
|
+
// External group serialization
|
|
686
|
+
const external = serialize(user, { groups: ["external"] });
|
|
687
|
+
// Output: { name: "John Doe", internalNote: "[REDACTED]" }
|
|
688
|
+
|
|
689
|
+
// Create a preset serializer
|
|
690
|
+
const adminSerializer = createSerializer({ groups: ["admin"] });
|
|
691
|
+
const adminData = adminSerializer(user);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Exclude Fields
|
|
695
|
+
|
|
696
|
+
Control which fields are excluded or exposed:
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
import { Dto, Field, Exclude, Expose, t } from "adorn-api";
|
|
700
|
+
|
|
701
|
+
@Dto()
|
|
702
|
+
export class UserDto {
|
|
703
|
+
@Field(t.string())
|
|
704
|
+
name!: string;
|
|
705
|
+
|
|
706
|
+
@Field(t.string())
|
|
707
|
+
@Exclude() // Always exclude from serialization
|
|
708
|
+
password!: string;
|
|
709
|
+
|
|
710
|
+
@Field(t.string())
|
|
711
|
+
@Expose({ name: "email_address" }) // Rename field in output
|
|
712
|
+
email!: string;
|
|
713
|
+
|
|
714
|
+
@Field(t.string())
|
|
715
|
+
@Exclude({ groups: ["public"] }) // Exclude from public group
|
|
716
|
+
internalComment!: string;
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
486
720
|
## Examples
|
|
487
721
|
|
|
488
722
|
Check out the `examples/` directory for more comprehensive examples:
|
|
@@ -493,6 +727,7 @@ Check out the `examples/` directory for more comprehensive examples:
|
|
|
493
727
|
- `metal-orm-sqlite-music/` - Complex relations with Metal ORM
|
|
494
728
|
- `streaming/` - SSE and streaming responses
|
|
495
729
|
- `openapi/` - OpenAPI documentation customization
|
|
730
|
+
- `validation/` - Comprehensive validation examples with various schema types
|
|
496
731
|
|
|
497
732
|
## Testing
|
|
498
733
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type InputMeta } from "../../core/metadata";
|
|
2
2
|
import type { InputCoercionMode } from "./types";
|
|
3
|
-
export type InputLocation = "params" | "query";
|
|
3
|
+
export type InputLocation = "params" | "query" | "body";
|
|
4
4
|
interface CoerceInputOptions {
|
|
5
5
|
mode: InputCoercionMode;
|
|
6
6
|
location: InputLocation;
|
|
@@ -57,7 +57,7 @@ function coerceValue(value, schema, mode) {
|
|
|
57
57
|
return coerceBoolean(value);
|
|
58
58
|
}
|
|
59
59
|
case "string": {
|
|
60
|
-
return coerceString(value);
|
|
60
|
+
return coerceString(value, schema);
|
|
61
61
|
}
|
|
62
62
|
case "array":
|
|
63
63
|
return coerceArrayValue(value, schema, mode);
|
|
@@ -101,13 +101,37 @@ function coerceBoolean(value) {
|
|
|
101
101
|
}
|
|
102
102
|
return { value: parsed, ok: true, changed: parsed !== value };
|
|
103
103
|
}
|
|
104
|
-
function coerceString(value) {
|
|
104
|
+
function coerceString(value, schema) {
|
|
105
|
+
if (!isPresent(value)) {
|
|
106
|
+
return { value, ok: true, changed: false };
|
|
107
|
+
}
|
|
108
|
+
if (schema.format === "date" || schema.format === "date-time") {
|
|
109
|
+
const parsed = parseDateValue(value);
|
|
110
|
+
if (!parsed) {
|
|
111
|
+
return { value, ok: false, changed: false };
|
|
112
|
+
}
|
|
113
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
114
|
+
}
|
|
105
115
|
const parsed = coerce_1.coerce.string(value);
|
|
106
116
|
if (parsed === undefined) {
|
|
107
117
|
return { value, ok: true, changed: false };
|
|
108
118
|
}
|
|
109
119
|
return { value: parsed, ok: true, changed: parsed !== value };
|
|
110
120
|
}
|
|
121
|
+
function parseDateValue(value) {
|
|
122
|
+
if (value instanceof Date) {
|
|
123
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
124
|
+
}
|
|
125
|
+
const text = coerce_1.coerce.string(value);
|
|
126
|
+
if (text === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const parsed = new Date(text);
|
|
130
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
111
135
|
function coerceArrayValue(value, schema, mode) {
|
|
112
136
|
if (value === undefined || value === null) {
|
|
113
137
|
return { value, ok: true, changed: false };
|
|
@@ -241,7 +265,13 @@ function isPresent(value) {
|
|
|
241
265
|
return coerce_1.coerce.string(value) !== undefined;
|
|
242
266
|
}
|
|
243
267
|
function buildInvalidMessage(location, fields) {
|
|
244
|
-
|
|
268
|
+
let label = "query parameter";
|
|
269
|
+
if (location === "params") {
|
|
270
|
+
label = "path parameter";
|
|
271
|
+
}
|
|
272
|
+
else if (location === "body") {
|
|
273
|
+
label = "request body field";
|
|
274
|
+
}
|
|
245
275
|
const suffix = fields.length > 1 ? "s" : "";
|
|
246
276
|
return `Invalid ${label}${suffix}: ${fields.join(", ")}.`;
|
|
247
277
|
}
|
|
@@ -4,6 +4,7 @@ exports.attachControllers = attachControllers;
|
|
|
4
4
|
const metadata_1 = require("../../core/metadata");
|
|
5
5
|
const errors_1 = require("../../core/errors");
|
|
6
6
|
const coercion_1 = require("./coercion");
|
|
7
|
+
const response_serializer_1 = require("./response-serializer");
|
|
7
8
|
const multipart_1 = require("./multipart");
|
|
8
9
|
const lifecycle_1 = require("../../core/lifecycle");
|
|
9
10
|
const streaming_1 = require("../../core/streaming");
|
|
@@ -38,6 +39,9 @@ async function attachControllers(app, controllers, inputCoercion = "safe", multi
|
|
|
38
39
|
const coerceQuery = inputCoercion === false
|
|
39
40
|
? undefined
|
|
40
41
|
: (0, coercion_1.createInputCoercer)(route.query, { mode: inputCoercion, location: "query" });
|
|
42
|
+
const coerceBody = inputCoercion === false
|
|
43
|
+
? undefined
|
|
44
|
+
: (0, coercion_1.createInputCoercer)(route.body, { mode: inputCoercion, location: "body" });
|
|
41
45
|
// Build middleware chain
|
|
42
46
|
const middlewares = [];
|
|
43
47
|
// Add multipart middleware if route has file uploads
|
|
@@ -54,7 +58,7 @@ async function attachControllers(app, controllers, inputCoercion = "safe", multi
|
|
|
54
58
|
const ctx = {
|
|
55
59
|
req,
|
|
56
60
|
res,
|
|
57
|
-
body: req.body,
|
|
61
|
+
body: coerceBody ? coerceBody(req.body) : req.body,
|
|
58
62
|
query: coerceQuery ? coerceQuery(req.query) : req.query,
|
|
59
63
|
params: coerceParams ? coerceParams(req.params) : req.params,
|
|
60
64
|
headers: req.headers,
|
|
@@ -94,7 +98,9 @@ async function attachControllers(app, controllers, inputCoercion = "safe", multi
|
|
|
94
98
|
res.status(defaultStatus(route)).end();
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
97
|
-
|
|
101
|
+
const responseSchema = getResponseSchema(route);
|
|
102
|
+
const output = responseSchema ? (0, response_serializer_1.serializeResponse)(result, responseSchema) : result;
|
|
103
|
+
res.status(defaultStatus(route)).json(output);
|
|
98
104
|
}
|
|
99
105
|
catch (error) {
|
|
100
106
|
if ((0, validation_errors_1.isValidationErrors)(error)) {
|
|
@@ -118,6 +124,11 @@ function defaultStatus(route) {
|
|
|
118
124
|
const success = responses.find((response) => !response.error && response.status < 400);
|
|
119
125
|
return success?.status ?? 200;
|
|
120
126
|
}
|
|
127
|
+
function getResponseSchema(route) {
|
|
128
|
+
const responses = route.responses ?? [];
|
|
129
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
130
|
+
return success?.schema;
|
|
131
|
+
}
|
|
121
132
|
function sendValidationError(res, error) {
|
|
122
133
|
if (res.headersSent) {
|
|
123
134
|
return;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serializeResponse = serializeResponse;
|
|
4
|
+
const metadata_1 = require("../../core/metadata");
|
|
5
|
+
function serializeResponse(value, schema) {
|
|
6
|
+
if (value === null || value === undefined) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (isSchemaNode(schema)) {
|
|
10
|
+
return serializeWithSchema(value, schema);
|
|
11
|
+
}
|
|
12
|
+
return serializeWithDto(value, schema);
|
|
13
|
+
}
|
|
14
|
+
function serializeWithDto(value, dto) {
|
|
15
|
+
if (value === null || value === undefined) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return value.map((entry) => serializeWithDto(entry, dto));
|
|
20
|
+
}
|
|
21
|
+
if (!isPlainObject(value)) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
const meta = (0, metadata_1.getDtoMeta)(dto);
|
|
25
|
+
if (!meta) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
const output = { ...value };
|
|
29
|
+
for (const [name, field] of Object.entries(meta.fields)) {
|
|
30
|
+
if (name in value) {
|
|
31
|
+
output[name] = serializeWithSchema(value[name], field.schema);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
function serializeWithSchema(value, schema) {
|
|
37
|
+
if (value === null || value === undefined) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
switch (schema.kind) {
|
|
41
|
+
case "string":
|
|
42
|
+
return serializeString(value, schema.format);
|
|
43
|
+
case "array":
|
|
44
|
+
if (!Array.isArray(value)) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
return value.map((entry) => serializeWithSchema(entry, schema.items));
|
|
48
|
+
case "object":
|
|
49
|
+
return serializeObject(value, schema.properties);
|
|
50
|
+
case "record":
|
|
51
|
+
if (!isPlainObject(value)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
return serializeRecord(value, schema.values);
|
|
55
|
+
case "ref":
|
|
56
|
+
return serializeWithDto(value, schema.dto);
|
|
57
|
+
case "union":
|
|
58
|
+
return serializeUnion(value, schema.anyOf);
|
|
59
|
+
default:
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function serializeString(value, format) {
|
|
64
|
+
if (!(value instanceof Date)) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (Number.isNaN(value.getTime())) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (format === "date") {
|
|
71
|
+
return value.toISOString().slice(0, 10);
|
|
72
|
+
}
|
|
73
|
+
if (format === "date-time") {
|
|
74
|
+
return value.toISOString();
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function serializeObject(value, properties) {
|
|
79
|
+
if (!isPlainObject(value)) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
const output = { ...value };
|
|
83
|
+
if (!properties) {
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
for (const [key, schema] of Object.entries(properties)) {
|
|
87
|
+
if (key in value) {
|
|
88
|
+
output[key] = serializeWithSchema(value[key], schema);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
function serializeRecord(value, schema) {
|
|
94
|
+
const output = { ...value };
|
|
95
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
96
|
+
output[key] = serializeWithSchema(entry, schema);
|
|
97
|
+
}
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
function serializeUnion(value, options) {
|
|
101
|
+
for (const option of options) {
|
|
102
|
+
const serialized = serializeWithSchema(value, option);
|
|
103
|
+
if (serialized !== value) {
|
|
104
|
+
return serialized;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function isSchemaNode(value) {
|
|
110
|
+
return !!value && typeof value === "object" && "kind" in value;
|
|
111
|
+
}
|
|
112
|
+
function isPlainObject(value) {
|
|
113
|
+
return (value !== null &&
|
|
114
|
+
typeof value === "object" &&
|
|
115
|
+
!Array.isArray(value) &&
|
|
116
|
+
!(value instanceof Date));
|
|
117
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TransformerMetadata } from "metal-orm";
|
|
1
2
|
import type { FieldMeta } from "../../core/metadata";
|
|
2
3
|
import type { FieldOverride } from "../../core/decorators";
|
|
3
4
|
export type MetalDtoMode = "response" | "create" | "update";
|
|
@@ -13,7 +14,7 @@ export interface MetalDtoOptions {
|
|
|
13
14
|
strict?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export declare function buildFields(target: any, options: MetalDtoOptions): Record<string, FieldMeta>;
|
|
16
|
-
export declare function buildFieldMeta(col: any, mode: MetalDtoMode): FieldMeta;
|
|
17
|
+
export declare function buildFieldMeta(col: any, mode: MetalDtoMode, transformer?: TransformerMetadata): FieldMeta;
|
|
17
18
|
/**
|
|
18
19
|
* Validates that an entity has properly loaded metadata (columns).
|
|
19
20
|
* Throws an error with helpful message if validation fails.
|