Skip to content

Commit

Permalink
implemented support for extendedWhereUniqueInput preview feature #91
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishoermann committed Mar 12, 2023
1 parent a7bc182 commit f7fd4ac
Show file tree
Hide file tree
Showing 14 changed files with 431 additions and 160 deletions.
33 changes: 24 additions & 9 deletions Readme.md
Expand Up @@ -16,6 +16,10 @@ Since I'm maintaining the generator in my spare time consider buying me a coffee

Be aware that some generator options have been removed, a few new have been added, the behaviour of custom imports has changed and ts-morph is no longer needed to generate files in v2.0.0.

## Known issues

> Since `zod version 3.21.2` some schemas throw a typescript error. Please use `zod version 3.21.1` until this issue is resolved.
## Table of contents

- [About this project](#about-this-project)
Expand Down Expand Up @@ -591,27 +595,38 @@ The above model would generate the following schema:
// DECIMAL HELPERS
//------------------------------------------------------

export const DecimalJSLikeSchema = z.object({
export const DecimalJSLikeSchema: z.ZodType<Prisma.DecimalJsLike> = z.object({
d: z.array(z.number()),
e: z.number(),
s: z.number(),
toFixed: z.function().args().returns(z.string()),
});

export type DecimalJSLike = z.infer<typeof DecimalJSLikeSchema>;
export const DecimalJSLikeListSchema: z.ZodType<Prisma.DecimalJsLike[]> = z
.object({
d: z.array(z.number()),
e: z.number(),
s: z.number(),
toFixed: z.function().args().returns(z.string()),
})
.array();

export const DECIMAL_STRING_REGEX = /^[0-9.,e+-bxffo_cp]+$|Infinity|NaN/;

export const isValidDecimalInput = (
v?: null | string | number | DecimalJsLike,
) => {
if (!v) return false;
v?: null | string | number | Prisma.DecimalJsLike,
): v is string | number | Prisma.DecimalJsLike => {
if (v === undefined || v === null) return false;
return (
(typeof v === 'object' && 'd' in v && 'e' in v && 's' in v) ||
(typeof v === 'object' &&
'd' in v &&
'e' in v &&
's' in v &&
'toFixed' in v) ||
(typeof v === 'string' && DECIMAL_STRING_REGEX.test(v)) ||
typeof v === 'number'
);
};

// SCHEMA
//------------------------------------------------------

Expand All @@ -620,8 +635,8 @@ export const MyModelSchema = z.object({
decimal: z
.union([z.number(), z.string(), DecimalJSLikeSchema])
.refine((v) => isValidDecimalInput(v), {
message: 'Field "decimal" must be a Decimal',
path: ['Models', 'DecimalModel'],
message:
"Field 'decimal' must be a Decimal. Location: ['Models', 'DecimalModel']",
}),
});
```
Expand Down
33 changes: 24 additions & 9 deletions packages/generator/Readme.md
Expand Up @@ -16,6 +16,10 @@ Since I'm maintaining the generator in my spare time consider buying me a coffee

Be aware that some generator options have been removed, a few new have been added, the behaviour of custom imports has changed and ts-morph is no longer needed to generate files in v2.0.0.

## Known issues

> Since `zod version 3.21.2` some schemas throw a typescript error. Please use `zod version 3.21.1` until this issue is resolved.
## Table of contents

- [About this project](#about-this-project)
Expand Down Expand Up @@ -591,27 +595,38 @@ The above model would generate the following schema:
// DECIMAL HELPERS
//------------------------------------------------------

export const DecimalJSLikeSchema = z.object({
export const DecimalJSLikeSchema: z.ZodType<Prisma.DecimalJsLike> = z.object({
d: z.array(z.number()),
e: z.number(),
s: z.number(),
toFixed: z.function().args().returns(z.string()),
});

export type DecimalJSLike = z.infer<typeof DecimalJSLikeSchema>;
export const DecimalJSLikeListSchema: z.ZodType<Prisma.DecimalJsLike[]> = z
.object({
d: z.array(z.number()),
e: z.number(),
s: z.number(),
toFixed: z.function().args().returns(z.string()),
})
.array();

export const DECIMAL_STRING_REGEX = /^[0-9.,e+-bxffo_cp]+$|Infinity|NaN/;

export const isValidDecimalInput = (
v?: null | string | number | DecimalJsLike,
) => {
if (!v) return false;
v?: null | string | number | Prisma.DecimalJsLike,
): v is string | number | Prisma.DecimalJsLike => {
if (v === undefined || v === null) return false;
return (
(typeof v === 'object' && 'd' in v && 'e' in v && 's' in v) ||
(typeof v === 'object' &&
'd' in v &&
'e' in v &&
's' in v &&
'toFixed' in v) ||
(typeof v === 'string' && DECIMAL_STRING_REGEX.test(v)) ||
typeof v === 'number'
);
};

// SCHEMA
//------------------------------------------------------

Expand All @@ -620,8 +635,8 @@ export const MyModelSchema = z.object({
decimal: z
.union([z.number(), z.string(), DecimalJSLikeSchema])
.refine((v) => isValidDecimalInput(v), {
message: 'Field "decimal" must be a Decimal',
path: ['Models', 'DecimalModel'],
message:
"Field 'decimal' must be a Decimal. Location: ['Models', 'DecimalModel']",
}),
});
```
Expand Down
4 changes: 2 additions & 2 deletions packages/generator/package.json
@@ -1,6 +1,6 @@
{
"name": "zod-prisma-types",
"version": "2.4.1",
"version": "2.5.0",
"description": "Generates zod schemas from Prisma models with advanced validation",
"author": "Chris Hörmann",
"license": "MIT",
Expand Down Expand Up @@ -49,6 +49,6 @@
"@prisma/generator-helper": "^4.11.0",
"code-block-writer": "^11.0.3",
"lodash": "^4.17.21",
"zod": "^3.21.4"
"zod": "^3.21.1"
}
}
78 changes: 68 additions & 10 deletions packages/generator/src/classes/extendedDMMFInputType.ts
Expand Up @@ -36,7 +36,9 @@ export class ExtendedDMMFInputType
readonly isDecimalField: boolean;
readonly omitFields: string[] = [];
readonly imports: Set<string>;
/** @deprecated */
readonly isWhereUniqueInput?: boolean;
readonly extendedWhereUniqueFields?: ExtendedDMMFSchemaArg[][];

constructor(
readonly generatorConfig: GeneratorConfig,
Expand All @@ -56,10 +58,9 @@ export class ExtendedDMMFInputType
this.isDecimalField = this._setIsDecimalField();
this.omitFields = this._setOmitFields();
this.imports = this._setImports();

// if (this.name === 'ProfileWhereUniqueInput') {
// console.log(type);
// }
this.extendedWhereUniqueFields = this._setExtendedWhereUniqueFields(
type.fields,
);
}

/**
Expand All @@ -80,12 +81,6 @@ export class ExtendedDMMFInputType
(modelField) => modelField.name === field.name,
);

// const hasConstraints = this.constraints.fields?.includes(field.name);

// if (this.name === 'ProfileWhereUniqueInput') {
// console.log({ fieldname: field.name, hasConstraints });
// }

// validators and omitField should only be written for create and update types.
// this prevents validation in e.g. search queries in "where inputs",
// where strings like email addresses can be incomplete.
Expand Down Expand Up @@ -179,6 +174,69 @@ export class ExtendedDMMFInputType
return new Set(fieldImports);
}

private _getExtendedWhereUniqueFieldCombinations(
arr: DMMF.SchemaArg[],
): DMMF.SchemaArg[][] {
const result: DMMF.SchemaArg[][] = [];

function combine(start: number, soFar: DMMF.SchemaArg[]) {
if (soFar.length === arr.length) {
result.push(soFar.slice());
return;
}

// include current element
combine(start + 1, [...soFar, { ...arr[start], isRequired: true }]);

// exclude current element
combine(start + 1, [...soFar, { ...arr[start], isRequired: false }]);
}

combine(0, []);
return result;
}

private _setExtendedWhereUniqueFields(fields: DMMF.SchemaArg[]) {
if (!this.constraints.fields || !this.name.includes('WhereUniqueInput')) {
return undefined;
}

// get the DMMF.SchemaArg for all fields that are part of the constraints
// that are marked for the extended where unique input
const extendedWhereUniqueFields = this.constraints.fields
.map((fieldName) => {
return fields.find((field) => field.name === fieldName);
})
.filter((field): field is DMMF.SchemaArg => field !== undefined);

// get all combinations of bool values on isRequired fields
// for the provided set of fields
const combinations = this._getExtendedWhereUniqueFieldCombinations(
extendedWhereUniqueFields,
);

// filter out combinations where isRequired is False because
// these cominations are included in the all optional type that is
// later cominened with the generated union type.
const filteredCombinations = combinations.filter(
(combination) => !combination.every((field) => !field.isRequired),
);

// filter out all fields that are not required
// since they are added via the all optional type
const extendedFilterdCombinations = filteredCombinations.map(
(combination) => {
return combination.filter((field) => field.isRequired);
},
);

// create an ExtendedDMMFSchemaArg for each combination field
// so the writer functions can be used as is
return extendedFilterdCombinations.map((combination) => {
return this._setFields(combination);
});
}

hasOmitFields() {
return this.omitFields.length > 0;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/generator/src/classes/extendedDMMFSchemaField.ts
Expand Up @@ -425,7 +425,9 @@ export class ExtendedDMMFSchemaField
private _getCustomArgsMultipleTypes(arg: ExtendedDMMFSchemaArg) {
return arg.inputTypes
.map((inputType) => {
return `z.infer<typeof ${inputType.type}Schema>`;
return `z.infer<typeof ${inputType.type}Schema>${
inputType.isList ? '[]' : ''
}`;
})
.join(' | ');
}
Expand Down

0 comments on commit f7fd4ac

Please sign in to comment.