adorn-api 1.0.31 → 1.0.32
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/package.json +2 -2
- package/src/adapter/metal-orm/field-builder.ts +148 -5
- package/src/adapter/metal-orm/index.ts +6 -0
- package/tests/unit/metal-orm.test.ts +49 -13
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adorn-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "Decorator-first web framework with OpenAPI 3.1 schema generation.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"express": "^4.19.2",
|
|
18
|
-
"metal-orm": "^1.0.
|
|
18
|
+
"metal-orm": "^1.0.94"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/express": "^4.17.21",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { ColumnDef } from "metal-orm";
|
|
2
1
|
import {
|
|
3
2
|
columnTypeToOpenApiFormat,
|
|
4
3
|
columnTypeToOpenApiType,
|
|
5
|
-
getColumnMap
|
|
4
|
+
getColumnMap,
|
|
5
|
+
getDecoratorMetadata,
|
|
6
|
+
type ColumnDef,
|
|
7
|
+
type TransformerMetadata
|
|
6
8
|
} from "metal-orm";
|
|
7
|
-
import type { SchemaNode } from "../../core/schema";
|
|
9
|
+
import type { SchemaNode, StringSchema } from "../../core/schema";
|
|
8
10
|
import { t } from "../../core/schema";
|
|
9
11
|
import type { FieldMeta } from "../../core/metadata";
|
|
10
12
|
import type { FieldOverride } from "../../core/decorators";
|
|
@@ -46,6 +48,7 @@ export function buildFields(
|
|
|
46
48
|
const include = options.include ? new Set(options.include) : undefined;
|
|
47
49
|
const exclude = options.exclude ? new Set(options.exclude) : undefined;
|
|
48
50
|
const mode = options.mode ?? "response";
|
|
51
|
+
const transformerMetadata = getTransformerMetadata(target);
|
|
49
52
|
const fields: Record<string, FieldMeta> = {};
|
|
50
53
|
|
|
51
54
|
for (const [name, col] of Object.entries(columns)) {
|
|
@@ -59,7 +62,7 @@ export function buildFields(
|
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
fields[name] = buildFieldMeta(col, mode);
|
|
65
|
+
fields[name] = buildFieldMeta(col, mode, transformerMetadata?.[name]);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
if (options.overrides) {
|
|
@@ -69,11 +72,16 @@ export function buildFields(
|
|
|
69
72
|
return fields;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
export function buildFieldMeta(
|
|
75
|
+
export function buildFieldMeta(
|
|
76
|
+
col: any,
|
|
77
|
+
mode: MetalDtoMode,
|
|
78
|
+
transformer?: TransformerMetadata
|
|
79
|
+
): FieldMeta {
|
|
73
80
|
let schema = columnToSchemaNode(col);
|
|
74
81
|
if (!col.notNull) {
|
|
75
82
|
schema = t.nullable(schema);
|
|
76
83
|
}
|
|
84
|
+
schema = applyTransformerMetadata(schema, transformer, mode);
|
|
77
85
|
|
|
78
86
|
const optional = isOptional(col, mode);
|
|
79
87
|
const field: FieldMeta = { schema };
|
|
@@ -206,6 +214,141 @@ function isSchemaNode(value: unknown): value is SchemaNode {
|
|
|
206
214
|
return !!value && typeof value === "object" && "kind" in (value as SchemaNode);
|
|
207
215
|
}
|
|
208
216
|
|
|
217
|
+
function getTransformerMetadata(
|
|
218
|
+
target: any
|
|
219
|
+
): Record<string, TransformerMetadata> | undefined {
|
|
220
|
+
if (typeof target !== "function") {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
const meta = getDecoratorMetadata(target);
|
|
224
|
+
if (!meta?.transformers?.length) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const output: Record<string, TransformerMetadata> = {};
|
|
228
|
+
for (const entry of meta.transformers) {
|
|
229
|
+
output[entry.propertyName] = entry.metadata;
|
|
230
|
+
}
|
|
231
|
+
return output;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function applyTransformerMetadata(
|
|
235
|
+
schema: SchemaNode,
|
|
236
|
+
transformer: TransformerMetadata | undefined,
|
|
237
|
+
mode: MetalDtoMode
|
|
238
|
+
): SchemaNode {
|
|
239
|
+
if (!transformer || !shouldApplyTransformers(transformer, mode)) {
|
|
240
|
+
return schema;
|
|
241
|
+
}
|
|
242
|
+
if (schema.kind !== "string") {
|
|
243
|
+
return schema;
|
|
244
|
+
}
|
|
245
|
+
const stringSchema = schema as StringSchema;
|
|
246
|
+
for (const validator of transformer.validators ?? []) {
|
|
247
|
+
applyStringValidator(stringSchema, validator);
|
|
248
|
+
}
|
|
249
|
+
return stringSchema;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function shouldApplyTransformers(
|
|
253
|
+
transformer: TransformerMetadata,
|
|
254
|
+
mode: MetalDtoMode
|
|
255
|
+
): boolean {
|
|
256
|
+
if (transformer.executionOrder === "both") {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (mode === "response") {
|
|
260
|
+
return transformer.executionOrder === "after-load";
|
|
261
|
+
}
|
|
262
|
+
return transformer.executionOrder === "before-save";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function applyStringValidator(
|
|
266
|
+
schema: StringSchema,
|
|
267
|
+
validator: { name?: string }
|
|
268
|
+
): void {
|
|
269
|
+
const name = validator.name?.toLowerCase();
|
|
270
|
+
switch (name) {
|
|
271
|
+
case "email":
|
|
272
|
+
if (!schema.format || schema.format === "email") {
|
|
273
|
+
schema.format = "email";
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
case "length":
|
|
277
|
+
applyLengthValidator(schema, validator);
|
|
278
|
+
break;
|
|
279
|
+
case "pattern":
|
|
280
|
+
applyPatternValidator(schema, validator);
|
|
281
|
+
break;
|
|
282
|
+
case "alphanumeric":
|
|
283
|
+
applyAlphanumericValidator(schema, validator);
|
|
284
|
+
break;
|
|
285
|
+
default:
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyLengthValidator(
|
|
291
|
+
schema: StringSchema,
|
|
292
|
+
validator: { [key: string]: unknown }
|
|
293
|
+
): void {
|
|
294
|
+
const options = (validator as { options?: { min?: number; max?: number; exact?: number } }).options;
|
|
295
|
+
if (!options) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (typeof options.exact === "number") {
|
|
299
|
+
schema.minLength = options.exact;
|
|
300
|
+
schema.maxLength = options.exact;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (typeof options.min === "number") {
|
|
304
|
+
schema.minLength = schema.minLength !== undefined
|
|
305
|
+
? Math.max(schema.minLength, options.min)
|
|
306
|
+
: options.min;
|
|
307
|
+
}
|
|
308
|
+
if (typeof options.max === "number") {
|
|
309
|
+
schema.maxLength = schema.maxLength !== undefined
|
|
310
|
+
? Math.min(schema.maxLength, options.max)
|
|
311
|
+
: options.max;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function applyPatternValidator(
|
|
316
|
+
schema: StringSchema,
|
|
317
|
+
validator: { [key: string]: unknown }
|
|
318
|
+
): void {
|
|
319
|
+
if (schema.pattern) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const options = (validator as { options?: { pattern?: RegExp } }).options;
|
|
323
|
+
if (!options?.pattern) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
schema.pattern = options.pattern.source;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function applyAlphanumericValidator(
|
|
330
|
+
schema: StringSchema,
|
|
331
|
+
validator: { [key: string]: unknown }
|
|
332
|
+
): void {
|
|
333
|
+
if (schema.pattern) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const options = (validator as {
|
|
337
|
+
options?: { allowSpaces?: boolean; allowUnderscores?: boolean; allowHyphens?: boolean }
|
|
338
|
+
}).options;
|
|
339
|
+
const extras: string[] = [];
|
|
340
|
+
if (options?.allowSpaces) {
|
|
341
|
+
extras.push(" ");
|
|
342
|
+
}
|
|
343
|
+
if (options?.allowUnderscores) {
|
|
344
|
+
extras.push("_");
|
|
345
|
+
}
|
|
346
|
+
if (options?.allowHyphens) {
|
|
347
|
+
extras.push("-");
|
|
348
|
+
}
|
|
349
|
+
schema.pattern = `^[a-zA-Z0-9${extras.join("")}]*$`;
|
|
350
|
+
}
|
|
351
|
+
|
|
209
352
|
/**
|
|
210
353
|
* Validates that an entity has properly loaded metadata (columns).
|
|
211
354
|
* Throws an error with helpful message if validation fails.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// Ensure standard decorator metadata is available for metal-orm transformers.
|
|
2
|
+
const symbolMetadata = (Symbol as { metadata?: symbol }).metadata;
|
|
3
|
+
if (!symbolMetadata) {
|
|
4
|
+
(Symbol as { metadata?: symbol }).metadata = Symbol("Symbol.metadata");
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export {
|
|
2
8
|
MetalDto
|
|
3
9
|
} from "./dto";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
createMetalDtoOverrides
|
|
13
13
|
} from "../../src/adapter/metal-orm/index";
|
|
14
14
|
import { HttpError } from "../../src/core/errors";
|
|
15
|
-
import { Column, Entity, PrimaryKey, col } from "metal-orm";
|
|
15
|
+
import { Alphanumeric, Column, Email, Entity, Length, Pattern, PrimaryKey, col } from "metal-orm";
|
|
16
16
|
import { getDtoMeta } from "../../src/core/metadata";
|
|
17
17
|
|
|
18
18
|
describe("metal-orm helpers", () => {
|
|
@@ -240,20 +240,20 @@ describe("metal-orm helpers", () => {
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
-
describe("createMetalCrudDtos", () => {
|
|
244
|
-
@Entity({ tableName: "crud_dto_entities" })
|
|
245
|
-
class CrudDtoEntity {
|
|
246
|
-
@PrimaryKey(col.autoIncrement(col.int()))
|
|
243
|
+
describe("createMetalCrudDtos", () => {
|
|
244
|
+
@Entity({ tableName: "crud_dto_entities" })
|
|
245
|
+
class CrudDtoEntity {
|
|
246
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
247
247
|
id!: number;
|
|
248
248
|
|
|
249
249
|
@Column(col.notNull(col.text()))
|
|
250
250
|
name!: string;
|
|
251
251
|
|
|
252
252
|
@Column(col.text())
|
|
253
|
-
nickname?: string | null;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
it("creates CRUD DTO decorators with defaults", () => {
|
|
253
|
+
nickname?: string | null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
it("creates CRUD DTO decorators with defaults", () => {
|
|
257
257
|
const crud = createMetalCrudDtos(CrudDtoEntity, {
|
|
258
258
|
mutationExclude: ["id"]
|
|
259
259
|
});
|
|
@@ -277,10 +277,46 @@ describe("metal-orm helpers", () => {
|
|
|
277
277
|
|
|
278
278
|
expect(responseMeta?.fields.id).toBeDefined();
|
|
279
279
|
expect(createMeta?.fields.id).toBeUndefined();
|
|
280
|
-
expect(updateMeta?.fields.name?.optional).toBe(true);
|
|
281
|
-
expect(Object.keys(paramsMeta?.fields ?? {})).toEqual(["id"]);
|
|
282
|
-
});
|
|
283
|
-
|
|
280
|
+
expect(updateMeta?.fields.name?.optional).toBe(true);
|
|
281
|
+
expect(Object.keys(paramsMeta?.fields ?? {})).toEqual(["id"]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
@Entity({ tableName: "transformer_entities" })
|
|
285
|
+
class TransformerEntity {
|
|
286
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
287
|
+
id!: number;
|
|
288
|
+
|
|
289
|
+
@Column(col.varchar(50))
|
|
290
|
+
@Length({ min: 2, max: 10 })
|
|
291
|
+
name!: string;
|
|
292
|
+
|
|
293
|
+
@Column(col.text())
|
|
294
|
+
@Pattern({ pattern: /^[A-Z]+$/ })
|
|
295
|
+
code!: string;
|
|
296
|
+
|
|
297
|
+
@Column(col.text())
|
|
298
|
+
@Email()
|
|
299
|
+
email!: string;
|
|
300
|
+
|
|
301
|
+
@Column(col.text())
|
|
302
|
+
@Alphanumeric({ allowHyphens: true })
|
|
303
|
+
slug!: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
it("maps transformer validators into string schemas", () => {
|
|
307
|
+
const crud = createMetalCrudDtos(TransformerEntity);
|
|
308
|
+
|
|
309
|
+
@crud.create
|
|
310
|
+
class CreateTransformerDto {}
|
|
311
|
+
|
|
312
|
+
const meta = getDtoMeta(CreateTransformerDto);
|
|
313
|
+
expect((meta?.fields.email?.schema as any).format).toBe("email");
|
|
314
|
+
expect((meta?.fields.name?.schema as any).minLength).toBe(2);
|
|
315
|
+
expect((meta?.fields.name?.schema as any).maxLength).toBe(10);
|
|
316
|
+
expect((meta?.fields.code?.schema as any).pattern).toBe("^[A-Z]+$");
|
|
317
|
+
expect((meta?.fields.slug?.schema as any).pattern).toBe("^[a-zA-Z0-9-]*$");
|
|
318
|
+
});
|
|
319
|
+
});
|
|
284
320
|
|
|
285
321
|
describe("createMetalCrudDtoClasses", () => {
|
|
286
322
|
@Entity({ tableName: "crud_dto_class_entities" })
|