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
JavaScript
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