Skip to content

Commit a8c6f83

Browse files
committed
feat(json-api-nestjs): Extend validate for array, json and datetime, use nullable if possible
1 parent 9e0f066 commit a8c6f83

File tree

3 files changed

+167
-21
lines changed

3 files changed

+167
-21
lines changed

libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../../types';
1515
import { getEntityName, ObjectTyped } from '../utils';
1616
import { guardKeyForPropertyTarget } from './orm-type-asserts';
17+
import { ColumnType } from 'typeorm/driver/types/ColumnTypes';
1718

1819
export enum PropsNameResultField {
1920
field = 'field',
@@ -72,6 +73,7 @@ export enum TypeField {
7273
number = 'number',
7374
boolean = 'boolean',
7475
string = 'string',
76+
object = 'object',
7577
}
7678

7779
export type TypeForId = Extract<TypeField, TypeField.number | TypeField.string>;
@@ -85,6 +87,8 @@ export type FieldWithType<E extends Entity> = {
8587
? TypeField.number
8688
: E[K] extends boolean
8789
? TypeField.boolean
90+
: E[K] extends object
91+
? TypeField.object
8892
: TypeField.string;
8993
};
9094

@@ -187,6 +191,9 @@ export const getFieldWithType = <E extends Entity>(
187191
case Boolean:
188192
typeProps = TypeField.boolean;
189193
break;
194+
case Object:
195+
typeProps = TypeField.object;
196+
break;
190197
default:
191198
typeProps = TypeField.string;
192199
}
@@ -343,7 +350,9 @@ export const fromRelationTreeToArrayName = <E extends Entity>(
343350
export type AllFieldWithTpe<E extends Entity> = FieldWithType<E> & {
344351
[K in EntityRelation<E>]: E[K] extends (infer U extends Entity)[]
345352
? FieldWithType<U>
346-
: E[K] extends Entity ? FieldWithType<E[K]> : never;
353+
: E[K] extends Entity
354+
? FieldWithType<E[K]>
355+
: never;
347356
};
348357

349358
export const getTypeForAllProps = <E extends Entity>(
@@ -363,3 +372,26 @@ export const getTypeForAllProps = <E extends Entity>(
363372
...relationField,
364373
};
365374
};
375+
376+
export type PropsFieldItem = {
377+
type: ColumnType;
378+
isArray: boolean;
379+
isNullable: boolean;
380+
};
381+
382+
export type PropsForField<E extends Entity> = {
383+
[K in EntityProps<E>]: PropsFieldItem;
384+
};
385+
386+
export const getPropsFromDb = <E extends Entity>(
387+
repository: Repository<E>
388+
): PropsForField<E> => {
389+
return repository.metadata.columns.reduce((acum, i) => {
390+
acum[i.propertyName as EntityProps<E>] = {
391+
type: i.type,
392+
isArray: i.isArray,
393+
isNullable: i.isNullable,
394+
};
395+
return acum;
396+
}, {} as PropsForField<E>);
397+
};

libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getTypePrimaryColumn,
1616
getTypeForAllProps,
1717
FieldWithType,
18+
getPropsFromDb,
1819
} from '../orm';
1920
import { Entity } from '../../types';
2021

@@ -193,6 +194,7 @@ export const zodInputPostSchema = <E extends Entity>(
193194
const relationArrayProps = getRelationTypeArray(repository);
194195
const relationPopsName = getRelationTypeName(repository);
195196
const primaryColumnType = getRelationTypePrimaryColumn(repository);
197+
const primaryType = getTypePrimaryColumn(repository);
196198
const fieldWithType = ObjectTyped.entries(getFieldWithType(repository))
197199
.filter(
198200
([key]) => key !== repository.metadata.primaryColumns[0].propertyName
@@ -205,9 +207,13 @@ export const zodInputPostSchema = <E extends Entity>(
205207
{} as FieldWithType<E>
206208
);
207209
const typeName = camelToKebab(getEntityName(repository.target));
210+
211+
const propsDb = getPropsFromDb(repository);
212+
208213
const postShape: PostShape<E> = {
214+
id: zodIdSchema(primaryType).optional(),
209215
type: zodTypeSchema(typeName),
210-
attributes: zodAttributesSchema(fieldWithType),
216+
attributes: zodAttributesSchema(fieldWithType, propsDb),
211217
relationships: zodRelationshipsSchema(
212218
relationArrayProps,
213219
relationPopsName,
@@ -242,11 +248,12 @@ export const zodInputPatchSchema = <E extends Entity>(
242248
{} as FieldWithType<E>
243249
);
244250
const typeName = camelToKebab(getEntityName(repository.target));
251+
const propsDb = getPropsFromDb(repository);
245252

246253
const patchShapeDefault: PatchShapeDefault<E> = {
247254
id: zodIdSchema(primaryType),
248255
type: zodTypeSchema(typeName),
249-
attributes: zodAttributesSchema(fieldWithType),
256+
attributes: zodAttributesSchema(fieldWithType, propsDb),
250257
relationships: zodPatchRelationshipsSchema(
251258
relationArrayProps,
252259
relationPopsName,
@@ -257,7 +264,7 @@ export const zodInputPatchSchema = <E extends Entity>(
257264
const patchShape: PatchShape<E> = {
258265
id: zodIdSchema(primaryType),
259266
type: zodTypeSchema(typeName),
260-
attributes: zodAttributesSchema(fieldWithType)
267+
attributes: zodAttributesSchema(fieldWithType, propsDb)
261268
.optional()
262269
.default({} as any),
263270
relationships: zodPatchRelationshipsSchema(

libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts

Lines changed: 124 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,62 @@ import {
99
ZodObject,
1010
ZodEffects,
1111
ZodOptional,
12+
ZodNullable,
13+
ZodType,
1214
} from 'zod';
1315

1416
import { Entity } from '../../../types';
15-
import { FieldWithType, TypeField } from '../../orm';
17+
import {
18+
FieldWithType,
19+
PropsFieldItem,
20+
PropsForField,
21+
TypeField,
22+
} from '../../orm';
1623
import { ObjectTyped } from '../../utils';
1724
import { nonEmptyObject } from '../zod-utils';
1825

26+
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
27+
28+
const getZodSchemaForJson = (isNull: boolean) => {
29+
const tmpSchema = isNull ? literalSchema.nullable() : literalSchema;
30+
const jsonSchema: any = z.lazy(() =>
31+
z.union([
32+
tmpSchema,
33+
z.array(jsonSchema.nullable()),
34+
z.record(jsonSchema.nullable()),
35+
])
36+
);
37+
38+
return jsonSchema;
39+
};
40+
41+
type Literal = ReturnType<typeof getZodSchemaForJson>;
42+
43+
type Json = Literal | { [key: string]: Json } | Json[];
44+
45+
type ZodTypeForArray =
46+
| ZodString
47+
| ZodDate
48+
| ZodEffects<ZodNumber, number, unknown>
49+
| ZodBoolean;
50+
type ZodArrayType =
51+
| ZodArray<ZodTypeForArray, 'many'>
52+
| ZodNullable<ZodArray<ZodTypeForArray, 'many'>>;
53+
1954
type TypeMapToZod = {
20-
[TypeField.array]: ZodOptional<ZodArray<ZodString, 'many'>>;
21-
[TypeField.date]: ZodOptional<ZodDate>;
22-
[TypeField.number]: ZodOptional<ZodNumber>;
23-
[TypeField.boolean]: ZodOptional<ZodBoolean>;
24-
[TypeField.string]: ZodOptional<ZodString | ZodEnum<[string, ...string[]]>>;
55+
[TypeField.array]: ZodOptional<ZodArrayType>;
56+
[TypeField.date]: ZodOptional<ZodDate | ZodNullable<ZodDate>>;
57+
[TypeField.number]: ZodOptional<
58+
| ZodEffects<ZodNumber, number, unknown>
59+
| ZodNullable<ZodEffects<ZodNumber, number, unknown>>
60+
>;
61+
[TypeField.boolean]: ZodOptional<ZodBoolean | ZodNullable<ZodBoolean>>;
62+
[TypeField.string]: ZodOptional<
63+
| ZodString
64+
| ZodEnum<[string, ...string[]]>
65+
| ZodNullable<ZodString | ZodEnum<[string, ...string[]]>>
66+
>;
67+
[TypeField.object]: ZodType<Json> | ZodNullable<ZodType<Json>>;
2568
};
2669

2770
type ZodShapeAttributes<E extends Entity> = Omit<
@@ -35,28 +78,92 @@ export type ZodAttributesSchema<E extends Entity> = ZodEffects<
3578
ZodObject<ZodShapeAttributes<E>, 'strict'>
3679
>;
3780

81+
function getZodSchemaForArray(props: PropsFieldItem): ZodTypeForArray {
82+
if (!props) return z.string();
83+
let zodSchema: ZodTypeForArray;
84+
switch (props.type) {
85+
case 'number':
86+
case 'real':
87+
case 'integer':
88+
case 'bigint':
89+
case 'double':
90+
case 'numeric':
91+
case Number:
92+
zodSchema = z.preprocess((x) => Number(x), z.number());
93+
break;
94+
case 'date':
95+
case Date:
96+
zodSchema = z.coerce.date();
97+
break;
98+
case 'boolean':
99+
case Boolean:
100+
zodSchema = z.boolean();
101+
break;
102+
default:
103+
zodSchema = z.string();
104+
}
105+
106+
return zodSchema;
107+
}
108+
38109
export const zodAttributesSchema = <E extends Entity>(
39-
fieldWithType: FieldWithType<E>
110+
fieldWithType: FieldWithType<E>,
111+
propsDb: PropsForField<E>
40112
): ZodAttributesSchema<E> => {
41113
const shape = ObjectTyped.entries(fieldWithType).reduce(
42114
(acum, [props, type]: [keyof FieldWithType<E>, TypeField]) => {
43115
let zodShema: TypeMapToZod[typeof type];
116+
const propsDbType = propsDb[props];
44117
switch (type) {
45-
case TypeField.array:
46-
zodShema = z.string().array().optional();
118+
case TypeField.array: {
119+
const tmpSchema = getZodSchemaForArray(propsDbType).array();
120+
zodShema = (
121+
propsDbType && propsDbType.isNullable
122+
? tmpSchema.nullable()
123+
: tmpSchema
124+
).optional();
125+
break;
126+
}
127+
case TypeField.date: {
128+
const tmpSchema = z.coerce.date();
129+
zodShema = (
130+
propsDbType && propsDbType.isNullable
131+
? tmpSchema.nullable()
132+
: tmpSchema
133+
).optional();
47134
break;
48-
case TypeField.date:
49-
zodShema = z.coerce.date().optional();
135+
}
136+
case TypeField.number: {
137+
const tmpSchema = z.preprocess((x) => Number(x), z.number());
138+
zodShema = (
139+
propsDbType && propsDbType.isNullable
140+
? tmpSchema.nullable()
141+
: tmpSchema
142+
).optional();
50143
break;
51-
case TypeField.number:
52-
zodShema = z.number().optional();
144+
}
145+
case TypeField.boolean: {
146+
const tmpSchema = z.boolean();
147+
zodShema = (
148+
propsDbType && propsDbType.isNullable
149+
? tmpSchema.nullable()
150+
: tmpSchema
151+
).optional();
53152
break;
54-
case TypeField.boolean:
55-
zodShema = z.boolean().optional();
153+
}
154+
case TypeField.object: {
155+
zodShema = getZodSchemaForJson(propsDbType.isNullable).optional();
56156
break;
57-
case TypeField.string:
58-
zodShema = z.string().optional();
157+
}
158+
case TypeField.string: {
159+
const tmpSchema = z.string();
160+
zodShema = (
161+
propsDbType && propsDbType.isNullable
162+
? tmpSchema.nullable()
163+
: tmpSchema
164+
).optional();
59165
break;
166+
}
60167
}
61168

62169
return {

0 commit comments

Comments
 (0)