Skip to content

Commit

Permalink
feat(json-api-nestjs): Extend validate for array, json and datetime, …
Browse files Browse the repository at this point in the history
…use nullable if possible
  • Loading branch information
klerick committed Dec 5, 2024
1 parent 9e0f066 commit a8c6f83
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 21 deletions.
34 changes: 33 additions & 1 deletion libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../types';
import { getEntityName, ObjectTyped } from '../utils';
import { guardKeyForPropertyTarget } from './orm-type-asserts';
import { ColumnType } from 'typeorm/driver/types/ColumnTypes';

export enum PropsNameResultField {
field = 'field',
Expand Down Expand Up @@ -72,6 +73,7 @@ export enum TypeField {
number = 'number',
boolean = 'boolean',
string = 'string',
object = 'object',
}

export type TypeForId = Extract<TypeField, TypeField.number | TypeField.string>;
Expand All @@ -85,6 +87,8 @@ export type FieldWithType<E extends Entity> = {
? TypeField.number
: E[K] extends boolean
? TypeField.boolean
: E[K] extends object
? TypeField.object
: TypeField.string;
};

Expand Down Expand Up @@ -187,6 +191,9 @@ export const getFieldWithType = <E extends Entity>(
case Boolean:
typeProps = TypeField.boolean;
break;
case Object:
typeProps = TypeField.object;
break;
default:
typeProps = TypeField.string;
}
Expand Down Expand Up @@ -343,7 +350,9 @@ export const fromRelationTreeToArrayName = <E extends Entity>(
export type AllFieldWithTpe<E extends Entity> = FieldWithType<E> & {
[K in EntityRelation<E>]: E[K] extends (infer U extends Entity)[]
? FieldWithType<U>
: E[K] extends Entity ? FieldWithType<E[K]> : never;
: E[K] extends Entity
? FieldWithType<E[K]>
: never;
};

export const getTypeForAllProps = <E extends Entity>(
Expand All @@ -363,3 +372,26 @@ export const getTypeForAllProps = <E extends Entity>(
...relationField,
};
};

export type PropsFieldItem = {
type: ColumnType;
isArray: boolean;
isNullable: boolean;
};

export type PropsForField<E extends Entity> = {
[K in EntityProps<E>]: PropsFieldItem;
};

export const getPropsFromDb = <E extends Entity>(
repository: Repository<E>
): PropsForField<E> => {
return repository.metadata.columns.reduce((acum, i) => {
acum[i.propertyName as EntityProps<E>] = {
type: i.type,
isArray: i.isArray,
isNullable: i.isNullable,
};
return acum;
}, {} as PropsForField<E>);
};
13 changes: 10 additions & 3 deletions libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getTypePrimaryColumn,
getTypeForAllProps,
FieldWithType,
getPropsFromDb,
} from '../orm';
import { Entity } from '../../types';

Expand Down Expand Up @@ -193,6 +194,7 @@ export const zodInputPostSchema = <E extends Entity>(
const relationArrayProps = getRelationTypeArray(repository);
const relationPopsName = getRelationTypeName(repository);
const primaryColumnType = getRelationTypePrimaryColumn(repository);
const primaryType = getTypePrimaryColumn(repository);
const fieldWithType = ObjectTyped.entries(getFieldWithType(repository))
.filter(
([key]) => key !== repository.metadata.primaryColumns[0].propertyName
Expand All @@ -205,9 +207,13 @@ export const zodInputPostSchema = <E extends Entity>(
{} as FieldWithType<E>
);
const typeName = camelToKebab(getEntityName(repository.target));

const propsDb = getPropsFromDb(repository);

const postShape: PostShape<E> = {
id: zodIdSchema(primaryType).optional(),
type: zodTypeSchema(typeName),
attributes: zodAttributesSchema(fieldWithType),
attributes: zodAttributesSchema(fieldWithType, propsDb),
relationships: zodRelationshipsSchema(
relationArrayProps,
relationPopsName,
Expand Down Expand Up @@ -242,11 +248,12 @@ export const zodInputPatchSchema = <E extends Entity>(
{} as FieldWithType<E>
);
const typeName = camelToKebab(getEntityName(repository.target));
const propsDb = getPropsFromDb(repository);

const patchShapeDefault: PatchShapeDefault<E> = {
id: zodIdSchema(primaryType),
type: zodTypeSchema(typeName),
attributes: zodAttributesSchema(fieldWithType),
attributes: zodAttributesSchema(fieldWithType, propsDb),
relationships: zodPatchRelationshipsSchema(
relationArrayProps,
relationPopsName,
Expand All @@ -257,7 +264,7 @@ export const zodInputPatchSchema = <E extends Entity>(
const patchShape: PatchShape<E> = {
id: zodIdSchema(primaryType),
type: zodTypeSchema(typeName),
attributes: zodAttributesSchema(fieldWithType)
attributes: zodAttributesSchema(fieldWithType, propsDb)
.optional()
.default({} as any),
relationships: zodPatchRelationshipsSchema(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,62 @@ import {
ZodObject,
ZodEffects,
ZodOptional,
ZodNullable,
ZodType,
} from 'zod';

import { Entity } from '../../../types';
import { FieldWithType, TypeField } from '../../orm';
import {
FieldWithType,
PropsFieldItem,
PropsForField,
TypeField,
} from '../../orm';
import { ObjectTyped } from '../../utils';
import { nonEmptyObject } from '../zod-utils';

const literalSchema = z.union([z.string(), z.number(), z.boolean()]);

const getZodSchemaForJson = (isNull: boolean) => {
const tmpSchema = isNull ? literalSchema.nullable() : literalSchema;
const jsonSchema: any = z.lazy(() =>
z.union([
tmpSchema,
z.array(jsonSchema.nullable()),
z.record(jsonSchema.nullable()),
])
);

return jsonSchema;
};

type Literal = ReturnType<typeof getZodSchemaForJson>;

type Json = Literal | { [key: string]: Json } | Json[];

type ZodTypeForArray =
| ZodString
| ZodDate
| ZodEffects<ZodNumber, number, unknown>
| ZodBoolean;
type ZodArrayType =
| ZodArray<ZodTypeForArray, 'many'>
| ZodNullable<ZodArray<ZodTypeForArray, 'many'>>;

type TypeMapToZod = {
[TypeField.array]: ZodOptional<ZodArray<ZodString, 'many'>>;
[TypeField.date]: ZodOptional<ZodDate>;
[TypeField.number]: ZodOptional<ZodNumber>;
[TypeField.boolean]: ZodOptional<ZodBoolean>;
[TypeField.string]: ZodOptional<ZodString | ZodEnum<[string, ...string[]]>>;
[TypeField.array]: ZodOptional<ZodArrayType>;
[TypeField.date]: ZodOptional<ZodDate | ZodNullable<ZodDate>>;
[TypeField.number]: ZodOptional<
| ZodEffects<ZodNumber, number, unknown>
| ZodNullable<ZodEffects<ZodNumber, number, unknown>>
>;
[TypeField.boolean]: ZodOptional<ZodBoolean | ZodNullable<ZodBoolean>>;
[TypeField.string]: ZodOptional<
| ZodString
| ZodEnum<[string, ...string[]]>
| ZodNullable<ZodString | ZodEnum<[string, ...string[]]>>
>;
[TypeField.object]: ZodType<Json> | ZodNullable<ZodType<Json>>;
};

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

function getZodSchemaForArray(props: PropsFieldItem): ZodTypeForArray {
if (!props) return z.string();
let zodSchema: ZodTypeForArray;
switch (props.type) {
case 'number':
case 'real':
case 'integer':
case 'bigint':
case 'double':
case 'numeric':
case Number:
zodSchema = z.preprocess((x) => Number(x), z.number());
break;
case 'date':
case Date:
zodSchema = z.coerce.date();
break;
case 'boolean':
case Boolean:
zodSchema = z.boolean();
break;
default:
zodSchema = z.string();
}

return zodSchema;
}

export const zodAttributesSchema = <E extends Entity>(
fieldWithType: FieldWithType<E>
fieldWithType: FieldWithType<E>,
propsDb: PropsForField<E>
): ZodAttributesSchema<E> => {
const shape = ObjectTyped.entries(fieldWithType).reduce(
(acum, [props, type]: [keyof FieldWithType<E>, TypeField]) => {
let zodShema: TypeMapToZod[typeof type];
const propsDbType = propsDb[props];
switch (type) {
case TypeField.array:
zodShema = z.string().array().optional();
case TypeField.array: {
const tmpSchema = getZodSchemaForArray(propsDbType).array();
zodShema = (
propsDbType && propsDbType.isNullable
? tmpSchema.nullable()
: tmpSchema
).optional();
break;
}
case TypeField.date: {
const tmpSchema = z.coerce.date();
zodShema = (
propsDbType && propsDbType.isNullable
? tmpSchema.nullable()
: tmpSchema
).optional();
break;
case TypeField.date:
zodShema = z.coerce.date().optional();
}
case TypeField.number: {
const tmpSchema = z.preprocess((x) => Number(x), z.number());
zodShema = (
propsDbType && propsDbType.isNullable
? tmpSchema.nullable()
: tmpSchema
).optional();
break;
case TypeField.number:
zodShema = z.number().optional();
}
case TypeField.boolean: {
const tmpSchema = z.boolean();
zodShema = (
propsDbType && propsDbType.isNullable
? tmpSchema.nullable()
: tmpSchema
).optional();
break;
case TypeField.boolean:
zodShema = z.boolean().optional();
}
case TypeField.object: {
zodShema = getZodSchemaForJson(propsDbType.isNullable).optional();
break;
case TypeField.string:
zodShema = z.string().optional();
}
case TypeField.string: {
const tmpSchema = z.string();
zodShema = (
propsDbType && propsDbType.isNullable
? tmpSchema.nullable()
: tmpSchema
).optional();
break;
}
}

return {
Expand Down

0 comments on commit a8c6f83

Please sign in to comment.