diff --git a/src/generate-model/generate-model.spec.ts b/src/generate-model/generate-model.spec.ts index 30f0fcac..291a0cfa 100644 --- a/src/generate-model/generate-model.spec.ts +++ b/src/generate-model/generate-model.spec.ts @@ -2,8 +2,10 @@ import assert from 'assert'; import { expect } from 'chai'; import { Project, QuoteKind, SourceFile } from 'ts-morph'; +import { createConfig } from '../generate'; import { generateModel } from '../generate-model'; import { generatorOptions, stringContains } from '../testing'; +import { GeneratorConfiguration } from '../types'; describe('generate models', () => { let sourceFile: SourceFile; @@ -19,13 +21,20 @@ describe('generate models', () => { manipulationSettings: { quoteKind: QuoteKind.Single }, }); const { + generator, prismaClientDmmf: { datamodel: { models }, }, } = await generatorOptions(schema, options); const [model] = models; sourceFile = project.createSourceFile('_.ts', sourceFileText); - generateModel({ model, sourceFile, projectFilePath: () => '_.ts' }); + const config = createConfig(generator.config); + generateModel({ + model, + sourceFile, + config, + projectFilePath: () => '_.ts', + }); sourceText = sourceFile.getText(); imports = sourceFile.getImportDeclarations().flatMap((d) => d.getNamedImports().map((index) => ({ diff --git a/src/generate-model/generate-model.ts b/src/generate-model/generate-model.ts index 1aab0cf2..28f4948a 100644 --- a/src/generate-model/generate-model.ts +++ b/src/generate-model/generate-model.ts @@ -3,19 +3,20 @@ import { SourceFile } from 'ts-morph'; import { generateClass } from '../generate-class'; import { generateImport } from '../generate-import'; import { generateProperty } from '../generate-property'; -import { PrismaDMMF } from '../types'; +import { GeneratorConfiguration, PrismaDMMF } from '../types'; type GenerateModelArgs = { model: PrismaDMMF.Model; sourceFile: SourceFile; projectFilePath(data: { name: string; type: string }): string; + config: GeneratorConfiguration; }; /** * Generate model (class). */ export function generateModel(args: GenerateModelArgs) { - const { model, sourceFile, projectFilePath } = args; + const { model, sourceFile, projectFilePath, config } = args; const classDeclaration = generateClass({ decorator: { name: 'ObjectType', @@ -33,6 +34,7 @@ export function generateModel(args: GenerateModelArgs) { className: model.name, classType: 'model', projectFilePath, + config, }); }); } diff --git a/src/generate-object.ts b/src/generate-object.ts index 12e882a6..2fb40699 100644 --- a/src/generate-object.ts +++ b/src/generate-object.ts @@ -3,19 +3,21 @@ import { SourceFile } from 'ts-morph'; import { generateClass } from './generate-class'; import { generateImport } from './generate-import'; import { generateProperty, Model } from './generate-property'; +import { GeneratorConfiguration } from './types'; type GenerateObjectArgs = { sourceFile: SourceFile; projectFilePath(data: { name: string; type: string }): string; model: Model; classType: string; + config: GeneratorConfiguration; }; /** * Generate object type (class). */ export function generateObject(args: GenerateObjectArgs) { - const { model, classType, sourceFile, projectFilePath } = args; + const { model, classType, sourceFile, projectFilePath, config } = args; const classDeclaration = generateClass({ decorator: { name: 'ObjectType', @@ -32,6 +34,7 @@ export function generateObject(args: GenerateObjectArgs) { className: model.name, classType, projectFilePath, + config, }); }); } diff --git a/src/generate-property.ts b/src/generate-property.ts index 3182679f..89d5d770 100644 --- a/src/generate-property.ts +++ b/src/generate-property.ts @@ -3,6 +3,7 @@ import { ClassDeclaration, SourceFile } from 'ts-morph'; import { generateClassProperty } from './generate-class'; import { generateDecorator } from './generate-decorator'; import { generateImport, generateProjectImport } from './generate-import'; +import { GeneratorConfiguration } from './types'; import { toGraphqlImportType, toPropertyType } from './utils'; export type Field = { @@ -28,14 +29,33 @@ type GeneratePropertyArgs = { className: string; field: Field; classType: string; + config: GeneratorConfiguration; }; /** * Generate property for class. */ export function generateProperty(args: GeneratePropertyArgs) { - const { field, className, classDeclaration, sourceFile, projectFilePath, classType } = args; - const propertyType = toPropertyType(field); + const { + field, + className, + classDeclaration, + sourceFile, + projectFilePath, + classType, + config, + } = args; + const customType = config.customPropertyTypes[field.type] as + | { name: string; specifier: string } + | undefined; + if (customType) { + generateImport({ + sourceFile, + name: customType.name, + moduleSpecifier: customType.specifier, + }); + } + const propertyType = customType?.name || toPropertyType(field); let fieldType = field.type; if (field.isId || field.kind === 'scalar') { fieldType = generateImport({ diff --git a/src/generate.spec.ts b/src/generate.spec.ts index d91fc9b8..090e49a4 100644 --- a/src/generate.spec.ts +++ b/src/generate.spec.ts @@ -4,13 +4,14 @@ import { PropertyDeclaration, SourceFile } from 'ts-morph'; import { generate } from './generate'; import { generatorOptions, getImportDeclarations, stringContains } from './testing'; +import { GeneratorConfigurationOptions } from './types'; describe('main generate', () => { let property: PropertyDeclaration | undefined; let sourceFile: SourceFile | undefined; let sourceFiles: SourceFile[]; let sourceText: string; - async function getResult(args: { schema: string } & Record) { + async function getResult(args: { schema: string } & GeneratorConfigurationOptions) { const { schema, ...options } = args; const generateOptions = { ...(await generatorOptions(schema, options)), @@ -218,7 +219,7 @@ describe('main generate', () => { it('get rid of atomic number operations', async () => { await getResult({ - atomicNumberOperations: false, + atomicNumberOperations: 'false', schema: ` model User { id String @id @@ -278,7 +279,7 @@ describe('main generate', () => { it('user args type', async () => { await getResult({ - atomicNumberOperations: false, + atomicNumberOperations: 'false', schema: ` model User { id String @id @@ -326,12 +327,8 @@ describe('main generate', () => { decoratorArguments = struct.decorators?.[0].arguments; assert.strictEqual(decoratorArguments?.[0], '() => UserMaxAggregateInput'); - const imports = sourceFile.getImportDeclarations().flatMap((d) => - d.getNamedImports().map((index) => ({ - name: index.getName(), - specifier: d.getModuleSpecifierValue(), - })), - ); + const imports = getImportDeclarations(sourceFile); + assert(imports.find((x) => x.name === 'UserAvgAggregateInput')); assert(imports.find((x) => x.name === 'UserSumAggregateInput')); assert(imports.find((x) => x.name === 'UserMinAggregateInput')); @@ -340,7 +337,7 @@ describe('main generate', () => { it('aggregate output types', async () => { await getResult({ - atomicNumberOperations: false, + atomicNumberOperations: 'false', schema: ` model User { id String @id @@ -376,7 +373,7 @@ describe('main generate', () => { date DateTime? } `, - combineScalarFilters: false, + combineScalarFilters: 'false', }); const filePaths = sourceFiles.map((s) => String(s.getFilePath())); const userWhereInput = sourceFiles.find((s) => @@ -410,7 +407,7 @@ describe('main generate', () => { USER } `, - combineScalarFilters: true, + combineScalarFilters: 'true', }); const filePaths = sourceFiles.map((s) => String(s.getFilePath())); for (const filePath of filePaths) { @@ -445,13 +442,13 @@ describe('main generate', () => { USER } `, - atomicNumberOperations: false, + atomicNumberOperations: 'false', }); expect(sourceFiles.length).to.be.greaterThan(0); for (const sourceFile of sourceFiles) { sourceFile.getClasses().forEach((classDeclaration) => { if (classDeclaration.getName()?.endsWith('FieldUpdateOperationsInput')) { - assert.fail(`Class should not exists ${classDeclaration.getName()!}`); + expect.fail(`Class should not exists ${classDeclaration.getName()!}`); } }); } @@ -469,8 +466,29 @@ describe('main generate', () => { })) .forEach((struct) => { if (struct.types.find((s) => s.endsWith('FieldUpdateOperationsInput'))) { - expect.fail(`Property ${struct.name} typed ${struct.type}`); + expect.fail(`Property ${struct.name} typed ${String(struct.type)}`); } }); }); + + it('custom property mapping', async () => { + await getResult({ + schema: ` + model User { + id String @id + d Decimal + } + `, + customPropertyTypes: 'Decimal:MyDec:decimal.js', + }); + const sourceFile = sourceFiles.find((s) => s.getFilePath().endsWith('user.model.ts')); + assert(sourceFile); + const property = sourceFile.getClasses()[0]?.getProperty('d')?.getStructure(); + expect(property?.type).to.equal('MyDec'); + const imports = getImportDeclarations(sourceFile); + expect(imports).to.deep.contain({ + name: 'MyDec', + specifier: 'decimal.js', + }); + }); }); diff --git a/src/generate.ts b/src/generate.ts index bb8304b0..10dd79d8 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -10,7 +10,7 @@ import { generateInput } from './generate-input'; import { generateModel } from './generate-model'; import { generateObject } from './generate-object'; import { mutateFilters } from './mutate-filters'; -import { PrismaDMMF } from './types'; +import { GeneratorConfiguration, PrismaDMMF } from './types'; import { featureName, getOutputTypeName, @@ -19,6 +19,25 @@ import { } from './utils'; import { generateFileName } from './utils/generate-file-name'; +export function createConfig(data: Record): GeneratorConfiguration { + return { + outputFilePattern: data.outputFilePattern || `{feature}/{dasherizedName}.{type}.ts`, + combineScalarFilters: ['true', '1'].includes(data.combineScalarFilters ?? 'true'), + atomicNumberOperations: ['true', '1'].includes(data.atomicNumberOperations ?? 'false'), + customPropertyTypes: Object.fromEntries( + (data.customPropertyTypes || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .map((kv) => kv.split(':')) + .map(({ 0: key, 1: name, 2: specifier }) => [ + key, + { name: name || key, specifier }, + ]), + ), + }; +} + type GenerateArgs = GeneratorOptions & { prismaClientDmmf?: PrismaDMMF.Document; fileExistsSync?: typeof existsSync; @@ -26,8 +45,8 @@ type GenerateArgs = GeneratorOptions & { export async function generate(args: GenerateArgs) { const { generator, otherGenerators } = args; - const output = generator.output; - assert(output, 'generator.output is empty'); + const config = createConfig(generator.config); + assert(generator.output, 'generator.output is empty'); const fileExistsSync = args.fileExistsSync ?? existsSync; const prismaClientOutput = otherGenerators.find((x) => x.provider === 'prisma-client-js') ?.output; @@ -43,7 +62,7 @@ export async function generate(args: GenerateArgs) { const projectFilePath = (args: { name: string; type: string; feature?: string }) => { return generateFileName({ ...args, - template: generator.config.outputFilePattern, + template: config.outputFilePattern, models, }); }; @@ -55,7 +74,7 @@ export async function generate(args: GenerateArgs) { return sourceFile; } let sourceFileText = ''; - const localFilePath = path.join(output, filePath); + const localFilePath = path.join(generator.output!, filePath); if (fileExistsSync(localFilePath)) { sourceFileText = await fs.readFile(localFilePath, { encoding: 'utf8' }); } @@ -78,18 +97,14 @@ export async function generate(args: GenerateArgs) { // Generate models for (const model of prismaClientDmmf.datamodel.models) { const sourceFile = await createSourceFile({ type: 'model', name: model.name }); - generateModel({ model, sourceFile, projectFilePath }); + generateModel({ model, sourceFile, projectFilePath, config }); } // Generate inputs let inputTypes = prismaClientDmmf.schema.inputTypes; inputTypes = inputTypes.filter( mutateFilters(inputTypes, { - combineScalarFilters: JSON.parse( - (generator.config.combineScalarFilters as string | undefined) ?? 'true', - ) as boolean, - atomicNumberOperations: JSON.parse( - (generator.config.atomicNumberOperations as string | undefined) ?? 'false', - ) as boolean, + combineScalarFilters: config.combineScalarFilters, + atomicNumberOperations: config.atomicNumberOperations, }), ); // Create aggregate inputs @@ -149,6 +164,7 @@ export async function generate(args: GenerateArgs) { sourceFile, projectFilePath, model, + config, }); } diff --git a/src/testing/generator-options.ts b/src/testing/generator-options.ts index c1ddec93..b5127698 100644 --- a/src/testing/generator-options.ts +++ b/src/testing/generator-options.ts @@ -4,7 +4,7 @@ import crypto from 'crypto'; import findCacheDir from 'find-cache-dir'; import fs from 'fs'; -import { PrismaDMMF } from '../types'; +import { GeneratorConfigurationOptions, PrismaDMMF } from '../types'; const { dependencies: { '@prisma/generator-helper': generatorVersion }, @@ -17,7 +17,7 @@ const cachePath: string = findCacheDir({ name: 'createGeneratorOptions', create: */ export async function generatorOptions( schema: string, - options?: Record, + options?: GeneratorConfigurationOptions, ): Promise { // eslint-disable-next-line prefer-rest-params const data = JSON.stringify([generatorVersion, arguments]); @@ -32,6 +32,7 @@ export async function generatorOptions( } generator client { provider = "prisma-client-js" + previewFeatures = ["nativeTypes"] } generator proxy { provider = "node -r ts-node/register/transpile-only src/testing/proxy-generator.ts" @@ -40,6 +41,7 @@ export async function generatorOptions( outputFilePattern = "${options?.outputFilePattern || ''}" combineScalarFilters = ${JSON.stringify(options?.combineScalarFilters ?? true)} atomicNumberOperations = ${JSON.stringify(options?.atomicNumberOperations ?? false)} + customPropertyTypes = "${options?.customPropertyTypes || ''}" } ${schema} `; diff --git a/src/types.ts b/src/types.ts index 6c772bd6..d066d382 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1 +1,12 @@ export { DMMF as PrismaDMMF } from '@prisma/client/runtime'; + +export type CustomPropertyTypes = Record; + +export type GeneratorConfiguration = { + outputFilePattern: string; + combineScalarFilters: boolean; + atomicNumberOperations: boolean; + customPropertyTypes: Record; +}; + +export type GeneratorConfigurationOptions = Partial>;