From 0217aca0bb02663ad085bfd5370c7481a3b0641a Mon Sep 17 00:00:00 2001 From: Steven Vergenz <1882376+stevenvergenz@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:24:32 -0700 Subject: [PATCH] Correctly validate optional enums --- prisma/schema.prisma | 1 + src/generator/properties.ts | 8 +- src/tests/generator.test.ts | 158 +++++++++++++++++++++++++++++++++++- 3 files changed, 164 insertions(+), 3 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4cf54e29..8f5d7161 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model User { successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id]) predecessor User? @relation("BlogOwnerHistory") role Role @default(USER) + secondRole Role? posts Post[] keywords String[] biography Json diff --git a/src/generator/properties.ts b/src/generator/properties.ts index f46af182..b743b6d2 100644 --- a/src/generator/properties.ts +++ b/src/generator/properties.ts @@ -145,13 +145,17 @@ function isSingleReference(field: DMMF.Field) { } function getEnumListByDMMFType(modelMetaData: ModelMetaData) { - return (field: DMMF.Field): string[] | undefined => { + return (field: DMMF.Field): (string | null)[] | undefined => { const enumItem = modelMetaData.enums.find( ({ name }) => name === field.type, ) if (!enumItem) return undefined - return enumItem.values.map((item) => item.name) + const items: (string | null)[] = enumItem.values.map((item) => item.name) + if (!field.isRequired) { + items.push(null) + } + return items } } diff --git a/src/tests/generator.test.ts b/src/tests/generator.test.ts index d45d61e4..39856c1c 100644 --- a/src/tests/generator.test.ts +++ b/src/tests/generator.test.ts @@ -25,6 +25,7 @@ const datamodelPostGresQL = /* Prisma */ ` successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id]) predecessor User? @relation("BlogOwnerHistory") role Role @default(USER) + secondRole Role? posts Post[] keywords String[] biography Json @@ -135,6 +136,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -235,6 +240,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -334,6 +343,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -415,6 +428,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, weight: { default: 333.33, type: ['number', 'null'], @@ -496,6 +513,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successorId: { default: 123, type: ['integer', 'null'], @@ -592,6 +613,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -709,6 +734,11 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + originalType: 'Role', + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -812,6 +842,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + anyOf: [{ type: 'string' }, { type: 'null' }], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -907,6 +941,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: '#/definitions/User' }, @@ -1011,6 +1049,10 @@ describe('JSON Schema Generator', () => { enum: ['USER', 'ADMIN'], type: 'string', }, + secondRole: { + type: ['string', 'null'], + enum: ['USER', 'ADMIN', null], + }, successor: { anyOf: [ { $ref: 'schemaId#/definitions/User' }, @@ -1038,7 +1080,120 @@ describe('JSON Schema Generator', () => { }) // eslint-disable-next-line jest/expect-expect - it('generated schema validates against input', async () => { + it('generated schema validates against input 1', async () => { + const dmmf = await getDMMF({ datamodel: datamodelPostGresQL }) + const jsonSchema = transformDMMF(dmmf, { + persistOriginalType: 'true', + }) + const ajv = new Ajv({ + allowUnionTypes: true, + }).addKeyword('originalType') + + addFormats(ajv) + + const validate = ajv.compile(jsonSchema) + const valid = validate({ + post: { + id: 0, + user: { + id: 100, + }, + }, + user: { + id: 10, + createdAt: '1997-07-16T19:20:30.45+01:00', + email: 'jan@scharnow.city', + biography: { + bornIn: 'Scharnow', + }, + number: 34534535435353, + bytes: 'SGVsbG8gd29ybGQ=', + favouriteDecimal: 22.32, + is18: true, + keywords: ['prisma2', 'json-schema', 'generator'], + name: null, + posts: [ + { + id: 4, + }, + { + id: 20, + }, + ], + predecessor: { + id: 10, + email: 'horst@wassermann.de', + }, + successor: null, + role: 'USER', + weight: 10.12, + }, + }) + + if (!valid) { + throw new Error(ajv.errorsText(validate.errors)) + } + }) + + // eslint-disable-next-line jest/expect-expect + it('generated schema validates against input 2', async () => { + const dmmf = await getDMMF({ datamodel: datamodelPostGresQL }) + const jsonSchema = transformDMMF(dmmf, { + persistOriginalType: 'true', + }) + const ajv = new Ajv({ + allowUnionTypes: true, + }).addKeyword('originalType') + + addFormats(ajv) + + const validate = ajv.compile(jsonSchema) + const valid = validate({ + post: { + id: 0, + user: { + id: 100, + }, + }, + user: { + id: 10, + createdAt: '1997-07-16T19:20:30.45+01:00', + email: 'jan@scharnow.city', + biography: { + bornIn: 'Scharnow', + }, + number: 34534535435353, + bytes: 'SGVsbG8gd29ybGQ=', + favouriteDecimal: 22.32, + is18: true, + keywords: ['prisma2', 'json-schema', 'generator'], + name: null, + posts: [ + { + id: 4, + }, + { + id: 20, + }, + ], + predecessor: { + id: 10, + email: 'horst@wassermann.de', + }, + successor: null, + role: 'USER', + secondRole: null, + weight: 10.12, + }, + }) + + if (!valid) { + throw new Error(ajv.errorsText(validate.errors)) + } + }) + + // eslint-disable-next-line jest/expect-expect + it('generated schema validates against input 3', async () => { const dmmf = await getDMMF({ datamodel: datamodelPostGresQL }) const jsonSchema = transformDMMF(dmmf, { persistOriginalType: 'true', @@ -1084,6 +1239,7 @@ describe('JSON Schema Generator', () => { }, successor: null, role: 'USER', + secondRole: 'ADMIN', weight: 10.12, }, })