UNPKG

prisma-nestjs-graphql

Version:

Generate object types, inputs, args, etc. from prisma schema file for usage with @nestjs/graphql module

1,667 lines (1,628 loc) 84.8 kB
import assert from 'node:assert'; import awaitEventEmitterModule from 'await-event-emitter'; import { StructureKind, Project, QuoteKind } from 'ts-morph'; import fs from 'node:fs'; import path from 'node:path'; import { unflatten } from 'flat'; import filenamify from 'filenamify'; import JSON5 from 'json5'; import outmatch from 'outmatch'; import lodash from 'lodash'; import { pathToFileURL } from 'node:url'; import { ok } from 'assert'; import pupa from 'pupa'; import getRelativePath from 'get-relative-path'; import fs$1 from 'graceful-fs'; import pluralize from 'pluralize'; function _classPrivateFieldLooseBase(e, t) { if (!{}.hasOwnProperty.call(e, t)) throw new TypeError("attempted to use private field on non-instance"); return e; } var id = 0; function _classPrivateFieldLooseKey(e) { return "__private_" + id++ + "_" + e; } const extensions = new Set(['.js', '.mjs', '.ts', '.mts', '.cts', '.cjs']); function adjustModuleSpecifier(moduleSpecifier, importExtension) { if (moduleSpecifier.startsWith('.')) { let specifierWithoutExtension = moduleSpecifier; const extension = path.extname(moduleSpecifier); if (extensions.has(extension)) { specifierWithoutExtension = moduleSpecifier.slice(0, -extension.length); } if (!importExtension) return specifierWithoutExtension; if (extension !== `.${importExtension}`) { return `${specifierWithoutExtension}.${importExtension}`; } } return moduleSpecifier; } const ReExport = { All: 'All', Directories: 'Directories', None: 'None', Single: 'Single' }; function reExport(emitter) { emitter.on('BeforeGenerateFiles', beforeGenerateFiles$1); } function beforeGenerateFiles$1(args) { const { config, output, project } = args; const rootDirectory = project.getDirectoryOrThrow(output); const { importExtension, reExport } = config; if ([ReExport.Directories, ReExport.All].includes(reExport)) { for (const directory of rootDirectory.getDescendantDirectories()) { let indexSourceFile; const exportDeclarations = directory.getSourceFiles().filter(sourceFile => { return sourceFile.getBaseName() !== 'index.ts'; }).map(sourcesFile => getExportDeclaration$1(directory, sourcesFile, importExtension)); if (exportDeclarations.length > 0) { indexSourceFile = directory.createSourceFile('index.ts', { statements: exportDeclarations }, { overwrite: true }); } if (indexSourceFile) { continue; } const namespaceExportDeclarations = directory.getDirectories().map(sourceDirectory => getNamespaceExportDeclaration(directory, sourceDirectory, importExtension)); project.createSourceFile(`${directory.getPath()}/index.ts`, { statements: namespaceExportDeclarations }, { overwrite: true }); } } if (reExport === ReExport.Single) { const exportDeclarations = project.getSourceFiles().filter(sourceFile => { return sourceFile.getBaseName() !== 'index.ts'; }).map(sourceFile => getExportDeclaration$1(rootDirectory, sourceFile, importExtension)); rootDirectory.createSourceFile('index.ts', { statements: exportDeclarations }, { overwrite: true }); } if (reExport === ReExport.All) { const exportDeclarations = []; for (const directory of rootDirectory.getDirectories()) { if (directory.getBaseName() === 'node_modules') continue; const sourceFile = directory.getSourceFileOrThrow('index.ts'); exportDeclarations.push(getExportDeclaration$1(rootDirectory, sourceFile, importExtension)); } rootDirectory.createSourceFile('index.ts', { statements: exportDeclarations }, { overwrite: true }); } } function getExportDeclaration$1(directory, sourceFile, importExtension) { const moduleSpecifier = adjustModuleSpecifier(directory.getRelativePathAsModuleSpecifierTo(sourceFile), importExtension); return { kind: StructureKind.ExportDeclaration, moduleSpecifier, namedExports: sourceFile.getExportSymbols().map(s => ({ name: s.getName() })) }; } function getNamespaceExportDeclaration(directory, sourceDirectory, importExtension) { const moduleSpecifier = adjustModuleSpecifier(directory.getRelativePathAsModuleSpecifierTo(sourceDirectory), importExtension); return { kind: StructureKind.ExportDeclaration, moduleSpecifier }; } const { camelCase, castArray, chain, cloneDeep, countBy, find, first, isEmpty, isEqual, isObject, kebabCase, keyBy, last, mapKeys, memoize, merge, omit, once, partition, remove, startCase, trim, uniq, uniqWith } = lodash; function pascalCase(string) { return startCase(camelCase(string)).replaceAll(' ', ''); } function toBoolean(value) { return ['true', '1', 'on'].includes(String(value)); } /** * Stringify field decorator options, handling `middleware` specially. * Middleware values are emitted as identifiers (not string literals) since they * are references to imported middleware functions. */ function stringifyFieldOptions(options) { const { middleware, ...rest } = options; // Stringify non-middleware options const baseString = JSON5.stringify(rest); // If no middleware, return the base string if (!middleware) { return baseString; } // Convert middleware to array of identifiers (unquoted) const middlewareArray = Array.isArray(middleware) ? middleware : [middleware]; const middlewareString = `[${middlewareArray.join(',')}]`; // If base is empty object, just return middleware if (baseString === '{}') { return `{middleware:${middlewareString}}`; } // Insert middleware into the object string (before closing brace) return baseString.replace(/\}$/, `,middleware:${middlewareString}}`); } function normalizeOutputFilePattern(outputFilePattern) { if (!outputFilePattern || typeof outputFilePattern !== 'string') return '{model}/{name}.{type}.ts'; const result = outputFilePattern.replaceAll('\\', '/').split('/').map(path => filenamify(path, { replacement: '' })).filter(Boolean).join('/'); return result; } function createUseInputType(configData) { const data = structuredClone(configData); const result = []; for (const [typeName, useInputs] of Object.entries(data)) { const entry = { ALL: undefined, typeName }; if (useInputs.ALL) { entry.ALL = useInputs.ALL; delete useInputs.ALL; } for (const [propertyName, pattern] of Object.entries(useInputs)) { entry[propertyName] = pattern; } result.push(entry); } return result; } function getSchemaFields(configFields) { const entries = Object.entries(configFields ?? {}).filter(({ 1: value }) => typeof value === 'object').map(([name, value]) => { // TODO: Improve type, make Required const fieldSetting = { arguments: [], defaultImport: toBoolean(value.defaultImport) ? true : value.defaultImport, from: value.from, input: toBoolean(value.input), model: toBoolean(value.model), namespaceImport: value.namespaceImport, output: toBoolean(value.output) }; return [name, fieldSetting]; }); return Object.fromEntries(entries); } function getSchemaDecorate(decorate) { const result = []; const configDecorate = Object.values(decorate || {}); for (const element of configDecorate) { if (!element) continue; assert.ok(element.from && element.name, `Missed 'from' or 'name' part in configuration for decorate`); result.push({ arguments: element.arguments ? JSON5.parse(element.arguments) : undefined, defaultImport: toBoolean(element.defaultImport) ? true : element.defaultImport, field: element.field, from: element.from, isMatchField: outmatch(element.field, { separator: false }), isMatchType: outmatch(element.type, { separator: false }), name: element.name, namedImport: toBoolean(element.namedImport), namespaceImport: element.namespaceImport, type: element.type }); } return result; } function getSchemaCustomImport(customImport) { const result = []; const configCustomImport = Object.values(customImport || {}); for (const element of configCustomImport) { if (!element) continue; assert.ok(element.from && element.name, `Missed 'from' or 'name' part in configuration for customImport`); result.push({ defaultImport: toBoolean(element.defaultImport) ? true : element.defaultImport, from: element.from, name: element.name, namedImport: toBoolean(element.namedImport), namespaceImport: element.namespaceImport }); } return result; } const allEmmittedBlocks = ['prismaEnums', 'schemaEnums', 'models', 'inputs', 'args', 'outputs']; const blocksDependencyMap = { args: ['args', 'inputs', 'prismaEnums'], enums: ['schemaEnums', 'prismaEnums'], inputs: ['inputs', 'prismaEnums'], models: ['models', 'schemaEnums'], outputs: ['outputs'] }; function createEmitBlocks(data) { if (!Array.isArray(data)) { return Object.fromEntries(allEmmittedBlocks.map(block => [block, true])); } let blocksToEmit = {}; for (const block of data) { if (typeof block !== 'string') continue; if (!Object.keys(blocksDependencyMap).includes(block)) continue; blocksToEmit = { ...blocksToEmit, ...Object.fromEntries(blocksDependencyMap[block].map(block => [block, true])) }; } return blocksToEmit; } function findByPattern(fieldInputTypes, pattern) { if (pattern.startsWith('matcher:') || pattern.startsWith('match:')) { const { 1: patternValue } = pattern.split(':', 2); const isMatch = outmatch(patternValue, { separator: false }); const result = fieldInputTypes.find(x => isMatch(x.type)); if (result) { return result; } } const result = fieldInputTypes.find(x => x.type.includes(pattern)); if (result) { return result; } } async function loadExternalConfig(configFile, sourceFilePath) { if (typeof configFile !== 'string') return; let externalConfigFile = configFile; if (!path.isAbsolute(configFile)) { assert.ok(sourceFilePath, 'Require sourceFilePath for relative config path'); externalConfigFile = path.resolve(path.dirname(sourceFilePath), configFile); } const absolutePath = path.resolve(externalConfigFile); const { href: fileUrl } = pathToFileURL(absolutePath); let configModule; try { configModule = await import(fileUrl); } catch { assert.fail(`Failed to load config file: ${externalConfigFile}`); } const externalConfig = configModule.default ?? configModule; assert.ok(isObject(externalConfig), `Config file must export a non-null object: ${externalConfigFile}`); if (externalConfig.output !== undefined) { assert.ok(typeof externalConfig.output === 'string' && externalConfig.output.length > 0, `Config 'output' must be a non-empty string`); } if (externalConfig.decorators !== undefined) { assert.ok(Array.isArray(externalConfig.decorators), `Config 'decorators' must be an array`); for (const element of externalConfig.decorators) { assert.ok(isObject(element), `Each 'decorators' element must be an object`); assert.ok(element.from, `Each 'decorators' element missing 'from'`); assert.ok(element.name, `Each 'decorators' element missing 'name'`); } } if (externalConfig.customImports !== undefined) { assert.ok(Array.isArray(externalConfig.customImports), `Config 'customImports' must be an array`); for (const element of externalConfig.customImports) { assert.ok(isObject(element), `Each 'customImports' element must be an object`); assert.ok(element.from, `Each 'customImports' element missing 'from'`); assert.ok(element.name, `Each 'customImports' element missing 'name'`); } } return { externalConfig, externalConfigFile }; } var _generatorOutput = /*#__PURE__*/_classPrivateFieldLooseKey("generatorOutput"); var _emitBlocks = /*#__PURE__*/_classPrivateFieldLooseKey("emitBlocks"); var _outputFilePattern = /*#__PURE__*/_classPrivateFieldLooseKey("outputFilePattern"); class Configuration { constructor(args) { this.warnings = new Set(); Object.defineProperty(this, _generatorOutput, { writable: true, value: void 0 }); this.schemaConfig = void 0; this.externalConfig = void 0; this.externalConfigFile = void 0; Object.defineProperty(this, _emitBlocks, { writable: true, value: void 0 }); Object.defineProperty(this, _outputFilePattern, { writable: true, value: '' }); const { externalConfig, externalConfigFile, schemaConfig } = args; this.schemaConfig = schemaConfig; this.externalConfigFile = externalConfigFile; this.externalConfig = externalConfig; _classPrivateFieldLooseBase(this, _generatorOutput)[_generatorOutput] = args.output; _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks] = createEmitBlocks(externalConfig?.emitBlocks ?? schemaConfig.emitBlocks); this.initializeOutputFilePattern(); for (const name of ['fields', 'useInputType', 'graphqlScalars', 'customImport', 'decorate']) { if (this.schemaConfig[name]) { this.warnings.add(`Generator options in schema (${name}) are deprecated, prefer config file`); } } } static async create(args) { const { config, output, sourceFilePath } = args; const schemaConfig = unflatten(config, { delimiter: '_' }); const { externalConfig, externalConfigFile } = (await loadExternalConfig(schemaConfig.configFile, sourceFilePath)) ?? {}; return new Configuration({ externalConfig, externalConfigFile, output, schemaConfig, sourceFilePath }); } initializeOutputFilePattern() { const testOutputFilePattern = this.externalConfig?.outputFilePattern ?? this.schemaConfig.outputFilePattern; _classPrivateFieldLooseBase(this, _outputFilePattern)[_outputFilePattern] = normalizeOutputFilePattern(testOutputFilePattern); if (testOutputFilePattern && testOutputFilePattern !== _classPrivateFieldLooseBase(this, _outputFilePattern)[_outputFilePattern]) { this.warnings.add(`outputFilePattern changed to ${_classPrivateFieldLooseBase(this, _outputFilePattern)[_outputFilePattern]}`); } } get output() { if (this.externalConfig?.output && this.externalConfigFile) { return path.resolve(path.dirname(this.externalConfigFile), this.externalConfig.output); } return _classPrivateFieldLooseBase(this, _generatorOutput)[_generatorOutput]; } get outputFilePattern() { return _classPrivateFieldLooseBase(this, _outputFilePattern)[_outputFilePattern]; } get importExtension() { if (this.externalConfig?.importExtension) { return this.externalConfig.importExtension; } if (typeof this.schemaConfig.importExtension === 'string' && this.schemaConfig.importExtension) { return this.schemaConfig.importExtension; } return ''; } get emitCompiled() { if (this.externalConfig?.emitCompiled !== undefined) { return this.externalConfig.emitCompiled; } if (this.schemaConfig.emitCompiled) { return toBoolean(this.schemaConfig.emitCompiled); } return false; } get tsConfigFilePath() { const tsConfigFilePath = this.externalConfig?.tsConfigFilePath || this.schemaConfig.tsConfigFilePath; if (typeof tsConfigFilePath === 'string') return tsConfigFilePath; if (fs.existsSync('tsconfig.json')) return 'tsconfig.json'; } get combineScalarFilters() { if (this.externalConfig?.combineScalarFilters !== undefined) { return this.externalConfig.combineScalarFilters; } if (this.schemaConfig.combineScalarFilters) { return toBoolean(this.schemaConfig.combineScalarFilters); } return true; } get noTypeId() { if (this.externalConfig?.noTypeId !== undefined) { return this.externalConfig.noTypeId; } if (this.schemaConfig.noTypeId) { return toBoolean(this.schemaConfig.noTypeId); } return false; } get noAtomicOperations() { if (this.externalConfig?.noAtomicOperations !== undefined) { return this.externalConfig.noAtomicOperations; } if (this.schemaConfig.noAtomicOperations) { return toBoolean(this.schemaConfig.noAtomicOperations); } return true; } get reExport() { const reExport = this.externalConfig?.reExport ?? this.schemaConfig.reExport; if (typeof reExport !== 'string') return ReExport.None; if (Object.values(ReExport).includes(reExport)) return reExport; return ReExport.None; } get purgeOutput() { if (this.externalConfig?.purgeOutput !== undefined) { return this.externalConfig.purgeOutput; } if (this.schemaConfig.purgeOutput) { return toBoolean(this.schemaConfig.purgeOutput); } return false; } get emitSingle() { if (this.externalConfig?.emitSingle !== undefined) { return this.externalConfig.emitSingle; } if (this.schemaConfig.emitSingle) { return toBoolean(this.schemaConfig.emitSingle); } return false; } get requireSingleFieldsInWhereUniqueInput() { if (this.externalConfig?.requireSingleFieldsInWhereUniqueInput !== undefined) { return this.externalConfig.requireSingleFieldsInWhereUniqueInput; } if (this.schemaConfig.requireSingleFieldsInWhereUniqueInput) { return toBoolean(this.schemaConfig.requireSingleFieldsInWhereUniqueInput); } return false; } get omitModelsCount() { if (this.externalConfig?.omitModelsCount !== undefined) { return this.externalConfig.omitModelsCount; } if (this.schemaConfig.omitModelsCount) { return toBoolean(this.schemaConfig.omitModelsCount); } return false; } get typeListNullable() { if (this.externalConfig?.typeListNullable !== undefined) { return this.externalConfig.typeListNullable; } if (this.schemaConfig.typeListNullable) { return toBoolean(this.schemaConfig.typeListNullable); } return false; } get unsafeCompatibleWhereUniqueInput() { if (this.externalConfig?.unsafeCompatibleWhereUniqueInput !== undefined) { return this.externalConfig.unsafeCompatibleWhereUniqueInput; } if (this.schemaConfig.unsafeCompatibleWhereUniqueInput) { return toBoolean(this.schemaConfig.unsafeCompatibleWhereUniqueInput); } return false; } get prismaClientImport() { const value = this.externalConfig?.prismaClientImport ?? this.schemaConfig.prismaClientImport; if (typeof value === 'string') return value; return '@prisma/client'; } get emitBlocksModels() { return _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks].models; } get emitBlocksPrismaEnums() { return _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks].prismaEnums; } get emitBlocksSchemaEnums() { return _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks].schemaEnums; } get emitBlocksOutputs() { return _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks].outputs; } get emitBlocksInputs() { return _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks].inputs; } get emitBlocksArgs() { return _classPrivateFieldLooseBase(this, _emitBlocks)[_emitBlocks].args; } getGraphqlScalar(type) { const graphqlScalars = this.externalConfig?.graphqlScalars ?? this.schemaConfig.graphqlScalars; if (typeof graphqlScalars?.[type]?.name === 'string') { return graphqlScalars[type]; } } getField(namespace) { if (!namespace) return; const fields = this.externalConfig?.fields ?? getSchemaFields(this.schemaConfig.fields); return fields?.[namespace]; } /** * Get graphql for input type */ getInputType(args) { const { fieldInputTypes, fieldName, inputTypeName } = args; const configInputType = this.externalConfig?.inputType; if (typeof configInputType === 'function') { const result = configInputType(args); if (typeof result === 'string') { return findByPattern(fieldInputTypes, result); } return result; } if (isObject(configInputType)) { const typeMap = find(configInputType, (_, typeName) => inputTypeName.includes(typeName)); if (isObject(typeMap)) { const pattern = find(typeMap, (_, field) => field === fieldName || field === '*'); if (pattern) return findByPattern(fieldInputTypes, pattern); } } if (this.schemaConfig.useInputType) { const configUseInputType = createUseInputType(this.schemaConfig.useInputType); const useInputType = configUseInputType.find(x => inputTypeName.includes(x.typeName)); const pattern = useInputType?.ALL || useInputType?.[fieldName]; if (pattern) return findByPattern(fieldInputTypes, pattern); } } get customImports() { if (this.externalConfig?.customImports) { return this.externalConfig.customImports; } if (this.schemaConfig.customImport) { return getSchemaCustomImport(this.schemaConfig.customImport); } return []; } shouldHideField(args) { const { input, output, settings, ...fieldInfo } = args; const { objectName, propertyName } = fieldInfo; if (typeof this.externalConfig?.shouldHideField === 'function') { return this.externalConfig.shouldHideField(fieldInfo); } return settings?.shouldHideField({ input, name: objectName, output }) || this.decorate.some(d => d.name === 'HideField' && d.from === '@nestjs/graphql' && d.isMatchField(propertyName) && d.isMatchType(objectName)); } *getDecorators() { if (this.externalConfig?.decorators) { yield* this.externalConfig.decorators; } if (this.schemaConfig.decorate) { const decorators = getSchemaDecorate(this.schemaConfig.decorate); for (const decorator of decorators) { const match = ({ objectName, propertyName }) => { return decorator.isMatchField(propertyName) && decorator.isMatchType(objectName); }; yield { ...decorator, match }; } } } /** * Get field override arguments for a specific field. * Returns merged fieldArguments from all matching overrides. */ getFieldOverride(args) { const overrides = this.externalConfig?.fieldDecoratorArguments; if (!overrides?.length) return; let result; for (const override of overrides) { if (override.match(args)) { result = { ...result, ...override.decoratorArguments }; } } return result; } /** * @deprecated Should be replaced by decorators */ get decorate() { if (this.schemaConfig.decorate) { return getSchemaDecorate(this.schemaConfig.decorate); } return []; } } function isWhereUniqueInputType(name) { return name.endsWith('WhereUniqueInput'); } function isManyAndReturnOutputType(name) { const lowerName = name.toLowerCase(); if ((lowerName.startsWith('createmany') || lowerName.startsWith('updatemany')) && (lowerName.endsWith('andreturnoutputtype') || lowerName.endsWith('andreturn'))) { return true; } return false; } /** * See https://github.com/prisma/prisma/blob/master/src/packages/client/src/generation/TSClient/Model.ts@getAggregationTypes * Subcribes on: 'ArgsType' */ function argsType(field, args) { if (['queryRaw', 'executeRaw'].includes(field.name)) { return; } if (isManyAndReturnOutputType(field.name)) return; const { eventEmitter, getModelName, typeNames } = args; let className = pascalCase(`${field.name}Args`); const modelName = getModelName(className) || ''; switch (className) { case `Aggregate${modelName}Args`: { className = `${modelName}AggregateArgs`; break; } case `GroupBy${modelName}Args`: { className = `${modelName}GroupByArgs`; break; } } const inputType = { constraints: { maxNumFields: null, minNumFields: null }, fields: [...field.args], name: className }; if (!field.args.some(x => x.name === '_count') && [`${modelName}AggregateArgs`, `${modelName}GroupByArgs`].includes(className)) { const names = ['Count', 'Avg', 'Sum', 'Min', 'Max']; if (`${modelName}GroupByArgs` === inputType.name) { // Make `by` property array only, noEnumerable const byField = inputType.fields.find(f => f.name === 'by'); if (byField?.inputTypes) { byField.inputTypes = byField.inputTypes.filter(inputType => inputType.isList); } } for (const name of names) { if (!typeNames.has(`${modelName}${name}AggregateInput`)) { continue; } inputType.fields.push({ inputTypes: [{ isList: false, location: 'inputObjectTypes', type: `${modelName}${name}AggregateInput` }], isNullable: true, isParameterizable: false, // ? isRequired: false, name: `_${name.toLowerCase()}` }); } } eventEmitter.emitSync('InputType', { ...args, classDecoratorName: 'ArgsType', fileType: 'args', inputType }); } const BeforeGenerateField = 'BeforeGenerateField'; /** * Subscribes on 'BeforeInputType' */ function combineScalarFilters(eventEmitter) { eventEmitter.on('BeforeInputType', beforeInputType$2); eventEmitter.on(BeforeGenerateField, beforeGenerateField); eventEmitter.on('PostBegin', postBegin); } function beforeInputType$2(args) { const { inputType, removeTypes } = args; if (isContainBogus(inputType.name) && isScalarFilter(inputType)) { removeTypes.add(inputType.name); inputType.name = replaceBogus(inputType.name); } } function beforeGenerateField(field, // eslint-disable-next-line @typescript-eslint/no-unused-vars args) { for (const fieldInput of field.inputTypes) { if (fieldInput.location !== 'inputObjectTypes') { continue; } const fieldInputType = fieldInput.type; if (isContainBogus(fieldInputType)) { fieldInput.type = replaceBogus(fieldInputType); } } } function replaceBogus(name) { return name.replaceAll(/Nullable|Nested/g, ''); } function isContainBogus(name) { return name.startsWith('Nested') || name.includes('Nullable') && name.endsWith('Filter') || name.endsWith('NullableFilter'); } function isScalarFilter(inputType) { if (!inputType.name.endsWith('Filter')) { return false; } let result = false; const equals = inputType.fields.find(f => f.name === 'equals'); if (equals) { result = equals.inputTypes.every(x => { return ['enumTypes', 'scalar'].includes(x.location); }); } return result; } function postBegin(args) { const { modelNames, schema } = args; const inputTypes = schema.inputObjectTypes.prisma ?? []; const enumTypes = schema.enumTypes.model || []; const types = ['Bool', 'Int', 'String', 'DateTime', 'Decimal', 'Float', 'Json', 'Bytes', 'BigInt', ...enumTypes.map(x => `Enum${x.name}`)]; const inputTypeByName = keyBy(inputTypes, inputType => inputType.name); const replaceBogusFilters = (filterName, filterNameCandidates) => { for (const filterNameCandidate of filterNameCandidates) { const candidate = inputTypeByName[filterNameCandidate]; if (candidate) { 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`, `Nested${type}Filter`]); replaceBogusFilters(`${type}WithAggregatesFilter`, [`${type}NullableWithAggregatesFilter`, `Nested${type}NullableWithAggregatesFilter`, `Nested${type}WithAggregatesFilter`]); replaceBogusFilters(`${type}ListFilter`, [`${type}NullableListFilter`, `Nested${type}NullableListFilter`, `Nested${type}ListFilter`]); } for (const modelName of modelNames) { replaceBogusFilters(`${modelName}RelationFilter`, [`${modelName}NullableRelationFilter`]); } for (const modelName of modelNames) { replaceBogusFilters(`${modelName}ScalarRelationFilter`, [`${modelName}NullableScalarRelationFilter`]); } remove(inputTypes, inputType => { return isContainBogus(inputType.name); }); } /** * Create aggregate inputs from aggregate outputs. * See client/src/generation/TSClient.ts @ getAggregationTypes * Subcribes on: 'AggregateOutput' */ function createAggregateInput(args) { const { eventEmitter, outputType } = args; const className = `${outputType.name}Input`; // console.dir({ outputType, className, __filename }, { depth: 5 }); const inputType = { constraints: { maxNumFields: null, minNumFields: null }, fields: outputType.fields.map(x => ({ inputTypes: [{ isList: false, location: 'scalar', type: 'true' }], isNullable: x.isNullable ?? true, isParameterizable: false, // ? isRequired: false, name: x.name })), name: className }; eventEmitter.emitSync('InputType', { ...args, classDecoratorName: 'InputType', fileType: 'input', inputType }); } class ImportDeclarationMap extends Map { add(name, value) { if (!this.has(name)) { if (typeof value === 'string') { this.set(name, { moduleSpecifier: value, namedImports: [{ name }] }); } else { this.set(name, value); } } } create(args) { const { config, propertySettings, propertyType } = args; if (propertySettings) { return this.createFrom({ ...propertySettings }); } if (/\bIdentity</.test(propertyType)) { this.add('Identity', { isTypeOnly: true, moduleSpecifier: 'identity-type', namedImports: [{ name: 'Identity' }] }); } if ([/\bDecimal\b/, /\bArray<Decimal>\b/].some(re => re.test(propertyType))) { // TODO: Deprecated and should be removed this.add('Decimal', '@prisma/client-runtime-utils'); } if (/\bPrisma\./.test(propertyType)) { this.add('Prisma', config.prismaClientImport); } } createFrom(args) { const { defaultImport, from, namedImport, namespaceImport } = args; let name = args.name; const value = { defaultImport: undefined, moduleSpecifier: from, namedImports: [], namespaceImport: undefined }; if (namedImport === true && namespaceImport) { value.namedImports = [{ name: namespaceImport }]; name = namespaceImport; } else if (defaultImport) { value.defaultImport = defaultImport === true ? name : defaultImport; name = value.defaultImport; } else if (namespaceImport) { value.namespaceImport = namespaceImport; name = namespaceImport; } else { value.namedImports = [{ name }]; } this.add(name, value); } *toStatements() { const iterator = this.values(); let result = iterator.next(); while (result.value) { yield { ...result.value, kind: StructureKind.ImportDeclaration }; result = iterator.next(); } } } async function generateFiles(args) { const { config, eventEmitter, output, project } = args; if (config.emitSingle) { combineToSingle({ config, output, project }); } if (config.emitCompiled) { project.compilerOptions.set({ declaration: true, declarationDir: output, emitDecoratorMetadata: true, outDir: output, rootDir: output, skipLibCheck: true }); const emitResult = await project.emit(); const errors = emitResult.getDiagnostics().map(d => String(d.getMessageText())); if (errors.length > 0) { eventEmitter.emitSync('Warning', errors); } } else { await project.save(); } } function combineToSingle(args) { const { config, output, project } = args; const rootDirectory = project.getDirectory(output) || project.createDirectory(output); const sourceFile = rootDirectory.getSourceFile('index.ts') || rootDirectory.createSourceFile('index.ts', undefined, { overwrite: true }); const statements = project.getSourceFiles().flatMap(s => { if (s === sourceFile) { return []; } const classDeclaration = s.getClass(() => true); const statements = s.getStructure().statements; // Reget decorator full name // TODO: Check possible bug of ts-morph if (Array.isArray(statements)) { for (const statement of statements) { if (!(typeof statement === 'object' && statement.kind === StructureKind.Class)) { continue; } for (const property of statement.properties || []) { for (const decorator of property.decorators || []) { const fullName = classDeclaration?.getProperty(property.name)?.getDecorator(decorator.name)?.getFullName(); assert.ok(fullName, `Cannot get full name of decorator of class ${statement.name}`); decorator.name = fullName; } } } } project.removeSourceFile(s); return statements; }); const imports = new ImportDeclarationMap(); const enums = []; const classes = []; for (const statement of statements) { if (typeof statement === 'string') { if (statement.startsWith('registerEnumType')) { enums.push(statement); } continue; } switch (statement.kind) { case StructureKind.ImportDeclaration: { if (statement.moduleSpecifier.startsWith('.')) { continue; } for (const namedImport of statement.namedImports) { const name = namedImport.alias || namedImport.name; if (statement.moduleSpecifier === 'identity-type') { imports.add(name, statement); continue; } imports.add(name, statement.moduleSpecifier); } if (statement.defaultImport) { imports.createFrom({ defaultImport: statement.defaultImport, from: statement.moduleSpecifier, name: statement.defaultImport }); } if (statement.namespaceImport) { imports.createFrom({ from: statement.moduleSpecifier, name: statement.namespaceImport, namespaceImport: statement.namespaceImport }); } break; } case StructureKind.Enum: { enums.unshift(statement); break; } case StructureKind.Class: { classes.push(statement); break; } } } for (const customImport of config.customImports) { imports.createFrom(customImport); } sourceFile.set({ kind: StructureKind.SourceFile, statements: [...imports.toStatements(), ...enums, ...classes] }); } function fileTypeByLocation(fieldLocation) { switch (fieldLocation) { case 'inputObjectTypes': { return 'input'; } case 'outputObjectTypes': { return 'output'; } case 'enumTypes': { return 'enum'; } } return 'object'; } function relativePath(from, to) { if (!from.startsWith('/')) { from = `/${from}`; } if (!to.startsWith('/')) { to = `/${to}`; } let result = getRelativePath(from, to); if (!result.startsWith('.')) { result = `./${result}`; } return result; } function getGraphqlImport(args) { const { config, fileType, getSourceFile, isId, location, sourceFile, typeName } = args; if (location === 'scalar') { if (isId && !config.noTypeId) { return { name: 'ID', specifier: '@nestjs/graphql' }; } const graphqlType = config.getGraphqlScalar(typeName); if (graphqlType) { return { name: graphqlType.name, specifier: graphqlType.specifier }; } switch (typeName) { case 'Float': case 'Int': { return { name: typeName, specifier: '@nestjs/graphql' }; } case 'DateTime': { return { name: 'Date', specifier: undefined }; } case 'true': case 'Boolean': { return { name: 'Boolean', specifier: undefined }; } case 'Decimal': { return { name: 'GraphQLDecimal', specifier: 'prisma-graphql-type-decimal' }; } case 'Json': { return { name: 'GraphQLJSON', specifier: 'graphql-type-json' }; } } return { name: 'String', specifier: undefined }; } let sourceFileType = fileTypeByLocation(location); if (sourceFileType === 'output' && fileType === 'model') { sourceFileType = 'model'; } const specifier = adjustModuleSpecifier(relativePath(sourceFile.getFilePath(), getSourceFile({ name: typeName, type: sourceFileType }).getFilePath()), config.importExtension); return { name: typeName, specifier }; } /** * Find input type for graphql field decorator. */ function getGraphqlInputType(field, inputTypeName, config) { let result; const inputTypes = chain(field.inputTypes).filter(t => !['null', 'Null'].includes(String(t.type))).uniqWith(isEqual).value(); if (inputTypes.length === 1) { return inputTypes[0]; } const countTypes = countBy(inputTypes, x => x.location); const isOneType = Object.keys(countTypes).length === 1; if (isOneType) { result = inputTypes.find(x => x.isList); if (result) { return result; } } result = config.getInputType({ fieldInputTypes: inputTypes, fieldName: field.name, inputTypeName }); if (result) { return result; } result = inputTypes.find(x => x.location === 'inputObjectTypes'); if (result) { return result; } if (countTypes.enumTypes && countTypes.scalar && inputTypes.some(x => x.type === 'Json' && x.location === 'scalar')) { result = inputTypes.find(x => x.type === 'Json' && x.location === 'scalar'); if (result) { return result; } } if ((countTypes.scalar >= 1 || countTypes.enumTypes >= 1) && countTypes.fieldRefTypes === 1) { result = inputTypes.find(x => (x.location === 'scalar' || x.location === 'enumTypes') && x.isList); if (result) { return result; } result = inputTypes.find(x => x.location === 'scalar' || x.location === 'enumTypes'); if (result) { return result; } } throw new TypeError(`Cannot get matching input type from ${inputTypes.map(x => x.type).join(', ') || 'zero length inputTypes'}`); } /** * Returns typescript property type. */ function getPropertyType(args) { const { location, type } = args; switch (type) { case 'Float': case 'Int': { return ['number']; } case 'String': { return ['string']; } case 'Boolean': { return ['boolean']; } case 'DateTime': { return ['Date', 'string']; } case 'Decimal': { return ['Decimal']; // TODO: Use Prisma.Decimal } case 'Json': { return ['any']; } case 'Null': { return ['null']; } case 'Bytes': { return ['Prisma.Bytes']; } case 'BigInt': { return ['bigint', 'number']; } } if (['inputObjectTypes', 'outputObjectTypes'].includes(location)) { return [type]; } if (location === 'enumTypes') { const enumType = '`${' + type + '}`'; return [enumType]; } if (location === 'scalar') { return [type]; } return ['unknown']; } function getWhereUniqueAtLeastKeys(model) { const names = model.fields.filter(field => field.isUnique || field.isId).map(field => field.name); if (model.primaryKey) { names.push(createFieldName(model.primaryKey)); } for (const uniqueIndex of model.uniqueIndexes) { names.push(createFieldName(uniqueIndex)); } return names; } function createFieldName(args) { const { fields, name } = args; return name || fields.join('_'); } /** * Get property structure (field) for class. */ function propertyStructure(args) { const { hasExclamationToken, hasQuestionToken, isList, isNullable, location, name, propertyType } = args; const type = createProperyType({ isList, location, propertyType }); return { decorators: [], hasExclamationToken: hasExclamationToken ?? !isNullable, hasQuestionToken: hasQuestionToken ?? isNullable, kind: StructureKind.Property, leadingTrivia: '\n', name, type }; } function createProperyType(args) { const { isList, location, propertyType } = args; return propertyType.map(type => { if (isList) return `Array<${type}>`; if (type.startsWith('Prisma.')) return type; if (type === 'null') return type; if (['inputObjectTypes', 'outputObjectTypes'].includes(location)) { return `Identity<${type}>`; } return type; }).join(' | '); } function inputType(args) { const { classDecoratorName, classTransformerTypeModels, config, eventEmitter, fieldSettings, fileType, getModelName, getSourceFile, inputType, models, removeTypes, typeNames } = args; typeNames.add(inputType.name); const importDeclarations = new ImportDeclarationMap(); const sourceFile = getSourceFile({ name: inputType.name, type: fileType }); const classStructure = { decorators: [{ arguments: [], name: classDecoratorName }], isExported: true, kind: StructureKind.Class, name: inputType.name, properties: [] }; const modelName = getModelName(inputType.name) || ''; const model = models.get(modelName); const modelFieldSettings = model && fieldSettings.get(model.name); const moduleSpecifier = '@nestjs/graphql'; importDeclarations.set('Field', { moduleSpecifier, namedImports: [{ name: 'Field' }] }).set(classDecoratorName, { moduleSpecifier, namedImports: [{ name: classDecoratorName }] }); const isWhereUnique = isWhereUniqueInputType(inputType.name); for (const field of inputType.fields) { field.inputTypes = field.inputTypes.filter(t => !removeTypes.has(t.type)); eventEmitter.emitSync(BeforeGenerateField, field, args); const { inputTypes, isRequired, name } = field; if (inputTypes.length === 0) { // No types continue; } const graphqlInputType = getGraphqlInputType(field, inputType.name, config); const { isList, location, type } = graphqlInputType; const typeName = type; const settings = modelFieldSettings?.get(name); const propertySettings = settings?.getPropertyType({ input: true, name: inputType.name }); const modelField = model?.fields.find(f => f.name === name); const isCustomsApplicable = typeName === modelField?.type; const atLeastKeys = model && getWhereUniqueAtLeastKeys(model); const whereUniqueInputType = isWhereUniqueInputType(typeName) && atLeastKeys && `Prisma.AtLeast<${typeName}, ${atLeastKeys.map(name => `'${name}'`).join(' | ')}>`; const propertyType = castArray(propertySettings?.name || whereUniqueInputType || getPropertyType({ location, type: typeName })); const hasExclamationToken = Boolean(isWhereUnique && config.unsafeCompatibleWhereUniqueInput && atLeastKeys?.includes(name)); const property = propertyStructure({ hasExclamationToken: hasExclamationToken || undefined, hasQuestionToken: hasExclamationToken ? false : undefined, isList, isNullable: !isRequired, location, name, propertyType }); classStructure.properties.push(property); importDeclarations.create({ config, propertySettings, propertyType: property.type }); const fieldInfo = { location, objectName: inputType.name, propertyName: property.name, propertyType: property.type, typeName }; const shouldHideField = config.shouldHideField({ ...fieldInfo, input: true, settings }); const fieldType = settings?.getFieldType({ input: true, name: inputType.name }); // Get graphql type let graphqlType; if (fieldType && isCustomsApplicable && !shouldHideField) { graphqlType = fieldType.name; importDeclarations.createFrom({ ...fieldType }); } else { // Import property type class const graphqlImport = getGraphqlImport({ config, getSourceFile, location, sourceFile, typeName }); graphqlType = graphqlImport.name; let referenceName = propertyType[0]; if (location === 'enumTypes') { referenceName = last(referenceName.split(' ')) ?? 'any'; } if (graphqlImport.specifier && !importDeclarations.has(graphqlImport.name) && graphqlImport.name !== inputType.name // ((graphqlImport.name !== inputType.name && !shouldHideField) || // (shouldHideField && referenceName === graphqlImport.name)) ) { importDeclarations.set(graphqlImport.name, { moduleSpecifier: graphqlImport.specifier, namedImports: [{ name: graphqlImport.name }] }); } } ok(property.decorators, 'property.decorators is undefined'); if (shouldHideField) { importDeclarations.add('HideField', moduleSpecifier); property.decorators.push({ arguments: [], name: 'HideField' }); } else { // Get field overrides from config const fieldOverride = config.getFieldOverride(fieldInfo); // Generate `@Field()` decorator property.decorators.push({ arguments: [isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`, stringifyFieldOptions({ ...settings?.fieldArguments(), nullable: !isRequired, ...fieldOverride })], name: 'Field' }); if (graphqlType === 'GraphQLDecimal') { importDeclarations.add('transformToDecimal', 'prisma-graphql-type-decimal'); importDeclarations.add('Transform', 'class-transformer'); importDeclarations.add('Type', 'class-transformer'); property.decorators.push({ arguments: ['() => Object'], name: 'Type' }, { arguments: ['transformToDecimal'], name: 'Transform' }); } else if (location === 'inputObjectTypes' && (modelField?.type === 'Decimal' || ['connect', 'connectOrCreate', 'create', 'createMany', 'data', 'delete', 'deleteMany', 'disconnect', 'set', 'update', 'updateMany', 'upsert', 'where'].includes(name) || classTransformerTypeModels.has(getModelName(graphqlType) || '') || modelField?.kind === 'object' && models.get(modelField.type) && models.get(modelField.type)?.fields.some(field => field.kind === 'object' && classTransformerTypeModels.has(field.type)))) { importDeclarations.add('Type', 'class-transformer'); property.decorators.push({ arguments: [`() => ${graphqlType}`], name: 'Type' }); } if (isCustomsApplicable) { for (const options of settings || []) { if ((options.kind === 'Decorator' && options.input && options.match?.(name)) ?? true) { property.decorators.push({ arguments: options.arguments, name: options.name }); ok(options.from, "Missed 'from' part in configuration or field setting"); importDeclarations.createFrom(options); } } } // TODO: DRY for (const decorator of config.getDecorators()) { // eslint-disable-next-line unicorn/prefer-regexp-test if (decorator.match(fieldInfo)) { property.decorators.push({ arguments: decorator.arguments?.map(x => pupa(x, { propertyType })), name: decorator.name }); importDeclarations.createFrom(decorator); } } } eventEmitter.emitSync('ClassProperty', property, { isList, location, propertyType }); } sourceFile.set({ statements: [...importDeclarations.toStatements(), classStructure] }); } // TODO: Should move to config class ObjectSettings extends Array { shouldHideField({ input = false, name, output = false }) { const hideField = this.find(s => s.name === 'HideField'); return Boolean(hideField?.input && input || hideField?.output && output || hideField?.match?.(name)); } getFieldType({ input, name, output }) { const fieldType = this.find(s => s.kind === 'FieldType'); if (!fieldType) { return undefined; } if (fieldType.match) { return fieldType.match(name) ? fieldType : undefined; } if (input && !fieldType.input) { return undefined; } if (output && !fieldType.output) { return undefined; } return fieldType; } getPropertyType({ input, name