import { Inject, Injectable } from '@nestjs/common'; import { type BaseEnum, type EnumRef, InputObjectRef, type InputType, type InputTypeParam, type SchemaTypes, } from '@pothos/core'; import { type PrismaModelTypes, getModel } from '@pothos/plugin-prisma'; import type { FilterOps } from '@pothos/plugin-prisma-utils'; import * as Prisma from '@prisma/client'; import { SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'; // const filterOps = ['equals', 'in', 'notIn', 'not', 'is', 'isNot'] as const; const sortableFilterProps = ['lt', 'lte', 'gt', 'gte'] as const; const stringFilterOps = [ ...filterOps, 'contains', 'startsWith', 'endsWith', ] as const; const sortableTypes = ['String', 'Int', 'Float', 'DateTime', 'BigInt'] as const; const listOps = ['every', 'some', 'none'] as const; const scalarListOps = [ 'has', 'hasSome', 'hasEvery', 'isEmpty', 'equals', ] as const; const JsonFilterOps = ['equals', 'in', 'notIn', 'not', 'is', 'isNot'] as const; const EnumFilterOps = ['equals', 'not'] as const; @Injectable() export class PrismaCrudGenerator { private refCache = new Map< InputType | string, Map> >(); private enumRefs = new Map>(); constructor( @Inject(SchemaBuilderToken) private builder: PothosSchemaTypes.SchemaBuilder, ) {} findManyArgs( modelName: Name, ) { return this.builder.args((t) => ({ filter: t.field({ type: this.getWhere(modelName), required: false, }), orderBy: t.field({ type: this.getOrderBy(modelName), required: false, }), cursor: t.field({ type: this.getWhereUnique(modelName), required: false, }), take: t.field({ type: 'Int', required: false, }), skip: t.field({ type: 'Int', required: false, }), })); } findUniqueArgs( modelName: Name, ) { return this.builder.args((t) => ({ where: t.field({ type: this.getWhereUnique(modelName), required: true }), })); } getWhere( modelName: Name, without?: string[], ) { const withoutName = (without ?? []) .map((name) => `Without${capitalize(name)}`) .join(''); const fullName = `${modelName}${withoutName}Filter`; return this.getRef(modelName, fullName, () => { const model = getModel(modelName, this.builder); return this.builder.prismaWhere(modelName, { name: fullName, fields: (() => { const fields: Record> = {}; const withoutFields = model.fields.filter((field) => without?.includes(field.name), ); model.fields .filter( (field) => !withoutFields.some( (f) => f.name === field.name || f.relationFromFields?.includes(field.name), ), ) .forEach((field) => { // biome-ignore lint/suspicious/noImplicitAnyLet: let type; switch (field.kind) { case 'scalar': type = field.isList ? this.getScalarListFilter( this.mapScalarType(field.type) as InputType, ) : this.getFilter( this.mapScalarType(field.type) as InputType, ); break; case 'enum': type = field.isList ? this.getScalarListFilter(this.getEnum(field.type)) : this.getFilter(this.getEnum(field.type)); break; case 'object': type = field.isList ? this.getListFilter(this.getWhere(field.type as Name)) : this.getWhere(field.type as Name); break; case 'unsupported': break; default: throw new Error(`Unknown field kind ${field.kind}`); } if (!type) { return; } fields[field.name] = type; }); return fields; }) as never, }) as InputObjectRef< Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Where'] >; }); } getWhereUnique( modelName: Name, ) { const name = `${modelName}UniqueFilter`; return this.getRef(modelName, name, () => { const model = getModel(modelName, this.builder); return this.builder.prismaWhereUnique(modelName, { name, fields: (() => { const fields: Record> = {}; model.fields .filter( (field) => field.isUnique || field.isId || model.uniqueIndexes.some((index) => index.fields.includes(field.name), ) || model.primaryKey?.fields.includes(field.name), ) .forEach((field) => { // biome-ignore lint/suspicious/noImplicitAnyLet: let type; switch (field.kind) { case 'scalar': type = this.mapScalarType(field.type) as InputType; break; case 'enum': type = this.getEnum(field.type); break; case 'object': case 'unsupported': break; default: throw new Error(`Unknown field kind ${field.kind}`); } if (!type) { return; } fields[field.name] = type; }); return fields; }) as never, }) as InputObjectRef< Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['WhereUnique'] >; }); } getOrderBy( modelName: Name, ) { const name = `${modelName}OrderBy`; return this.getRef(modelName, name, () => { const model = getModel(modelName, this.builder); return this.builder.prismaOrderBy(modelName, { name, fields: () => { const fields: Record | boolean> = {}; model.fields.forEach((field) => { // biome-ignore lint/suspicious/noImplicitAnyLet: let type; switch (field.kind) { case 'scalar': case 'enum': type = true; break; case 'object': type = this.getOrderBy(field.type as Name); break; case 'unsupported': break; default: throw new Error(`Unknown field kind ${field.kind}`); } if (type) { fields[field.name] = type; } }); return fields as {}; }, }); }); } getCreateInput( modelName: Name, without?: string[], ) { const withoutName = (without ?? []) .map((name) => `Without${capitalize(name)}`) .join(''); const fullName = `${modelName}Create${withoutName}Input`; return this.getRef(modelName, fullName, () => { const model = getModel(modelName, this.builder); return this.builder.prismaCreate(modelName, { name: fullName, fields: (() => { const fields: Record> = {}; const withoutFields = model.fields.filter((field) => without?.includes(field.name), ); const relationIds = model.fields.flatMap( (field) => field.relationFromFields ?? [], ); model.fields .filter( (field) => !withoutFields.some( (f) => f.name === field.name || f.relationFromFields?.includes(field.name), ) && !relationIds.includes(field.name), ) .forEach((field) => { // biome-ignore lint/suspicious/noImplicitAnyLet: let type; switch (field.kind) { case 'scalar': type = this.mapScalarType(field.type) as InputType; break; case 'enum': type = this.getEnum(field.type); break; case 'object': type = this.getCreateRelationInput(modelName, field.name); break; case 'unsupported': break; default: throw new Error(`Unknown field kind ${field.kind}`); } if (type) { fields[field.name] = type; } }); return fields; }) as never, }) as InputObjectRef< Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Create'] >; }); } getCreateRelationInput< Name extends string & keyof Types['PrismaTypes'], Relation extends Model['RelationName'], Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes ? Types['PrismaTypes'][Name] : never, >(modelName: Name, relation: Relation) { return this.getRef( `${modelName}${capitalize(relation)}`, 'CreateRelationInput', () => { const model = getModel(modelName, this.builder); return this.builder.prismaCreateRelation(modelName, relation, { fields: () => { const relationField = model.fields.find( (field) => field.name === relation, )!; const relatedModel = getModel(relationField.type, this.builder); const relatedFieldName = relatedModel.fields.find( (field) => field.relationName === relationField.relationName, )!; return { create: this.getCreateInput(relationField.type as Name, [ relatedFieldName.name, ]), connect: this.getWhereUnique(relationField.type as Name), }; }, } as never) as InputObjectRef< Types, NonNullable >; }, ); } getCreateManyInput( modelName: Name, without?: string[], ) { const withoutName = (without ?? []) .map((name) => `Without${capitalize(name)}`) .join(''); const fullName = `${modelName}Create${withoutName}Input`; return this.getRef(modelName, fullName, () => { const model = getModel(modelName, this.builder); return this.builder.prismaCreateMany(modelName, { name: fullName, fields: (() => { const fields: Record> = {}; const withoutFields = model.fields.filter((field) => without?.includes(field.name), ); const relationIds = model.fields.flatMap( (field) => field.relationFromFields ?? [], ); model.fields .filter( (field) => !withoutFields.some( (f) => f.name === field.name || f.relationFromFields?.includes(field.name), ) && !relationIds.includes(field.name), ) .forEach((field) => { // biome-ignore lint/suspicious/noImplicitAnyLet: let type; switch (field.kind) { case 'scalar': type = this.mapScalarType(field.type) as InputType; break; case 'enum': type = this.getEnum(field.type); break; case 'unsupported': break; default: throw new Error(`Unknown field kind ${field.kind}`); } if (type) { fields[field.name] = type; } }); return fields; }) as never, }) as InputObjectRef< Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Create'] >; }); } getUpdateInput( modelName: Name, without?: string[], ) { const withoutName = (without ?? []) .map((name) => `Without${capitalize(name)}`) .join(''); const fullName = `${modelName}Update${withoutName}Input`; return this.getRef(modelName, fullName, () => { const model = getModel(modelName, this.builder); return this.builder.prismaUpdate(modelName, { name: fullName, fields: (() => { const fields: Record> = {}; const withoutFields = model.fields.filter((field) => without?.includes(field.name), ); const relationIds = model.fields.flatMap( (field) => field.relationFromFields ?? [], ); model.fields .filter( (field) => !withoutFields.some( (f) => f.name === field.name || f.relationFromFields?.includes(field.name), ) && !relationIds.includes(field.name), ) .forEach((field) => { // biome-ignore lint/suspicious/noImplicitAnyLet: let type; switch (field.kind) { case 'scalar': type = this.mapScalarType(field.type) as InputType; break; case 'enum': type = this.getEnum(field.type); break; case 'object': type = this.getUpdateRelationInput(modelName, field.name); break; case 'unsupported': break; default: throw new Error(`Unknown field kind ${field.kind}`); } if (type) { fields[field.name] = type; } }); return fields; }) as never, }) as InputObjectRef< Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Update'] >; }); } getUpdateRelationInput< Name extends string & keyof Types['PrismaTypes'], Relation extends Model['RelationName'], Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes ? Types['PrismaTypes'][Name] : never, >(modelName: Name, relation: Relation) { return this.getRef( `${modelName}${capitalize(relation)}`, 'UpdateRelationInput', () => { const model = getModel(modelName, this.builder); return this.builder.prismaUpdateRelation(modelName, relation, { fields: () => { const relationField = model.fields.find( (field) => field.name === relation, )!; const relatedModel = getModel(relationField.type, this.builder); const relatedFieldName = relatedModel.fields.find( (field) => field.relationName === relationField.relationName, )!.name; if (relationField.isList) { return { create: this.getCreateInput(relationField.type as Name, [ relatedFieldName, ]), createMany: { skipDuplicates: 'Boolean', data: this.getCreateInput(relationField.type as Name, [ relatedFieldName, ]), }, set: this.getWhereUnique(relationField.type as Name), disconnect: this.getWhereUnique(relationField.type as Name), delete: this.getWhereUnique(relationField.type as Name), connect: this.getWhereUnique(relationField.type as Name), update: { where: this.getWhereUnique(relationField.type as Name), data: this.getUpdateInput(relationField.type as Name, [ relatedFieldName, ]), }, updateMany: { where: this.getWhere(relationField.type as Name, [ relatedFieldName, ]), data: this.getUpdateInput(relationField.type as Name, [ relatedFieldName, ]), }, deleteMany: this.getWhere(relationField.type as Name, [ relatedFieldName, ]), }; } return { create: this.getCreateInput(relationField.type as Name, [ relatedFieldName, ]), update: this.getUpdateInput(relationField.type as Name, [ relatedFieldName, ]), connect: this.getWhereUnique(relationField.type as Name), disconnect: relationField.isRequired ? undefined : 'Boolean', delete: relationField.isRequired ? undefined : 'Boolean', }; }, } as never) as InputObjectRef< Types, NonNullable >; }, ); } private getFilter(type: InputType) { return this.getRef(type, `${String(type)}Filter`, () => { const ops: FilterOps[] = [...filterOps]; if (type === 'String') { ops.push(...stringFilterOps); } if (sortableTypes.includes(type as never)) { ops.push(...sortableFilterProps); } return this.builder.prismaFilter(type, { ops, }); }); } private getScalarListFilter(type: InputType) { return this.getRef(type, `${String(type)}ListFilter`, () => this.builder.prismaScalarListFilter(type, { ops: scalarListOps, }), ); } private getListFilter(type: InputType) { return this.getRef(type, `${String(type)}ListFilter`, () => this.builder.prismaListFilter(type, { ops: listOps, }), ); } private getEnum(name: string) { if (!this.enumRefs.has(name)) { const enumRef = this.builder.enumType( (Prisma as unknown as Record)[name], { name, }, ); this.enumRefs.set(name, enumRef); } return this.enumRefs.get(name)!; } private mapScalarType(type: string) { switch (type) { case 'String': case 'Boolean': case 'Int': case 'Float': case 'DateTime': case 'Json': return type; default: return null; } } private getRef>( key: InputType | string, name: string, create: () => T, ): T { if (!this.refCache.has(key)) { this.refCache.set(key, new Map()); } const cache = this.refCache.get(key)!; if (cache.has(name)) { return cache.get(name)! as T; } const ref = new InputObjectRef(name); cache.set(name, ref); this.builder.configStore.associateParamWithRef(ref, create()); return ref as T; } } function capitalize(str: string) { return str[0].toUpperCase() + str.slice(1); }