From f53d629ff796a98bec764ea20639575bf4ad32a0 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 14 Apr 2024 00:08:58 +0800 Subject: [PATCH 1/3] fix: properly set fields with default auth value to optional in create input --- .../src/plugins/enhancer/enhance/index.ts | 85 +++++++++++++++---- .../src/plugins/prisma/schema-generator.ts | 17 ---- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 108e2d17d..b4a333822 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -6,7 +6,9 @@ import { getAuthModel, getDMMF, getDataModels, + getForeignKeyFields, getPrismaClientImportSpec, + hasAttribute, isDelegateModel, type DMMF, type PluginOptions, @@ -245,22 +247,17 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara ]); }); + // transform index.d.ts and save it into a new file (better perf than in-line editing) + const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { overwrite: true, }); - - if (delegateInfo.length > 0) { - // transform types for delegated models - this.transformDelegate(sf, sfNew, delegateInfo); - sfNew.formatText(); - } else { - // just copy - sfNew.replaceWithText(sf.getFullText()); - } + this.transform(sf, sfNew, delegateInfo); + sfNew.formatText(); await sfNew.save(); } - private transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { + private transform(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { // copy toplevel imports sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); @@ -379,22 +376,74 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara // remove aux fields source = this.removeAuxFieldsFromTypeAlias(typeAlias, source); - // remove discriminator field from concrete input types - source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); + if (delegateInfo.length > 0) { + // remove discriminator field from concrete input types + source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); + + // remove create/connectOrCreate/upsert fields from delegate's input types + source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); - // remove create/connectOrCreate/upsert fields from delegate's input types - source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); + // remove delegate fields from nested mutation input types + source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); - // remove delegate fields from nested mutation input types - source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); + // fix delegate payload union type + source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); + } - // fix delegate payload union type - source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); + // fix the optionality of input args for fields with `auth()` in `@default` + source = this.fixFieldsWithDefaultAuth(typeAlias, source); structure.type = source; return structure; } + private fixFieldsWithDefaultAuth(typeAlias: TypeAliasDeclaration, source: string) { + const modelsWithDefaultAuth = this.model.declarations.filter( + (d): d is DataModel => + isDataModel(d) && d.fields.some((f) => !f.type.optional && f.attributes.some(isDefaultWithAuth)) + ); + if (modelsWithDefaultAuth.length === 0) { + return source; + } + + // set fields optional for create input types + const typeName = typeAlias.getName(); + const createInputRegex = new RegExp( + `(${modelsWithDefaultAuth.map((model) => model.name).join('|')})(Unchecked)?Create.*Input` + ); + const match = typeName.match(createInputRegex); + if (!match) { + return source; + } + + const model = modelsWithDefaultAuth.find((model) => model.name === match[1]); + if (!model) { + return source; + } + + const fieldsWithDefaultAuth = model.fields.filter( + (f) => !f.type.optional && f.attributes.some(isDefaultWithAuth) + ); + fieldsWithDefaultAuth.forEach((field) => { + // mark the field optional + const fieldDef = this.findNamedProperty(typeAlias, field.name); + if (fieldDef) { + source = source.replace(`${field.name}:`, `${field.name}?:`); + } + }); + + const relationFields = model.fields.filter((f) => !f.type.optional && hasAttribute(f, '@relation')); + relationFields.forEach((field) => { + // if the relation field's all fk fields are optional or have default value, make the relation optional + const fkFields = getForeignKeyFields(field); + if (fkFields.every((fk) => fk.type.optional || hasAttribute(fk, '@default'))) { + source = source.replace(`${field.name}:`, `${field.name}?:`); + } + }); + + return source; + } + private fixDelegatePayloadType(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) { // change the type of `$Payload` type of delegate model to a union of concrete types const typeName = typeAlias.getName(); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 649362eff..620e2deba 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -587,23 +587,6 @@ export class PrismaSchemaGenerator { const type = new ModelFieldType(fieldType, field.type.array, field.type.optional); - if (this.mode === 'logical') { - if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { - // field has `@default` with `auth()`, it should be set optional, and the - // default value setting is handled outside Prisma - type.optional = true; - } - - if (isRelationshipField(field)) { - // if foreign key field has `@default` with `auth()`, the relation - // field should be set optional - const foreignKeyFields = getForeignKeyFields(field); - if (foreignKeyFields.some((fkField) => fkField.attributes.some((attr) => isDefaultWithAuth(attr)))) { - type.optional = true; - } - } - } - const attributes = field.attributes .filter((attr) => this.isPrismaAttribute(attr)) // `@default` with `auth()` is handled outside Prisma From cc76ee007b87c059ce9d0eac8a1fde39efb5fd47 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 14 Apr 2024 00:15:08 +0800 Subject: [PATCH 2/3] fix build --- packages/schema/src/plugins/prisma/schema-generator.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 620e2deba..24db72b01 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -36,12 +36,10 @@ import { getAttribute, getAttributeArg, getAttributeArgLiteral, - getForeignKeyFields, getLiteral, getPrismaVersion, isDelegateModel, isIdField, - isRelationshipField, PluginError, PluginOptions, resolved, From c648b9d5a33c0c6ec02753493059bc21023749eb Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 14 Apr 2024 00:52:17 +0800 Subject: [PATCH 3/3] refactored fix --- .../src/plugins/enhancer/enhance/index.ts | 83 ++++--------------- .../src/plugins/prisma/schema-generator.ts | 28 ++++++- 2 files changed, 45 insertions(+), 66 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index b4a333822..0c630e28c 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -6,9 +6,7 @@ import { getAuthModel, getDMMF, getDataModels, - getForeignKeyFields, getPrismaClientImportSpec, - hasAttribute, isDelegateModel, type DMMF, type PluginOptions, @@ -252,12 +250,19 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { overwrite: true, }); - this.transform(sf, sfNew, delegateInfo); - sfNew.formatText(); + + if (delegateInfo.length > 0) { + // transform types for delegated models + this.transformDelegate(sf, sfNew, delegateInfo); + sfNew.formatText(); + } else { + // just copy + sfNew.replaceWithText(sf.getFullText()); + } await sfNew.save(); } - private transform(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { + private transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { // copy toplevel imports sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); @@ -376,74 +381,22 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara // remove aux fields source = this.removeAuxFieldsFromTypeAlias(typeAlias, source); - if (delegateInfo.length > 0) { - // remove discriminator field from concrete input types - source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); - - // remove create/connectOrCreate/upsert fields from delegate's input types - source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); + // remove discriminator field from concrete input types + source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); - // remove delegate fields from nested mutation input types - source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); + // remove create/connectOrCreate/upsert fields from delegate's input types + source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); - // fix delegate payload union type - source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); - } + // remove delegate fields from nested mutation input types + source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); - // fix the optionality of input args for fields with `auth()` in `@default` - source = this.fixFieldsWithDefaultAuth(typeAlias, source); + // fix delegate payload union type + source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); structure.type = source; return structure; } - private fixFieldsWithDefaultAuth(typeAlias: TypeAliasDeclaration, source: string) { - const modelsWithDefaultAuth = this.model.declarations.filter( - (d): d is DataModel => - isDataModel(d) && d.fields.some((f) => !f.type.optional && f.attributes.some(isDefaultWithAuth)) - ); - if (modelsWithDefaultAuth.length === 0) { - return source; - } - - // set fields optional for create input types - const typeName = typeAlias.getName(); - const createInputRegex = new RegExp( - `(${modelsWithDefaultAuth.map((model) => model.name).join('|')})(Unchecked)?Create.*Input` - ); - const match = typeName.match(createInputRegex); - if (!match) { - return source; - } - - const model = modelsWithDefaultAuth.find((model) => model.name === match[1]); - if (!model) { - return source; - } - - const fieldsWithDefaultAuth = model.fields.filter( - (f) => !f.type.optional && f.attributes.some(isDefaultWithAuth) - ); - fieldsWithDefaultAuth.forEach((field) => { - // mark the field optional - const fieldDef = this.findNamedProperty(typeAlias, field.name); - if (fieldDef) { - source = source.replace(`${field.name}:`, `${field.name}?:`); - } - }); - - const relationFields = model.fields.filter((f) => !f.type.optional && hasAttribute(f, '@relation')); - relationFields.forEach((field) => { - // if the relation field's all fk fields are optional or have default value, make the relation optional - const fkFields = getForeignKeyFields(field); - if (fkFields.every((fk) => fk.type.optional || hasAttribute(fk, '@default'))) { - source = source.replace(`${field.name}:`, `${field.name}?:`); - } - }); - - return source; - } - private fixDelegatePayloadType(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) { // change the type of `$Payload` type of delegate model to a union of concrete types const typeName = typeAlias.getName(); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 24db72b01..d7a62f0d0 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -28,7 +28,7 @@ import { NumberLiteral, StringLiteral, } from '@zenstackhq/language/ast'; -import { match } from 'ts-pattern'; +import { match, P } from 'ts-pattern'; import { getIdFields } from '../../utils/ast-utils'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; @@ -57,6 +57,7 @@ import { execPackage } from '../../utils/exec-utils'; import { isDefaultWithAuth } from '../enhancer/enhancer-utils'; import { AttributeArgValue, + ModelField, ModelFieldType, AttributeArg as PrismaAttributeArg, AttributeArgValue as PrismaAttributeArgValue, @@ -607,10 +608,35 @@ export class PrismaSchemaGenerator { const result = model.addField(field.name, type, attributes, documentations, addToFront); + if (this.mode === 'logical') { + if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { + // field has `@default` with `auth()`, turn it into a dummy default value, and the + // real default value setting is handled outside Prisma + this.setDummyDefault(result, field); + } + } + // user defined comments pass-through field.comments.forEach((c) => result.addComment(c)); } + private setDummyDefault(result: ModelField, field: DataModelField) { + const dummyDefaultValue = match(field.type.type) + .with('String', () => new AttributeArgValue('String', '')) + .with(P.union('Int', 'BigInt', 'Float', 'Decimal'), () => new AttributeArgValue('Number', '0')) + .with('Boolean', () => new AttributeArgValue('Boolean', 'false')) + .with('DateTime', () => new AttributeArgValue('FunctionCall', new PrismaFunctionCall('now'))) + .with('Json', () => new AttributeArgValue('String', '{}')) + .with('Bytes', () => new AttributeArgValue('String', '')) + .otherwise(() => { + throw new PluginError(name, `Unsupported field type with default value: ${field.type.type}`); + }); + + result.attributes.push( + new PrismaFieldAttribute('@default', [new PrismaAttributeArg(undefined, dummyDefaultValue)]) + ); + } + private isInheritedFromDelegate(field: DataModelField) { return field.$inheritedFrom && isDelegateModel(field.$inheritedFrom); }