From 1f1ef9c39a2db9f86d83788cfc0eae2d9f55cda6 Mon Sep 17 00:00:00 2001 From: Roman_Vasilev Date: Wed, 14 Dec 2022 21:42:54 +0400 Subject: [PATCH] fix: Combine scalar filters with `fieldReference` close: #148 --- src/generate.ts | 8 +- src/handlers/args-type.ts | 6 +- src/handlers/combine-scalar-filters.ts | 52 +++++- src/handlers/generate-files.ts | 9 +- src/handlers/input-type.ts | 1 + src/helpers/file-type-by-location.ts | 9 +- src/helpers/get-graphql-import.ts | 15 +- src/helpers/get-property-type.ts | 27 ++- src/test/combine-scalar-filters.spec.ts | 235 ++++++++++++++++++++++++ src/test/generate.spec.ts | 226 ----------------------- src/types.ts | 1 + 11 files changed, 335 insertions(+), 254 deletions(-) create mode 100644 src/test/combine-scalar-filters.spec.ts diff --git a/src/generate.ts b/src/generate.ts index 9f8272e7..de311f69 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -97,12 +97,10 @@ export async function generate( outputFilePattern: config.outputFilePattern, eventEmitter, }); - const { - datamodel, - schema: { inputObjectTypes, outputObjectTypes, enumTypes }, - } = JSON.parse(JSON.stringify(dmmf)) as DMMF.Document; + const { datamodel, schema } = JSON.parse(JSON.stringify(dmmf)) as DMMF.Document; const removeTypes = new Set(); const eventArguments: EventArguments = { + schema, models, config, modelNames, @@ -135,6 +133,8 @@ export async function generate( await eventEmitter.emit('Model', model, eventArguments); } + const { inputObjectTypes, outputObjectTypes, enumTypes } = schema; + await eventEmitter.emit('PostBegin', eventArguments); for (const enumType of enumTypes.prisma.concat(enumTypes.model || [])) { diff --git a/src/handlers/args-type.ts b/src/handlers/args-type.ts index 89a93b41..527646ce 100644 --- a/src/handlers/args-type.ts +++ b/src/handlers/args-type.ts @@ -14,12 +14,14 @@ export function argsType(field: SchemaField, args: EventArguments) { const modelName = getModelName(className) || ''; switch (className) { - case `Aggregate${modelName}Args`: + case `Aggregate${modelName}Args`: { className = `${modelName}AggregateArgs`; break; - case `GroupBy${modelName}Args`: + } + case `GroupBy${modelName}Args`: { className = `${modelName}GroupByArgs`; break; + } } const inputType: InputType = { diff --git a/src/handlers/combine-scalar-filters.ts b/src/handlers/combine-scalar-filters.ts index 4bb0753d..3d29b0cc 100644 --- a/src/handlers/combine-scalar-filters.ts +++ b/src/handlers/combine-scalar-filters.ts @@ -1,4 +1,5 @@ import AwaitEventEmitter from 'await-event-emitter'; +import { cloneDeep, keyBy, remove } from 'lodash'; import { DMMF, EventArguments, InputType } from '../types'; @@ -8,6 +9,7 @@ import { DMMF, EventArguments, InputType } from '../types'; export function combineScalarFilters(eventEmitter: AwaitEventEmitter) { eventEmitter.on('BeforeInputType', beforeInputType); eventEmitter.on('BeforeGenerateField', beforeGenerateField); + eventEmitter.on('PostBegin', postBegin); } function beforeInputType( @@ -17,10 +19,11 @@ function beforeInputType( classDecoratorName: string; }, ) { - const { inputType } = args; + const { inputType, removeTypes } = args; if (isContainBogus(inputType.name) && isScalarFilter(inputType)) { - inputType.name = replaceBogus(String(inputType.name)); + removeTypes.add(inputType.name); + inputType.name = replaceBogus(inputType.name); } } @@ -61,3 +64,48 @@ function isScalarFilter(inputType: InputType) { } return result; } + +function postBegin(args: EventArguments) { + const { schema } = args; + const inputTypes = schema.inputObjectTypes.prisma; + const enumTypes = schema.enumTypes.model || []; + const types = ['Bool', 'Int', 'String', 'DateTime', 'Decimal', 'Float', 'Json']; + + for (const enumType of enumTypes) { + const { name } = enumType; + types.push(`Enum${name}`); + } + + const inputTypeByName = keyBy(inputTypes, inputType => inputType.name); + const replaceBogusFilters = (filterName: string, filterNameCandidates: string[]) => { + for (const filterNameCandidate of filterNameCandidates) { + const candidate = inputTypeByName[filterNameCandidate]; + if (candidate as InputType | undefined) { + const inputType = cloneDeep({ ...candidate, name: filterName }); + inputTypes.push(inputType); + inputTypeByName[filterName] = inputType; + break; + } + } + }; + + for (const type of types) { + // Scalar filters + replaceBogusFilters(`${type}Filter`, [ + `${type}NullableFilter`, + `Nested${type}NullableFilter`, + ]); + + replaceBogusFilters(`${type}WithAggregatesFilter`, [ + `${type}NullableWithAggregatesFilter`, + `Nested${type}NullableWithAggregatesFilter`, + ]); + + replaceBogusFilters(`${type}ListFilter`, [ + `${type}NullableListFilter`, + `Nested${type}NullableListFilter`, + ]); + } + + remove(inputTypes, inputType => isContainBogus(inputType.name)); +} diff --git a/src/handlers/generate-files.ts b/src/handlers/generate-files.ts index 7abf02fe..688cacc6 100644 --- a/src/handlers/generate-files.ts +++ b/src/handlers/generate-files.ts @@ -63,7 +63,7 @@ export async function generateFiles(args: EventArguments) { continue; } switch (statement.kind) { - case StructureKind.ImportDeclaration: + case StructureKind.ImportDeclaration: { if ( statement.moduleSpecifier.startsWith('./') || statement.moduleSpecifier.startsWith('..') @@ -89,12 +89,15 @@ export async function generateFiles(args: EventArguments) { }); } break; - case StructureKind.Enum: + } + case StructureKind.Enum: { enums.unshift(statement); break; - case StructureKind.Class: + } + case StructureKind.Class: { classes.push(statement); break; + } } } sourceFile.set({ diff --git a/src/handlers/input-type.ts b/src/handlers/input-type.ts index 437990dd..2da4a344 100644 --- a/src/handlers/input-type.ts +++ b/src/handlers/input-type.ts @@ -75,6 +75,7 @@ export function inputType( for (const field of inputType.fields) { field.inputTypes = field.inputTypes.filter(t => !removeTypes.has(String(t.type))); + eventEmitter.emitSync('BeforeGenerateField', field, args); const { inputTypes, isRequired, name } = field; diff --git a/src/helpers/file-type-by-location.ts b/src/helpers/file-type-by-location.ts index 3f952d14..23210f46 100644 --- a/src/helpers/file-type-by-location.ts +++ b/src/helpers/file-type-by-location.ts @@ -2,12 +2,15 @@ import { FieldLocation } from '../types'; export function fileTypeByLocation(fieldLocation: FieldLocation) { switch (fieldLocation) { - case 'inputObjectTypes': + case 'inputObjectTypes': { return 'input'; - case 'outputObjectTypes': + } + case 'outputObjectTypes': { return 'output'; - case 'enumTypes': + } + case 'enumTypes': { return 'enum'; + } } return 'object'; } diff --git a/src/helpers/get-graphql-import.ts b/src/helpers/get-graphql-import.ts index fa3d8b11..f7a81342 100644 --- a/src/helpers/get-graphql-import.ts +++ b/src/helpers/get-graphql-import.ts @@ -37,20 +37,25 @@ export function getGraphqlImport(args: { switch (typeName) { case 'Float': - case 'Int': + case 'Int': { return { name: typeName, specifier: '@nestjs/graphql' }; - case 'DateTime': + } + case 'DateTime': { return { name: 'Date', specifier: undefined }; + } case 'true': - case 'Boolean': + case 'Boolean': { return { name: 'Boolean', specifier: undefined }; - case 'Decimal': + } + case 'Decimal': { return { name: 'GraphQLDecimal', specifier: 'prisma-graphql-type-decimal', }; - case 'Json': + } + case 'Json': { return { name: 'GraphQLJSON', specifier: 'graphql-type-json' }; + } } return { name: 'String', specifier: undefined }; diff --git a/src/helpers/get-property-type.ts b/src/helpers/get-property-type.ts index 84f7a5ae..9a507e33 100644 --- a/src/helpers/get-property-type.ts +++ b/src/helpers/get-property-type.ts @@ -10,24 +10,33 @@ export function getPropertyType(args: { const { type, location } = args; switch (type) { case 'Float': - case 'Int': + case 'Int': { return ['number']; - case 'String': + } + case 'String': { return ['string']; - case 'Boolean': + } + case 'Boolean': { return ['boolean']; - case 'DateTime': + } + case 'DateTime': { return ['Date', 'string']; - case 'Decimal': + } + case 'Decimal': { return ['Decimal']; - case 'Json': + } + case 'Json': { return ['any']; - case 'Null': + } + case 'Null': { return ['null']; - case 'Bytes': + } + case 'Bytes': { return ['Buffer']; - case 'BigInt': + } + case 'BigInt': { return ['bigint', 'number']; + } } if (['inputObjectTypes', 'outputObjectTypes'].includes(location)) { return [type]; diff --git a/src/test/combine-scalar-filters.spec.ts b/src/test/combine-scalar-filters.spec.ts new file mode 100644 index 00000000..4d95eaf7 --- /dev/null +++ b/src/test/combine-scalar-filters.spec.ts @@ -0,0 +1,235 @@ +import expect from 'expect'; +import { Project, SourceFile } from 'ts-morph'; + +import { testSourceFile } from './helpers'; +import { testGenerate } from './test-generate'; + +let project: Project; +let sourceFiles: SourceFile[]; + +describe('combine scalar filters', () => { + before(async () => { + ({ project, sourceFiles } = await testGenerate({ + schema: ` + model User { + id String @id + bio String? + count Int? + rating Float? + born DateTime? + humanoid Boolean? + money Decimal? + data Json? + role Role? + } + enum Role { + USER + ADMIN + } + `, + options: [ + `outputFilePattern = "{name}.{type}.ts"`, + `combineScalarFilters = true`, + ], + })); + }); + + it('files should not contain nested and nullable', () => { + const filePaths = sourceFiles + .map(s => s.getFilePath().slice(1)) + .filter( + p => + !( + p.endsWith('field-update-operations.input.ts') || + p.endsWith('json-null-value-input.enum.ts') + ), + ); + + for (const filePath of filePaths) { + expect(filePath).not.toContain('nested'); + expect(filePath).not.toContain('nullable'); + } + }); + + it('source file should not reference bogus type', () => { + for (const sourceFile of sourceFiles) { + const types = sourceFile + .getClass(() => true) + ?.getProperties() + .map(p => String(p.getStructure().type)) + .filter((t: string) => t.endsWith('Filter') || t.endsWith('Filter>')); + if (types) { + for (const type of types) { + expect(type).not.toContain('Nested'); + expect(type).not.toContain('Nullable'); + } + } + } + }); + + it('user-where.input count', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'count', + }); + expect(s.property?.type).toBe('IntFilter'); + }); + + it('user-where.input bio', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'bio', + }); + expect(s.property?.type).toBe('StringFilter'); + }); + + it('user-where.input money', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'money', + }); + expect(s.property?.type).toBe('DecimalFilter'); + }); + + it('user-where.input rating', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'rating', + }); + expect(s.property?.type).toBe('FloatFilter'); + }); + + it('user-where.input born', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'born', + }); + expect(s.property?.type).toBe('DateTimeFilter'); + }); + + it('user-where.input humanoid', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'humanoid', + }); + expect(s.property?.type).toBe('BoolFilter'); + }); + + it('user-where.input role', () => { + const s = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'role', + }); + expect(s.property?.type).toBe('EnumRoleFilter'); + }); + + it('user scalar where with aggregates id', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'id', + }); + expect(s.property?.type).toBe('StringWithAggregatesFilter'); + }); + + it('user scalar where with aggregates bio', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'bio', + }); + expect(s.property?.type).toBe('StringWithAggregatesFilter'); + }); + + it('user scalar where with aggregates count', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'count', + }); + expect(s.property?.type).toBe('IntWithAggregatesFilter'); + }); + + it('user scalar where with aggregates rating', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'rating', + }); + expect(s.property?.type).toBe('FloatWithAggregatesFilter'); + }); + + it('user scalar where with aggregates born', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'born', + }); + expect(s.property?.type).toBe('DateTimeWithAggregatesFilter'); + }); + + it('user scalar where with aggregates humanoid', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'humanoid', + }); + expect(s.property?.type).toBe('BoolWithAggregatesFilter'); + }); + + it('user scalar where with aggregates money', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'money', + }); + expect(s.property?.type).toBe('DecimalWithAggregatesFilter'); + }); + + it('user scalar where with aggregates data', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'data', + }); + expect(s.property?.type).toBe('JsonWithAggregatesFilter'); + }); + + it('user scalar where with aggregates role', () => { + const s = testSourceFile({ + project, + file: 'user-scalar-where-with-aggregates.input.ts', + property: 'role', + }); + expect(s.property?.type).toBe('EnumRoleWithAggregatesFilter'); + }); +}); + +describe('combine scalar filters on array', () => { + before(async () => { + ({ project, sourceFiles } = await testGenerate({ + schema: ` + model User { + id String @id + str String[] + int Int? + } + `, + options: [ + `outputFilePattern = "{name}.{type}.ts"`, + `combineScalarFilters = true`, + ], + })); + }); + + it('smoke', () => { + 'no errors'; + }); +}); diff --git a/src/test/generate.spec.ts b/src/test/generate.spec.ts index e95be9d4..aa1bfa75 100644 --- a/src/test/generate.spec.ts +++ b/src/test/generate.spec.ts @@ -1237,232 +1237,6 @@ describe('scalar arrays with noAtomicOperations', () => { }); }); -describe('combine scalar filters', () => { - before(async () => { - ({ project, sourceFiles } = await testGenerate({ - schema: ` - model User { - id String @id - bio String? - count Int? - rating Float? - born DateTime? - humanoid Boolean? - money Decimal? - data Json? - role Role? - } - enum Role { - USER - ADMIN - } - `, - options: [ - `outputFilePattern = "{name}.{type}.ts"`, - `combineScalarFilters = true`, - ], - })); - }); - - it('files should not contain nested and nullable', () => { - const filePaths = sourceFiles - .map(s => s.getFilePath().slice(1)) - .filter( - p => - !( - p.endsWith('field-update-operations.input.ts') || - p.endsWith('json-null-value-input.enum.ts') - ), - ); - for (const filePath of filePaths) { - expect(filePath).not.toContain('nested'); - expect(filePath).not.toContain('nullable'); - } - }); - - it('source file should not reference bogus type', () => { - for (const sourceFile of sourceFiles) { - const types = sourceFile - .getClass(() => true) - ?.getProperties() - .map(p => String(p.getStructure().type)) - .filter((t: string) => t.endsWith('Filter') || t.endsWith('Filter>')); - if (types) { - for (const type of types) { - expect(type).not.toContain('Nested'); - expect(type).not.toContain('Nullable'); - } - } - } - }); - - it('user-where.input count', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'count', - }); - expect(s.property?.type).toBe('IntFilter'); - }); - - it('user-where.input bio', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'bio', - }); - expect(s.property?.type).toBe('StringFilter'); - }); - - it('user-where.input money', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'money', - }); - expect(s.property?.type).toBe('DecimalFilter'); - }); - - it('user-where.input rating', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'rating', - }); - expect(s.property?.type).toBe('FloatFilter'); - }); - - it('user-where.input born', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'born', - }); - expect(s.property?.type).toBe('DateTimeFilter'); - }); - - it('user-where.input humanoid', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'humanoid', - }); - expect(s.property?.type).toBe('BoolFilter'); - }); - - it('user-where.input role', () => { - const s = testSourceFile({ - project, - file: 'user-where.input.ts', - property: 'role', - }); - expect(s.property?.type).toBe('EnumRoleFilter'); - }); - - it('user scalar where with aggregates id', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'id', - }); - expect(s.property?.type).toBe('StringWithAggregatesFilter'); - }); - - it('user scalar where with aggregates bio', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'bio', - }); - expect(s.property?.type).toBe('StringWithAggregatesFilter'); - }); - - it('user scalar where with aggregates count', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'count', - }); - expect(s.property?.type).toBe('IntWithAggregatesFilter'); - }); - - it('user scalar where with aggregates rating', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'rating', - }); - expect(s.property?.type).toBe('FloatWithAggregatesFilter'); - }); - - it('user scalar where with aggregates born', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'born', - }); - expect(s.property?.type).toBe('DateTimeWithAggregatesFilter'); - }); - - it('user scalar where with aggregates humanoid', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'humanoid', - }); - expect(s.property?.type).toBe('BoolWithAggregatesFilter'); - }); - - it('user scalar where with aggregates money', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'money', - }); - expect(s.property?.type).toBe('DecimalWithAggregatesFilter'); - }); - - it('user scalar where with aggregates data', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'data', - }); - expect(s.property?.type).toBe('JsonWithAggregatesFilter'); - }); - - it('user scalar where with aggregates role', () => { - const s = testSourceFile({ - project, - file: 'user-scalar-where-with-aggregates.input.ts', - property: 'role', - }); - expect(s.property?.type).toBe('EnumRoleWithAggregatesFilter'); - }); -}); - -describe('combine scalar filters on array', () => { - before(async () => { - ({ project, sourceFiles } = await testGenerate({ - schema: ` - model User { - id String @id - str String[] - int Int? - } - `, - options: [ - `outputFilePattern = "{name}.{type}.ts"`, - `combineScalarFilters = true`, - ], - })); - }); - - it('smoke', () => { - 'no errors'; - }); -}); - it('model with prisma keyword output', async () => { ({ project, sourceFiles } = await testGenerate({ schema: ` diff --git a/src/types.ts b/src/types.ts index 219b9c5f..2f750abe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,7 @@ export type TypeRecord = Partial<{ export type GeneratorConfiguration = ReturnType; export type EventArguments = { + schema: DMMF.Schema; models: Map; modelNames: string[]; modelFields: Map>;