diff --git a/package.json b/package.json index cdaf1a920..b0cb830c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.8.1", + "version": "2.9.0", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index 75dd964c5..79da788cc 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -4,6 +4,18 @@ ### Added +- Support for using `@@validate` attribute inside type declarations. + +## 2.8.1 + +### Fixed + +- Wrong validation errors when using strongly typed JSON fields in a multi-file schema setup. + +## 2.8.0 + +### Added + - Type declaration support. ## 2.7.0 diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 442557a34..cde13a348 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.8.1" +version = "2.9.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 389060d74..67a5d32c1 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.8.1", + "version": "2.9.0", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index ca9b3cc89..94d978d7a 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.8.1", + "version": "2.9.0", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 1705f019b..32fa2515d 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -70,7 +70,15 @@ export function isLiteralExpr(item: unknown): item is LiteralExpr { return reflection.isInstance(item, LiteralExpr); } -export type ReferenceTarget = DataModelField | EnumField | FunctionParam; +export type MemberAccessTarget = DataModelField | TypeDefField; + +export const MemberAccessTarget = 'MemberAccessTarget'; + +export function isMemberAccessTarget(item: unknown): item is MemberAccessTarget { + return reflection.isInstance(item, MemberAccessTarget); +} + +export type ReferenceTarget = DataModelField | EnumField | FunctionParam | TypeDefField; export const ReferenceTarget = 'ReferenceTarget'; @@ -285,7 +293,7 @@ export function isDataModel(item: unknown): item is DataModel { } export interface DataModelAttribute extends AstNode { - readonly $container: DataModel | Enum; + readonly $container: DataModel | Enum | TypeDef; readonly $type: 'DataModelAttribute'; args: Array decl: Reference @@ -298,7 +306,7 @@ export function isDataModelAttribute(item: unknown): item is DataModelAttribute } export interface DataModelField extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl; + readonly $container: DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'DataModelField'; attributes: Array comments: Array @@ -370,7 +378,7 @@ export function isEnum(item: unknown): item is Enum { } export interface EnumField extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl; + readonly $container: DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'EnumField'; attributes: Array comments: Array @@ -413,7 +421,7 @@ export function isFunctionDecl(item: unknown): item is FunctionDecl { } export interface FunctionParam extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl; + readonly $container: DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'FunctionParam'; name: RegularID optional: boolean @@ -482,7 +490,7 @@ export function isInvocationExpr(item: unknown): item is InvocationExpr { export interface MemberAccessExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'MemberAccessExpr'; - member: Reference + member: Reference operand: Expression } @@ -631,6 +639,7 @@ export function isThisExpr(item: unknown): item is ThisExpr { export interface TypeDef extends AstNode { readonly $container: Model; readonly $type: 'TypeDef'; + attributes: Array comments: Array fields: Array name: RegularID @@ -643,7 +652,7 @@ export function isTypeDef(item: unknown): item is TypeDef { } export interface TypeDefField extends AstNode { - readonly $container: TypeDef; + readonly $container: DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'TypeDefField'; attributes: Array comments: Array @@ -730,6 +739,7 @@ export type ZModelAstType = { InvocationExpr: InvocationExpr LiteralExpr: LiteralExpr MemberAccessExpr: MemberAccessExpr + MemberAccessTarget: MemberAccessTarget Model: Model ModelImport: ModelImport NullExpr: NullExpr @@ -754,7 +764,7 @@ export type ZModelAstType = { export class ZModelAstReflection extends AbstractAstReflection { getAllTypes(): string[] { - return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'ConfigArrayExpr', 'ConfigExpr', 'ConfigField', 'ConfigInvocationArg', 'ConfigInvocationExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'TypeDef', 'TypeDefField', 'TypeDefFieldType', 'TypeDefFieldTypes', 'UnaryExpr', 'UnsupportedFieldType']; + return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'ConfigArrayExpr', 'ConfigExpr', 'ConfigField', 'ConfigInvocationArg', 'ConfigInvocationExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'MemberAccessTarget', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'TypeDef', 'TypeDefField', 'TypeDefFieldType', 'TypeDefFieldTypes', 'UnaryExpr', 'UnsupportedFieldType']; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { @@ -788,14 +798,17 @@ export class ZModelAstReflection extends AbstractAstReflection { return this.isSubtype(AbstractDeclaration, supertype) || this.isSubtype(TypeDeclaration, supertype); } case DataModelField: - case EnumField: - case FunctionParam: { - return this.isSubtype(ReferenceTarget, supertype); + case TypeDefField: { + return this.isSubtype(MemberAccessTarget, supertype) || this.isSubtype(ReferenceTarget, supertype); } case Enum: case TypeDef: { return this.isSubtype(AbstractDeclaration, supertype) || this.isSubtype(TypeDeclaration, supertype) || this.isSubtype(TypeDefFieldTypes, supertype); } + case EnumField: + case FunctionParam: { + return this.isSubtype(ReferenceTarget, supertype); + } case InvocationExpr: case LiteralExpr: { return this.isSubtype(ConfigExpr, supertype) || this.isSubtype(Expression, supertype); @@ -826,7 +839,7 @@ export class ZModelAstReflection extends AbstractAstReflection { return FunctionDecl; } case 'MemberAccessExpr:member': { - return DataModelField; + return MemberAccessTarget; } case 'ReferenceExpr:target': { return ReferenceTarget; @@ -1055,6 +1068,7 @@ export class ZModelAstReflection extends AbstractAstReflection { return { name: 'TypeDef', mandatory: [ + { name: 'attributes', type: 'array' }, { name: 'comments', type: 'array' }, { name: 'fields', type: 'array' } ] diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 43beb12a2..c6a0113b4 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -1301,7 +1301,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@38" + "$ref": "#/types@1" }, "deprecatedSyntax": false } @@ -2165,7 +2165,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@2" + "$ref": "#/types@3" }, "terminal": { "$type": "RuleCall", @@ -2257,16 +2257,33 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "value": "{" }, { - "$type": "Assignment", - "feature": "fields", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@41" + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "fields", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@41" + }, + "arguments": [] + } }, - "arguments": [] - }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@55" + }, + "arguments": [] + } + } + ], "cardinality": "*" }, { @@ -2375,7 +2392,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@1" + "$ref": "#/types@2" }, "terminal": { "$type": "RuleCall", @@ -2827,7 +2844,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@2" + "$ref": "#/types@3" }, "terminal": { "$type": "RuleCall", @@ -3255,7 +3272,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@2" + "$ref": "#/types@3" }, "terminal": { "$type": "RuleCall", @@ -3829,6 +3846,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$ref": "#/rules@38" } }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@41" + } + }, { "$type": "SimpleType", "typeRef": { @@ -3838,6 +3861,27 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel ] } }, + { + "$type": "Type", + "name": "MemberAccessTarget", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@38" + } + }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@41" + } + } + ] + } + }, { "$type": "Type", "name": "TypeDefFieldTypes", diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index ef5a1f883..86d28276b 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -66,7 +66,7 @@ ConfigArrayExpr: ConfigExpr: LiteralExpr | InvocationExpr | ConfigArrayExpr; -type ReferenceTarget = FunctionParam | DataModelField | EnumField; +type ReferenceTarget = FunctionParam | DataModelField | TypeDefField | EnumField; ThisExpr: value='this'; @@ -94,18 +94,20 @@ FieldInitializer: InvocationExpr: function=[FunctionDecl] '(' ArgumentList? ')'; -// binary operator precedence follow Javascript's rules: -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table +type MemberAccessTarget = DataModelField | TypeDefField; MemberAccessExpr infers Expression: PrimaryExpr ( {infer MemberAccessExpr.operand=current} - ('.' member=[DataModelField]) + ('.' member=[MemberAccessTarget]) )*; UnaryExpr: operator=('!') operand=MemberAccessExpr; +// binary operator precedence follow Javascript's rules: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table + CollectionPredicateExpr infers Expression: MemberAccessExpr ( {infer BinaryExpr.left=current} @@ -179,10 +181,12 @@ DataModelField: DataModelFieldType: (type=BuiltinType | unsupported=UnsupportedFieldType | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?; +// TODO: unify TypeDef and abstract DataModel TypeDef: (comments+=TRIPLE_SLASH_COMMENT)* 'type' name=RegularID '{' ( - fields+=TypeDefField + fields+=TypeDefField | + attributes+=DataModelAttribute )* '}'; @@ -245,6 +249,7 @@ type TypeDeclaration = DataModel | TypeDef | Enum; DataModelFieldAttribute: decl=[Attribute:FIELD_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?; +// TODO: need rename since it's for both DataModel and TypeDef DataModelAttribute: TRIPLE_SLASH_COMMENT* decl=[Attribute:MODEL_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?; diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 1f74d793b..c3958bcb7 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "2.8.1", + "version": "2.9.0", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index a55d40ac1..bb8fc4d59 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "2.8.1", + "version": "2.9.0", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 8927198cc..86ed9736a 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -7,10 +7,12 @@ import { isForeignKeyField, isIdField, isRelationshipField, + PluginError, + PluginOptions, requireOption, resolvePath, } from '@zenstackhq/sdk'; -import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; +import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum, Model } from '@zenstackhq/sdk/ast'; import type { DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; @@ -18,7 +20,7 @@ import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import path from 'path'; import pluralize from 'pluralize'; import invariant from 'tiny-invariant'; -import { P, match } from 'ts-pattern'; +import { match, P } from 'ts-pattern'; import YAML from 'yaml'; import { name } from '.'; import { OpenAPIGeneratorBase } from './generator-base'; @@ -32,6 +34,14 @@ type Policies = ReturnType; export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private warnings: string[] = []; + constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) { + super(model, options, dmmf); + + if (this.options.omitInputDetails !== undefined) { + throw new PluginError(name, '"omitInputDetails" option is not supported for "rest" flavor'); + } + } + generate() { let output = requireOption(this.options, 'output', name); output = resolvePath(output, this.options); @@ -858,6 +868,10 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { const required: string[] = []; for (const field of fields) { + if (isForeignKeyField(field) && mode !== 'read') { + // foreign keys are not exposed as attributes + continue; + } if (isRelationshipField(field)) { let relType: string; if (mode === 'create' || mode === 'update') { diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index c524d9cd1..edd4bc9a6 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,7 +1,7 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator -import { PluginError, analyzePolicies, requireOption, resolvePath } from '@zenstackhq/sdk'; -import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; +import { PluginError, PluginOptions, analyzePolicies, requireOption, resolvePath } from '@zenstackhq/sdk'; +import { DataModel, Model, isDataModel } from '@zenstackhq/sdk/ast'; import { AggregateOperationSupport, addMissingInputObjectTypesForAggregate, @@ -23,6 +23,8 @@ import { name } from '.'; import { OpenAPIGeneratorBase } from './generator-base'; import { getModelResourceMeta } from './meta'; +const ANY_OBJECT = '_AnyObject'; + /** * Generates OpenAPI specification. */ @@ -32,6 +34,16 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { private usedComponents: Set = new Set(); private aggregateOperationSupport: AggregateOperationSupport; private warnings: string[] = []; + private omitInputDetails: boolean; + + constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) { + super(model, options, dmmf); + + this.omitInputDetails = this.getOption('omitInputDetails', false); + if (this.omitInputDetails !== undefined && typeof this.omitInputDetails !== 'boolean') { + throw new PluginError(name, `Invalid option value for "omitInputDetails", boolean expected`); + } + } generate() { let output = requireOption(this.options, 'output', name); @@ -151,9 +163,9 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['data'], properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - data: this.ref(`${modelName}CreateInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + data: this.omittableRef(`${modelName}CreateInput`), meta: this.ref('_Meta'), }, }, @@ -177,8 +189,8 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { required: ['data'], properties: { data: this.oneOf( - this.ref(`${modelName}CreateManyInput`), - this.array(this.ref(`${modelName}CreateManyInput`)) + this.omittableRef(`${modelName}CreateManyInput`), + this.array(this.omittableRef(`${modelName}CreateManyInput`)) ), skipDuplicates: { type: 'boolean', @@ -207,9 +219,9 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['where'], properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - where: this.ref(`${modelName}WhereUniqueInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + where: this.omittableRef(`${modelName}WhereUniqueInput`), meta: this.ref('_Meta'), }, }, @@ -230,9 +242,9 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { { type: 'object', properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - where: this.ref(`${modelName}WhereInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + where: this.omittableRef(`${modelName}WhereInput`), meta: this.ref('_Meta'), }, }, @@ -253,9 +265,9 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { { type: 'object', properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - where: this.ref(`${modelName}WhereInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + where: this.omittableRef(`${modelName}WhereInput`), meta: this.ref('_Meta'), }, }, @@ -277,10 +289,10 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['where', 'data'], properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - where: this.ref(`${modelName}WhereUniqueInput`), - data: this.ref(`${modelName}UpdateInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + where: this.omittableRef(`${modelName}WhereUniqueInput`), + data: this.omittableRef(`${modelName}UpdateInput`), meta: this.ref('_Meta'), }, }, @@ -302,8 +314,8 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['data'], properties: { - where: this.ref(`${modelName}WhereInput`), - data: this.ref(`${modelName}UpdateManyMutationInput`), + where: this.omittableRef(`${modelName}WhereInput`), + data: this.omittableRef(`${modelName}UpdateManyMutationInput`), meta: this.ref('_Meta'), }, }, @@ -325,11 +337,11 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['create', 'update', 'where'], properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - where: this.ref(`${modelName}WhereUniqueInput`), - create: this.ref(`${modelName}CreateInput`), - update: this.ref(`${modelName}UpdateInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + where: this.omittableRef(`${modelName}WhereUniqueInput`), + create: this.omittableRef(`${modelName}CreateInput`), + update: this.omittableRef(`${modelName}UpdateInput`), meta: this.ref('_Meta'), }, }, @@ -351,9 +363,9 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['where'], properties: { - select: this.ref(`${modelName}Select`), - include: hasRelation ? this.ref(`${modelName}Include`) : undefined, - where: this.ref(`${modelName}WhereUniqueInput`), + select: this.omittableRef(`${modelName}Select`), + include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, + where: this.omittableRef(`${modelName}WhereUniqueInput`), meta: this.ref('_Meta'), }, }, @@ -374,7 +386,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { { type: 'object', properties: { - where: this.ref(`${modelName}WhereInput`), + where: this.omittableRef(`${modelName}WhereInput`), meta: this.ref('_Meta'), }, }, @@ -395,8 +407,8 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { { type: 'object', properties: { - select: this.ref(`${modelName}Select`), - where: this.ref(`${modelName}WhereInput`), + select: this.omittableRef(`${modelName}Select`), + where: this.omittableRef(`${modelName}WhereInput`), meta: this.ref('_Meta'), }, }, @@ -425,9 +437,9 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { { type: 'object', properties: { - where: this.ref(`${modelName}WhereInput`), - orderBy: this.ref(orderByWithRelationInput), - cursor: this.ref(`${modelName}WhereUniqueInput`), + where: this.omittableRef(`${modelName}WhereInput`), + orderBy: this.omittableRef(orderByWithRelationInput), + cursor: this.omittableRef(`${modelName}WhereUniqueInput`), take: { type: 'integer' }, skip: { type: 'integer' }, ...this.aggregateFields(model), @@ -451,10 +463,10 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { { type: 'object', properties: { - where: this.ref(`${modelName}WhereInput`), - orderBy: this.ref(orderByWithRelationInput), - by: this.ref(`${modelName}ScalarFieldEnum`), - having: this.ref(`${modelName}ScalarWhereWithAggregatesInput`), + where: this.omittableRef(`${modelName}WhereInput`), + orderBy: this.omittableRef(orderByWithRelationInput), + by: this.omittableRef(`${modelName}ScalarFieldEnum`), + having: this.omittableRef(`${modelName}ScalarWhereWithAggregatesInput`), take: { type: 'integer' }, skip: { type: 'integer' }, ...this.aggregateFields(model), @@ -587,19 +599,19 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { const modelName = upperCaseFirst(model.name); if (supportedOps) { if (supportedOps.count) { - result._count = this.oneOf({ type: 'boolean' }, this.ref(`${modelName}CountAggregateInput`)); + result._count = this.oneOf({ type: 'boolean' }, this.omittableRef(`${modelName}CountAggregateInput`)); } if (supportedOps.min) { - result._min = this.ref(`${modelName}MinAggregateInput`); + result._min = this.omittableRef(`${modelName}MinAggregateInput`); } if (supportedOps.max) { - result._max = this.ref(`${modelName}MaxAggregateInput`); + result._max = this.omittableRef(`${modelName}MaxAggregateInput`); } if (supportedOps.sum) { - result._sum = this.ref(`${modelName}SumAggregateInput`); + result._sum = this.omittableRef(`${modelName}SumAggregateInput`); } if (supportedOps.avg) { - result._avg = this.ref(`${modelName}AvgAggregateInput`); + result._avg = this.omittableRef(`${modelName}AvgAggregateInput`); } } return result; @@ -617,6 +629,14 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { schemas, }; + if (this.omitInputDetails) { + // generate a catch-all object type + schemas[ANY_OBJECT] = { + type: 'object', + additionalProperties: true, + }; + } + // user-defined and built-in enums for (const _enum of [...(this.dmmf.schema.enumTypes.model ?? []), ...this.dmmf.schema.enumTypes.prisma]) { schemas[upperCaseFirst(_enum.name)] = this.generateEnumComponent(_enum); @@ -824,6 +844,14 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { return { $ref: `#/components/schemas/${upperCaseFirst(type)}`, description }; } + private omittableRef(type: string, rooted = true, description?: string): OAPI.ReferenceObject { + if (this.omitInputDetails) { + return this.ref(ANY_OBJECT); + } else { + return this.ref(type, rooted, description); + } + } + private response(schema: OAPI.SchemaObject): OAPI.SchemaObject { return { type: 'object', diff --git a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml index adb9ded12..0ea258018 100644 --- a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml @@ -2791,14 +2791,10 @@ components: type: string attributes: type: object - required: - - userId properties: image: type: string nullable: true - userId: - type: string relationships: type: object properties: @@ -2830,8 +2826,6 @@ components: image: type: string nullable: true - userId: - type: string relationships: type: object properties: @@ -2974,9 +2968,6 @@ components: format: date-time title: type: string - authorId: - type: string - nullable: true published: type: boolean viewCount: @@ -3022,9 +3013,6 @@ components: format: date-time title: type: string - authorId: - type: string - nullable: true published: type: boolean viewCount: diff --git a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml index f69536b30..9bd34467c 100644 --- a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml @@ -2797,15 +2797,11 @@ components: type: string attributes: type: object - required: - - userId properties: image: oneOf: - type: 'null' - type: string - userId: - type: string relationships: type: object properties: @@ -2838,8 +2834,6 @@ components: oneOf: - type: 'null' - type: string - userId: - type: string relationships: type: object properties: @@ -2984,10 +2978,6 @@ components: format: date-time title: type: string - authorId: - oneOf: - - type: 'null' - - type: string published: type: boolean viewCount: @@ -3034,10 +3024,6 @@ components: format: date-time title: type: string - authorId: - oneOf: - - type: 'null' - - type: string published: type: boolean viewCount: diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml new file mode 100644 index 000000000..dd683e9e3 --- /dev/null +++ b/packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml @@ -0,0 +1,2996 @@ +openapi: 3.0.0 +info: + title: ZenStack Generated API + version: 1.0.0 +tags: + - name: user + description: User operations + - name: profile + description: Profile operations + - name: post_Item + description: Post-related operations +components: + schemas: + _AnyObject: + type: object + additionalProperties: true + Role: + type: string + enum: + - USER + - ADMIN + User: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + posts: + type: array + items: + $ref: '#/components/schemas/Post_Item' + profile: + allOf: + - $ref: '#/components/schemas/Profile' + nullable: true + required: + - id + - createdAt + - updatedAt + - email + - role + Profile: + type: object + properties: + id: + type: string + image: + type: string + nullable: true + user: + $ref: '#/components/schemas/User' + userId: + type: string + required: + - id + - user + - userId + Post_Item: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + author: + allOf: + - $ref: '#/components/schemas/User' + nullable: true + authorId: + type: string + nullable: true + published: + type: boolean + viewCount: + type: integer + notes: + type: string + nullable: true + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + AggregateUser: + type: object + properties: + _count: + allOf: + - $ref: '#/components/schemas/UserCountAggregateOutputType' + nullable: true + _min: + allOf: + - $ref: '#/components/schemas/UserMinAggregateOutputType' + nullable: true + _max: + allOf: + - $ref: '#/components/schemas/UserMaxAggregateOutputType' + nullable: true + UserGroupByOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + _count: + allOf: + - $ref: '#/components/schemas/UserCountAggregateOutputType' + nullable: true + _min: + allOf: + - $ref: '#/components/schemas/UserMinAggregateOutputType' + nullable: true + _max: + allOf: + - $ref: '#/components/schemas/UserMaxAggregateOutputType' + nullable: true + required: + - id + - createdAt + - updatedAt + - email + - role + AggregateProfile: + type: object + properties: + _count: + allOf: + - $ref: '#/components/schemas/ProfileCountAggregateOutputType' + nullable: true + _min: + allOf: + - $ref: '#/components/schemas/ProfileMinAggregateOutputType' + nullable: true + _max: + allOf: + - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' + nullable: true + ProfileGroupByOutputType: + type: object + properties: + id: + type: string + image: + type: string + nullable: true + userId: + type: string + _count: + allOf: + - $ref: '#/components/schemas/ProfileCountAggregateOutputType' + nullable: true + _min: + allOf: + - $ref: '#/components/schemas/ProfileMinAggregateOutputType' + nullable: true + _max: + allOf: + - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' + nullable: true + required: + - id + - userId + AggregatePost_Item: + type: object + properties: + _count: + allOf: + - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' + nullable: true + _avg: + allOf: + - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' + nullable: true + _sum: + allOf: + - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' + nullable: true + _min: + allOf: + - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' + nullable: true + _max: + allOf: + - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' + nullable: true + Post_ItemGroupByOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + nullable: true + published: + type: boolean + viewCount: + type: integer + notes: + type: string + nullable: true + _count: + allOf: + - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' + nullable: true + _avg: + allOf: + - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' + nullable: true + _sum: + allOf: + - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' + nullable: true + _min: + allOf: + - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' + nullable: true + _max: + allOf: + - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' + nullable: true + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + UserCountAggregateOutputType: + type: object + properties: + id: + type: integer + createdAt: + type: integer + updatedAt: + type: integer + email: + type: integer + role: + type: integer + _all: + type: integer + required: + - id + - createdAt + - updatedAt + - email + - role + - _all + UserMinAggregateOutputType: + type: object + properties: + id: + type: string + nullable: true + createdAt: + type: string + format: date-time + nullable: true + updatedAt: + type: string + format: date-time + nullable: true + email: + type: string + nullable: true + role: + allOf: + - $ref: '#/components/schemas/Role' + nullable: true + UserMaxAggregateOutputType: + type: object + properties: + id: + type: string + nullable: true + createdAt: + type: string + format: date-time + nullable: true + updatedAt: + type: string + format: date-time + nullable: true + email: + type: string + nullable: true + role: + allOf: + - $ref: '#/components/schemas/Role' + nullable: true + ProfileCountAggregateOutputType: + type: object + properties: + id: + type: integer + image: + type: integer + userId: + type: integer + _all: + type: integer + required: + - id + - image + - userId + - _all + ProfileMinAggregateOutputType: + type: object + properties: + id: + type: string + nullable: true + image: + type: string + nullable: true + userId: + type: string + nullable: true + ProfileMaxAggregateOutputType: + type: object + properties: + id: + type: string + nullable: true + image: + type: string + nullable: true + userId: + type: string + nullable: true + Post_ItemCountAggregateOutputType: + type: object + properties: + id: + type: integer + createdAt: + type: integer + updatedAt: + type: integer + title: + type: integer + authorId: + type: integer + published: + type: integer + viewCount: + type: integer + notes: + type: integer + _all: + type: integer + required: + - id + - createdAt + - updatedAt + - title + - authorId + - published + - viewCount + - notes + - _all + Post_ItemAvgAggregateOutputType: + type: object + properties: + viewCount: + type: number + nullable: true + Post_ItemSumAggregateOutputType: + type: object + properties: + viewCount: + type: integer + nullable: true + Post_ItemMinAggregateOutputType: + type: object + properties: + id: + type: string + nullable: true + createdAt: + type: string + format: date-time + nullable: true + updatedAt: + type: string + format: date-time + nullable: true + title: + type: string + nullable: true + authorId: + type: string + nullable: true + published: + type: boolean + nullable: true + viewCount: + type: integer + nullable: true + notes: + type: string + nullable: true + Post_ItemMaxAggregateOutputType: + type: object + properties: + id: + type: string + nullable: true + createdAt: + type: string + format: date-time + nullable: true + updatedAt: + type: string + format: date-time + nullable: true + title: + type: string + nullable: true + authorId: + type: string + nullable: true + published: + type: boolean + nullable: true + viewCount: + type: integer + nullable: true + notes: + type: string + nullable: true + _Meta: + type: object + description: Meta information about the request or response + properties: + serialization: + description: Serialization metadata + additionalProperties: true + _Error: + type: object + required: + - error + properties: + error: + type: object + required: + - message + properties: + prisma: + type: boolean + description: Indicates if the error occurred during a Prisma call + rejectedByPolicy: + type: boolean + description: Indicates if the error was due to rejection by a policy + code: + type: string + description: Prisma error code. Only available when "prisma" field is true. + message: + type: string + description: Error message + reason: + type: string + description: Detailed error reason + zodErrors: + type: object + additionalProperties: true + description: Zod validation errors if the error is due to data validation + failure + additionalProperties: true + BatchPayload: + type: object + properties: + count: + type: integer + UserCreateArgs: + type: object + required: + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserCreateManyArgs: + type: object + required: + - data + properties: + data: + oneOf: + - $ref: '#/components/schemas/_AnyObject' + - type: array + items: + $ref: '#/components/schemas/_AnyObject' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. + meta: + $ref: '#/components/schemas/_Meta' + UserFindUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserFindManyArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserUpdateArgs: + type: object + required: + - where + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserUpdateManyArgs: + type: object + required: + - data + properties: + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserUpsertArgs: + type: object + required: + - create + - update + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + create: + $ref: '#/components/schemas/_AnyObject' + update: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserDeleteUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + cursor: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + by: + $ref: '#/components/schemas/_AnyObject' + having: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileCreateArgs: + type: object + required: + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileCreateManyArgs: + type: object + required: + - data + properties: + data: + oneOf: + - $ref: '#/components/schemas/_AnyObject' + - type: array + items: + $ref: '#/components/schemas/_AnyObject' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. + meta: + $ref: '#/components/schemas/_Meta' + ProfileFindUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileFindManyArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileUpdateArgs: + type: object + required: + - where + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileUpdateManyArgs: + type: object + required: + - data + properties: + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileUpsertArgs: + type: object + required: + - create + - update + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + create: + $ref: '#/components/schemas/_AnyObject' + update: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileDeleteUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + cursor: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + by: + $ref: '#/components/schemas/_AnyObject' + having: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemCreateArgs: + type: object + required: + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemCreateManyArgs: + type: object + required: + - data + properties: + data: + oneOf: + - $ref: '#/components/schemas/_AnyObject' + - type: array + items: + $ref: '#/components/schemas/_AnyObject' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemFindUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemUpdateArgs: + type: object + required: + - where + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemUpdateManyArgs: + type: object + required: + - data + properties: + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemUpsertArgs: + type: object + required: + - create + - update + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + create: + $ref: '#/components/schemas/_AnyObject' + update: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemDeleteUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + cursor: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + by: + $ref: '#/components/schemas/_AnyObject' + having: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + meta: + $ref: '#/components/schemas/_Meta' +paths: + /user/create: + post: + operationId: createUser + description: Create a new User + tags: + - user + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreateArgs' + /user/createMany: + post: + operationId: createManyUser + description: Create several User + tags: + - user + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreateManyArgs' + /user/findUnique: + get: + operationId: findUniqueUser + description: Find one unique User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/findFirst: + get: + operationId: findFirstUser + description: Find the first User matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindFirstArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/findMany: + get: + operationId: findManyUser + description: Find users matching the given conditions + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/update: + patch: + operationId: updateUser + description: Update a User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateArgs' + /user/updateMany: + patch: + operationId: updateManyUser + description: Update Users matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateManyArgs' + /user/upsert: + post: + operationId: upsertUser + description: Upsert a User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpsertArgs' + /user/dodelete: + put: + operationId: deleteUser + description: Delete a unique user + tags: + - delete + - user + summary: Delete a user yeah yeah + deprecated: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteUniqueArgs' + /user/deleteMany: + delete: + operationId: deleteManyUser + description: Delete Users matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/count: + get: + operationId: countUser + description: Find a list of User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + oneOf: + - type: integer + - $ref: '#/components/schemas/UserCountAggregateOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserCountArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/aggregate: + get: + operationId: aggregateUser + description: Aggregate Users + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AggregateUser' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserAggregateArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/groupBy: + get: + operationId: groupByUser + description: Group Users by fields + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserGroupByOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserGroupByArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/create: + post: + operationId: createProfile + description: Create a new Profile + tags: + - profile + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileCreateArgs' + /profile/createMany: + post: + operationId: createManyProfile + description: Create several Profile + tags: + - profile + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileCreateManyArgs' + /profile/findUnique: + get: + operationId: findUniqueProfile + description: Find one unique Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileFindUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/findFirst: + get: + operationId: findFirstProfile + description: Find the first Profile matching the given condition + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileFindFirstArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/findMany: + get: + operationId: findManyProfile + description: Find a list of Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileFindManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/update: + patch: + operationId: updateProfile + description: Update a Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileUpdateArgs' + /profile/updateMany: + patch: + operationId: updateManyProfile + description: Update Profiles matching the given condition + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileUpdateManyArgs' + /profile/upsert: + post: + operationId: upsertProfile + description: Upsert a Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileUpsertArgs' + /profile/delete: + delete: + operationId: deleteProfile + description: Delete one unique Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileDeleteUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/deleteMany: + delete: + operationId: deleteManyProfile + description: Delete Profiles matching the given condition + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileDeleteManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/count: + get: + operationId: countProfile + description: Find a list of Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + oneOf: + - type: integer + - $ref: '#/components/schemas/ProfileCountAggregateOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileCountArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/aggregate: + get: + operationId: aggregateProfile + description: Aggregate Profiles + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AggregateProfile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileAggregateArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/groupBy: + get: + operationId: groupByProfile + description: Group Profiles by fields + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/ProfileGroupByOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileGroupByArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/create: + post: + operationId: createPost_Item + description: Create a new Post_Item + tags: + - post_Item + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemCreateArgs' + /post_Item/createMany: + post: + operationId: createManyPost_Item + description: Create several Post_Item + tags: + - post_Item + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemCreateManyArgs' + /post_Item/findUnique: + get: + operationId: findUniquePost_Item + description: Find one unique Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemFindUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/findFirst: + get: + operationId: findFirstPost_Item + description: Find the first Post_Item matching the given condition + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemFindFirstArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/update: + patch: + operationId: updatePost_Item + description: Update a Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemUpdateArgs' + /post_Item/updateMany: + patch: + operationId: updateManyPost_Item + description: Update Post_Items matching the given condition + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemUpdateManyArgs' + /post_Item/upsert: + post: + operationId: upsertPost_Item + description: Upsert a Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemUpsertArgs' + /post_Item/delete: + delete: + operationId: deletePost_Item + description: Delete one unique Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemDeleteUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/deleteMany: + delete: + operationId: deleteManyPost_Item + description: Delete Post_Items matching the given condition + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemDeleteManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/count: + get: + operationId: countPost_Item + description: Find a list of Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + oneOf: + - type: integer + - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemCountArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/aggregate: + get: + operationId: aggregatePost_Item + description: Aggregate Post_Items + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AggregatePost_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemAggregateArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/groupBy: + get: + operationId: groupByPost_Item + description: Group Post_Items by fields + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Post_ItemGroupByOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemGroupByArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml new file mode 100644 index 000000000..fae21b204 --- /dev/null +++ b/packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml @@ -0,0 +1,3034 @@ +openapi: 3.1.0 +info: + title: ZenStack Generated API + version: 1.0.0 +tags: + - name: user + description: User operations + - name: profile + description: Profile operations + - name: post_Item + description: Post-related operations +components: + schemas: + _AnyObject: + type: object + additionalProperties: true + Role: + type: string + enum: + - USER + - ADMIN + User: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + posts: + type: array + items: + $ref: '#/components/schemas/Post_Item' + profile: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Profile' + required: + - id + - createdAt + - updatedAt + - email + - role + Profile: + type: object + properties: + id: + type: string + image: + oneOf: + - type: 'null' + - type: string + user: + $ref: '#/components/schemas/User' + userId: + type: string + required: + - id + - user + - userId + Post_Item: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + author: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/User' + authorId: + oneOf: + - type: 'null' + - type: string + published: + type: boolean + viewCount: + type: integer + notes: + oneOf: + - type: 'null' + - type: string + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + AggregateUser: + type: object + properties: + _count: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserCountAggregateOutputType' + _min: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserMinAggregateOutputType' + _max: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserMaxAggregateOutputType' + UserGroupByOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + _count: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserCountAggregateOutputType' + _min: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserMinAggregateOutputType' + _max: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserMaxAggregateOutputType' + required: + - id + - createdAt + - updatedAt + - email + - role + AggregateProfile: + type: object + properties: + _count: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/ProfileCountAggregateOutputType' + _min: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/ProfileMinAggregateOutputType' + _max: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' + ProfileGroupByOutputType: + type: object + properties: + id: + type: string + image: + oneOf: + - type: 'null' + - type: string + userId: + type: string + _count: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/ProfileCountAggregateOutputType' + _min: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/ProfileMinAggregateOutputType' + _max: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' + required: + - id + - userId + AggregatePost_Item: + type: object + properties: + _count: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' + _avg: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' + _sum: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' + _min: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' + _max: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' + Post_ItemGroupByOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + oneOf: + - type: 'null' + - type: string + published: + type: boolean + viewCount: + type: integer + notes: + oneOf: + - type: 'null' + - type: string + _count: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' + _avg: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' + _sum: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' + _min: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' + _max: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + UserCountAggregateOutputType: + type: object + properties: + id: + type: integer + createdAt: + type: integer + updatedAt: + type: integer + email: + type: integer + role: + type: integer + _all: + type: integer + required: + - id + - createdAt + - updatedAt + - email + - role + - _all + UserMinAggregateOutputType: + type: object + properties: + id: + oneOf: + - type: 'null' + - type: string + createdAt: + oneOf: + - type: 'null' + - type: string + format: date-time + updatedAt: + oneOf: + - type: 'null' + - type: string + format: date-time + email: + oneOf: + - type: 'null' + - type: string + role: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Role' + UserMaxAggregateOutputType: + type: object + properties: + id: + oneOf: + - type: 'null' + - type: string + createdAt: + oneOf: + - type: 'null' + - type: string + format: date-time + updatedAt: + oneOf: + - type: 'null' + - type: string + format: date-time + email: + oneOf: + - type: 'null' + - type: string + role: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Role' + ProfileCountAggregateOutputType: + type: object + properties: + id: + type: integer + image: + type: integer + userId: + type: integer + _all: + type: integer + required: + - id + - image + - userId + - _all + ProfileMinAggregateOutputType: + type: object + properties: + id: + oneOf: + - type: 'null' + - type: string + image: + oneOf: + - type: 'null' + - type: string + userId: + oneOf: + - type: 'null' + - type: string + ProfileMaxAggregateOutputType: + type: object + properties: + id: + oneOf: + - type: 'null' + - type: string + image: + oneOf: + - type: 'null' + - type: string + userId: + oneOf: + - type: 'null' + - type: string + Post_ItemCountAggregateOutputType: + type: object + properties: + id: + type: integer + createdAt: + type: integer + updatedAt: + type: integer + title: + type: integer + authorId: + type: integer + published: + type: integer + viewCount: + type: integer + notes: + type: integer + _all: + type: integer + required: + - id + - createdAt + - updatedAt + - title + - authorId + - published + - viewCount + - notes + - _all + Post_ItemAvgAggregateOutputType: + type: object + properties: + viewCount: + oneOf: + - type: 'null' + - type: number + Post_ItemSumAggregateOutputType: + type: object + properties: + viewCount: + oneOf: + - type: 'null' + - type: integer + Post_ItemMinAggregateOutputType: + type: object + properties: + id: + oneOf: + - type: 'null' + - type: string + createdAt: + oneOf: + - type: 'null' + - type: string + format: date-time + updatedAt: + oneOf: + - type: 'null' + - type: string + format: date-time + title: + oneOf: + - type: 'null' + - type: string + authorId: + oneOf: + - type: 'null' + - type: string + published: + oneOf: + - type: 'null' + - type: boolean + viewCount: + oneOf: + - type: 'null' + - type: integer + notes: + oneOf: + - type: 'null' + - type: string + Post_ItemMaxAggregateOutputType: + type: object + properties: + id: + oneOf: + - type: 'null' + - type: string + createdAt: + oneOf: + - type: 'null' + - type: string + format: date-time + updatedAt: + oneOf: + - type: 'null' + - type: string + format: date-time + title: + oneOf: + - type: 'null' + - type: string + authorId: + oneOf: + - type: 'null' + - type: string + published: + oneOf: + - type: 'null' + - type: boolean + viewCount: + oneOf: + - type: 'null' + - type: integer + notes: + oneOf: + - type: 'null' + - type: string + _Meta: + type: object + description: Meta information about the request or response + properties: + serialization: + description: Serialization metadata + additionalProperties: true + _Error: + type: object + required: + - error + properties: + error: + type: object + required: + - message + properties: + prisma: + type: boolean + description: Indicates if the error occurred during a Prisma call + rejectedByPolicy: + type: boolean + description: Indicates if the error was due to rejection by a policy + code: + type: string + description: Prisma error code. Only available when "prisma" field is true. + message: + type: string + description: Error message + reason: + type: string + description: Detailed error reason + zodErrors: + type: object + additionalProperties: true + description: Zod validation errors if the error is due to data validation + failure + additionalProperties: true + BatchPayload: + type: object + properties: + count: + type: integer + UserCreateArgs: + type: object + required: + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserCreateManyArgs: + type: object + required: + - data + properties: + data: + oneOf: + - $ref: '#/components/schemas/_AnyObject' + - type: array + items: + $ref: '#/components/schemas/_AnyObject' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. + meta: + $ref: '#/components/schemas/_Meta' + UserFindUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserFindManyArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserUpdateArgs: + type: object + required: + - where + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserUpdateManyArgs: + type: object + required: + - data + properties: + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserUpsertArgs: + type: object + required: + - create + - update + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + create: + $ref: '#/components/schemas/_AnyObject' + update: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserDeleteUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + cursor: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + UserGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + by: + $ref: '#/components/schemas/_AnyObject' + having: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileCreateArgs: + type: object + required: + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileCreateManyArgs: + type: object + required: + - data + properties: + data: + oneOf: + - $ref: '#/components/schemas/_AnyObject' + - type: array + items: + $ref: '#/components/schemas/_AnyObject' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. + meta: + $ref: '#/components/schemas/_Meta' + ProfileFindUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileFindManyArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileUpdateArgs: + type: object + required: + - where + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileUpdateManyArgs: + type: object + required: + - data + properties: + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileUpsertArgs: + type: object + required: + - create + - update + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + create: + $ref: '#/components/schemas/_AnyObject' + update: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileDeleteUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + cursor: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + ProfileGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + by: + $ref: '#/components/schemas/_AnyObject' + having: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/_AnyObject' + _min: + $ref: '#/components/schemas/_AnyObject' + _max: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemCreateArgs: + type: object + required: + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemCreateManyArgs: + type: object + required: + - data + properties: + data: + oneOf: + - $ref: '#/components/schemas/_AnyObject' + - type: array + items: + $ref: '#/components/schemas/_AnyObject' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemFindUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemUpdateArgs: + type: object + required: + - where + - data + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemUpdateManyArgs: + type: object + required: + - data + properties: + where: + $ref: '#/components/schemas/_AnyObject' + data: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemUpsertArgs: + type: object + required: + - create + - update + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + create: + $ref: '#/components/schemas/_AnyObject' + update: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemDeleteUniqueArgs: + type: object + required: + - where + properties: + select: + $ref: '#/components/schemas/_AnyObject' + include: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/_AnyObject' + where: + $ref: '#/components/schemas/_AnyObject' + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + cursor: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + meta: + $ref: '#/components/schemas/_Meta' + Post_ItemGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/_AnyObject' + orderBy: + $ref: '#/components/schemas/_AnyObject' + by: + $ref: '#/components/schemas/_AnyObject' + having: + $ref: '#/components/schemas/_AnyObject' + take: + type: integer + skip: + type: integer + meta: + $ref: '#/components/schemas/_Meta' +paths: + /user/create: + post: + operationId: createUser + description: Create a new User + tags: + - user + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreateArgs' + /user/createMany: + post: + operationId: createManyUser + description: Create several User + tags: + - user + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreateManyArgs' + /user/findUnique: + get: + operationId: findUniqueUser + description: Find one unique User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/findFirst: + get: + operationId: findFirstUser + description: Find the first User matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindFirstArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/findMany: + get: + operationId: findManyUser + description: Find users matching the given conditions + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/update: + patch: + operationId: updateUser + description: Update a User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateArgs' + /user/updateMany: + patch: + operationId: updateManyUser + description: Update Users matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateManyArgs' + /user/upsert: + post: + operationId: upsertUser + description: Upsert a User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpsertArgs' + /user/dodelete: + put: + operationId: deleteUser + description: Delete a unique user + tags: + - delete + - user + summary: Delete a user yeah yeah + deprecated: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteUniqueArgs' + /user/deleteMany: + delete: + operationId: deleteManyUser + description: Delete Users matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/count: + get: + operationId: countUser + description: Find a list of User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + oneOf: + - type: integer + - $ref: '#/components/schemas/UserCountAggregateOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserCountArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/aggregate: + get: + operationId: aggregateUser + description: Aggregate Users + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AggregateUser' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserAggregateArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /user/groupBy: + get: + operationId: groupByUser + description: Group Users by fields + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserGroupByOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/UserGroupByArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/create: + post: + operationId: createProfile + description: Create a new Profile + tags: + - profile + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileCreateArgs' + /profile/createMany: + post: + operationId: createManyProfile + description: Create several Profile + tags: + - profile + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileCreateManyArgs' + /profile/findUnique: + get: + operationId: findUniqueProfile + description: Find one unique Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileFindUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/findFirst: + get: + operationId: findFirstProfile + description: Find the first Profile matching the given condition + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileFindFirstArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/findMany: + get: + operationId: findManyProfile + description: Find a list of Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileFindManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/update: + patch: + operationId: updateProfile + description: Update a Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileUpdateArgs' + /profile/updateMany: + patch: + operationId: updateManyProfile + description: Update Profiles matching the given condition + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileUpdateManyArgs' + /profile/upsert: + post: + operationId: upsertProfile + description: Upsert a Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileUpsertArgs' + /profile/delete: + delete: + operationId: deleteProfile + description: Delete one unique Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileDeleteUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/deleteMany: + delete: + operationId: deleteManyProfile + description: Delete Profiles matching the given condition + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileDeleteManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/count: + get: + operationId: countProfile + description: Find a list of Profile + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + oneOf: + - type: integer + - $ref: '#/components/schemas/ProfileCountAggregateOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileCountArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/aggregate: + get: + operationId: aggregateProfile + description: Aggregate Profiles + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AggregateProfile' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileAggregateArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /profile/groupBy: + get: + operationId: groupByProfile + description: Group Profiles by fields + tags: + - profile + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/ProfileGroupByOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileGroupByArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/create: + post: + operationId: createPost_Item + description: Create a new Post_Item + tags: + - post_Item + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemCreateArgs' + /post_Item/createMany: + post: + operationId: createManyPost_Item + description: Create several Post_Item + tags: + - post_Item + responses: + '201': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemCreateManyArgs' + /post_Item/findUnique: + get: + operationId: findUniquePost_Item + description: Find one unique Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemFindUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/findFirst: + get: + operationId: findFirstPost_Item + description: Find the first Post_Item matching the given condition + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemFindFirstArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/update: + patch: + operationId: updatePost_Item + description: Update a Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemUpdateArgs' + /post_Item/updateMany: + patch: + operationId: updateManyPost_Item + description: Update Post_Items matching the given condition + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemUpdateManyArgs' + /post_Item/upsert: + post: + operationId: upsertPost_Item + description: Upsert a Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemUpsertArgs' + /post_Item/delete: + delete: + operationId: deletePost_Item + description: Delete one unique Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Post_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemDeleteUniqueArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/deleteMany: + delete: + operationId: deleteManyPost_Item + description: Delete Post_Items matching the given condition + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BatchPayload' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemDeleteManyArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/count: + get: + operationId: countPost_Item + description: Find a list of Post_Item + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + oneOf: + - type: integer + - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemCountArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/aggregate: + get: + operationId: aggregatePost_Item + description: Aggregate Post_Items + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AggregatePost_Item' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemAggregateArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} + /post_Item/groupBy: + get: + operationId: groupByPost_Item + description: Group Post_Items by fields + tags: + - post_Item + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Post_ItemGroupByOutputType' + description: The Prisma response data serialized with superjson + meta: + $ref: '#/components/schemas/_Meta' + description: The superjson serialization metadata for the "data" field + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Invalid request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors + parameters: + - name: q + in: query + required: true + description: Superjson-serialized Prisma query object + content: + application/json: + schema: + $ref: '#/components/schemas/Post_ItemGroupByArgs' + - name: meta + in: query + description: Superjson serialization metadata for parameter "q" + content: + application/json: + schema: {} diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts index 6873ab5ce..f58b7fa09 100644 --- a/packages/plugins/openapi/tests/openapi-rpc.test.ts +++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts @@ -16,10 +16,12 @@ tmp.setGracefulCleanup(); describe('Open API Plugin RPC Tests', () => { it('run plugin', async () => { for (const specVersion of ['3.0.0', '3.1.0']) { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` + for (const omitInputDetails of [true, false]) { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '${specVersion}' + omitInputDetails = ${omitInputDetails} } enum role { @@ -89,40 +91,46 @@ model Bar { } `); - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log(`OpenAPI specification generated for ${specVersion}: ${output}`); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - const baseline = YAML.parse( - fs.readFileSync(`${__dirname}/baseline/rpc-${specVersion}.baseline.yaml`, 'utf-8') - ); - expect(parsed).toMatchObject(baseline); - - const api = await OpenAPIParser.validate(output); - - expect(api.tags).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'user', description: 'User operations' }), - expect.objectContaining({ name: 'post_Item', description: 'Post-related operations' }), - ]) - ); - - expect(api.paths?.['/user/findMany']?.['get']?.description).toBe( - 'Find users matching the given conditions' - ); - const del = api.paths?.['/user/dodelete']?.['put']; - expect(del?.description).toBe('Delete a unique user'); - expect(del?.summary).toBe('Delete a user yeah yeah'); - expect(del?.tags).toEqual(expect.arrayContaining(['delete', 'user'])); - expect(del?.deprecated).toBe(true); - expect(api.paths?.['/post/findMany']).toBeUndefined(); - expect(api.paths?.['/foo/findMany']).toBeUndefined(); - expect(api.paths?.['/bar/findMany']).toBeUndefined(); + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + console.log( + `OpenAPI specification generated for ${specVersion}${omitInputDetails ? ' - omit' : ''}: ${output}` + ); + + const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); + expect(parsed.openapi).toBe(specVersion); + const baseline = YAML.parse( + fs.readFileSync( + `${__dirname}/baseline/rpc-${specVersion}${omitInputDetails ? '-omit' : ''}.baseline.yaml`, + 'utf-8' + ) + ); + expect(parsed).toMatchObject(baseline); + + const api = await OpenAPIParser.validate(output); + + expect(api.tags).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'user', description: 'User operations' }), + expect.objectContaining({ name: 'post_Item', description: 'Post-related operations' }), + ]) + ); + + expect(api.paths?.['/user/findMany']?.['get']?.description).toBe( + 'Find users matching the given conditions' + ); + const del = api.paths?.['/user/dodelete']?.['put']; + expect(del?.description).toBe('Delete a unique user'); + expect(del?.summary).toBe('Delete a user yeah yeah'); + expect(del?.tags).toEqual(expect.arrayContaining(['delete', 'user'])); + expect(del?.deprecated).toBe(true); + expect(api.paths?.['/post/findMany']).toBeUndefined(); + expect(api.paths?.['/foo/findMany']).toBeUndefined(); + expect(api.paths?.['/bar/findMany']).toBeUndefined(); + } } }); diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 2918da8f7..10eb42014 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "2.8.1", + "version": "2.9.0", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index 967be9727..5ecfe4d84 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -60,8 +60,6 @@ function generateModelHooks( const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma'], @@ -261,6 +259,7 @@ function generateIndex(project: Project, outDir: string, models: DataModel[]) { const sf = project.createSourceFile(path.join(outDir, 'index.ts'), undefined, { overwrite: true }); sf.addStatements(models.map((d) => `export * from './${paramCase(d.name)}';`)); sf.addStatements(`export { Provider } from '@zenstackhq/swr/runtime';`); + sf.addStatements(`export { default as metadata } from './__model_meta';`); } function generateQueryHook( diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 87a8a2bd9..5e5a736d4 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "2.8.1", + "version": "2.9.0", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 8833f77f9..c45a32517 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -14,6 +14,7 @@ import { import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; +import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; @@ -45,6 +46,14 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. outDir = resolvePath(outDir, options); ensureEmptyDir(outDir); + if (options.portable && typeof options.portable !== 'boolean') { + throw new PluginError( + name, + `Invalid value for "portable" option: ${options.portable}, a boolean value is expected` + ); + } + const portable = options.portable ?? false; + await generateModelMeta(project, models, typeDefs, { output: path.join(outDir, '__model_meta.ts'), generateAttributes: false, @@ -61,6 +70,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. generateModelHooks(target, version, project, outDir, dataModel, mapping, options); }); + if (portable) { + generateBundledTypes(project, outDir, options); + } + await saveProject(project); return { warnings }; } @@ -333,9 +346,7 @@ function generateModelHooks( const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); - - const prismaImport = getPrismaClientImportSpec(outDir, options); + const prismaImport = options.portable ? './__types' : getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma', model.name], isTypeOnly: true, @@ -584,6 +595,7 @@ function generateIndex( sf.addStatements(`export { SvelteQueryContextKey, setHooksContext } from '${runtimeImportBase}/svelte';`); break; } + sf.addStatements(`export { default as metadata } from './__model_meta';`); } function makeGetContext(target: TargetFramework) { @@ -724,3 +736,21 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin function makeRuntimeImportBase(version: TanStackVersion) { return `@zenstackhq/tanstack-query/runtime${version === 'v5' ? '-v5' : ''}`; } + +function generateBundledTypes(project: Project, outDir: string, options: PluginOptions) { + if (!options.prismaClientDtsPath) { + throw new PluginError(name, `Unable to determine the location of PrismaClient types`); + } + + // copy PrismaClient index.d.ts + const content = fs.readFileSync(options.prismaClientDtsPath, 'utf-8'); + project.createSourceFile(path.join(outDir, '__types.d.ts'), content, { overwrite: true }); + + // "runtime/library.d.ts" is referenced by Prisma's DTS, and it's generated into Prisma's output + // folder if a custom output is specified; if not, it's referenced from '@prisma/client' + const libraryDts = path.join(path.dirname(options.prismaClientDtsPath), 'runtime', 'library.d.ts'); + if (fs.existsSync(libraryDts)) { + const content = fs.readFileSync(libraryDts, 'utf-8'); + project.createSourceFile(path.join(outDir, 'runtime', 'library.d.ts'), content, { overwrite: true }); + } +} diff --git a/packages/plugins/tanstack-query/tests/portable.test.ts b/packages/plugins/tanstack-query/tests/portable.test.ts new file mode 100644 index 000000000..6ddbb1cc3 --- /dev/null +++ b/packages/plugins/tanstack-query/tests/portable.test.ts @@ -0,0 +1,153 @@ +/// + +import { loadSchema, normalizePath } from '@zenstackhq/testtools'; +import path from 'path'; +import tmp from 'tmp'; + +describe('Tanstack Query Plugin Portable Tests', () => { + it('supports portable for standard prisma client', async () => { + await loadSchema( + ` + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/hooks' + target = 'react' + portable = true + } + + model User { + id Int @id @default(autoincrement()) + email String + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + } + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], + copyDependencies: [path.resolve(__dirname, '../dist')], + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { useFindUniqueUser } from './hooks'; +const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } }); +console.log(data?.email); +console.log(data?.posts[0].title); +`, + }, + ], + } + ); + }); + + it('supports portable for custom prisma client output', async () => { + const t = tmp.dirSync({ unsafeCleanup: true }); + const projectDir = t.name; + + await loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator client { + provider = 'prisma-client-js' + output = '$projectRoot/myprisma' + } + + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/hooks' + target = 'react' + portable = true + } + + model User { + id Int @id @default(autoincrement()) + email String + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + } + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], + copyDependencies: [path.resolve(__dirname, '../dist')], + compile: true, + addPrelude: false, + projectDir, + prismaLoadPath: `${projectDir}/myprisma`, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { useFindUniqueUser } from './hooks'; +const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } }); +console.log(data?.email); +console.log(data?.posts[0].title); +`, + }, + ], + } + ); + }); + + it('supports portable for logical client', async () => { + await loadSchema( + ` + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/hooks' + target = 'react' + portable = true + } + + model Base { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + type String + @@delegate(type) + } + + model User extends Base { + email String + } + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], + copyDependencies: [path.resolve(__dirname, '../dist')], + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { useFindUniqueUser } from './hooks'; +const { data } = useFindUniqueUser({ where: { id: 1 } }); +console.log(data?.email); +console.log(data?.createdAt); +`, + }, + ], + } + ); + }); +}); diff --git a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx index 51f7de64f..0a1810ed0 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx @@ -281,6 +281,124 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); + it('optimistic create updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = [{ id: '1', name: 'user1', posts: [] }]; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => + useModelQuery( + 'User', + makeUrl('User', 'findMany'), + { include: { posts: true } }, + { optimisticUpdate: true } + ), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('Post', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ data: { title: 'post1', owner: { connect: { id: '1' } } } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'User', + 'findMany', + { include: { posts: true } }, + { infinite: false, optimisticUpdate: true } + ) + ); + const posts = cacheData[0].posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ $optimistic: true, id: expect.any(String), title: 'post1', ownerId: '1' }); + }); + }); + + it('optimistic nested create updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = []; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ data: { name: 'user1', posts: { create: { title: 'post1' } } } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].title).toBe('post1'); + }); + }); + it('optimistic create many', async () => { const { queryClient, wrapper } = createWrapper(); @@ -420,7 +538,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); - it('optimistic update', async () => { + it('optimistic update simple', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -472,7 +590,121 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); - it('optimistic upsert - create', async () => { + it('optimistic update updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' }, include: { posts: true } }; + const data = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('Post', 'update')) + .put(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return data; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: 'p1' }, + data: { title: 'post2', owner: { connect: { id: '2' } } }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData.posts[0]).toMatchObject({ title: 'post2', $optimistic: true, ownerId: '2' }); + }); + }); + + it('optimistic nested update updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: 'p1' } }; + const data = { id: 'p1', title: 'post1' }; + + nock(makeUrl('Post', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('Post', makeUrl('Post', 'findUnique'), queryArgs, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ title: 'post1' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return data; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + data: { posts: { update: { where: { id: 'p1' }, data: { title: 'post2' } } } }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toMatchObject({ title: 'post2', $optimistic: true }); + }); + }); + + it('optimistic upsert - create simple', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = []; @@ -526,13 +758,141 @@ describe('Tanstack Query React Hooks V5 Test', () => { getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) ); expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].name).toBe('foo'); + expect(cacheData[0]).toMatchObject({ id: '1', name: 'foo', $optimistic: true }); }); }); - it('optimistic upsert - update', async () => { + it('optimistic upsert - create updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; + + nock(makeUrl('User', 'findUnique')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => + useModelQuery( + 'User', + makeUrl('User', 'findUnique'), + { where: { id: '1' } }, + { optimisticUpdate: true } + ), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ id: '1' }); + }); + + nock(makeUrl('Post', 'upsert')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('Post', 'POST', makeUrl('Post', 'upsert'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: 'p2' }, + create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } }, + update: { title: 'post3' }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }) + ); + const posts = cacheData.posts; + expect(posts).toHaveLength(2); + expect(posts[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true }); + }); + }); + + it('optimistic upsert - nested create updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = [{ id: 'p1', title: 'post1' }]; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('User', 'update')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + data: { + posts: { + upsert: { + where: { id: 'p2' }, + create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } }, + update: { title: 'post3', owner: { connect: { id: '2' } } }, + }, + }, + }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(2); + expect(cacheData[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true }); + }); + }); + + it('optimistic upsert - update simple', async () => { const { queryClient, wrapper } = createWrapper(); const queryArgs = { where: { id: '1' } }; @@ -584,6 +944,136 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); + it('optimistic upsert - update updating nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; + + nock(makeUrl('User', 'findUnique')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => + useModelQuery( + 'User', + makeUrl('User', 'findUnique'), + { where: { id: '1' } }, + { optimisticUpdate: true } + ), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ id: '1' }); + }); + + nock(makeUrl('Post', 'upsert')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('Post', 'POST', makeUrl('Post', 'upsert'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: 'p1' }, + create: { id: 'p1', title: 'post1' }, + update: { title: 'post2' }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }) + ); + const posts = cacheData.posts; + expect(posts).toHaveLength(1); + expect(posts[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); + }); + }); + + it('optimistic upsert - nested update updating query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = [{ id: 'p1', title: 'post1' }]; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('User', 'update')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + data: { + posts: { + upsert: { + where: { id: 'p1' }, + create: { id: 'p1', title: 'post1' }, + update: { title: 'post2' }, + }, + }, + }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); + }); + }); + it('delete and invalidation', async () => { const { queryClient, wrapper } = createWrapper(); @@ -627,7 +1117,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); - it('optimistic delete', async () => { + it('optimistic delete simple', async () => { const { queryClient, wrapper } = createWrapper(); const data: any[] = [{ id: '1', name: 'foo' }]; @@ -678,6 +1168,122 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); + it('optimistic delete nested query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; + + nock(makeUrl('User', 'findFirst')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => + useModelQuery( + 'User', + makeUrl('User', 'findFirst'), + { include: { posts: true } }, + { optimisticUpdate: true } + ), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ id: '1' }); + }); + + nock(makeUrl('Post', 'delete')) + .delete(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('Post', 'DELETE', makeUrl('Post', 'delete'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ where: { id: 'p1' } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey( + 'User', + 'findFirst', + { include: { posts: true } }, + { infinite: false, optimisticUpdate: true } + ) + ); + expect(cacheData.posts).toHaveLength(0); + }); + }); + + it('optimistic nested delete update query', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any = [ + { id: 'p1', title: 'post1' }, + { id: 'p2', title: 'post2' }, + ]; + + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(2); + }); + + nock(makeUrl('User', 'update')) + .delete(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { delete: { id: 'p1' } } } })); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(1); + }); + }); + it('top-level mutation and nested-read invalidation', async () => { const { queryClient, wrapper } = createWrapper(); diff --git a/packages/plugins/tanstack-query/tests/test-model-meta.ts b/packages/plugins/tanstack-query/tests/test-model-meta.ts index 001d773a9..174abd1bd 100644 --- a/packages/plugins/tanstack-query/tests/test-model-meta.ts +++ b/packages/plugins/tanstack-query/tests/test-model-meta.ts @@ -45,8 +45,15 @@ export const modelMeta: ModelMeta = { isOptional: false, }, title: { ...fieldDefaults, type: 'String', name: 'title' }, - owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, - ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, + owner: { + ...fieldDefaults, + type: 'User', + name: 'owner', + isDataModel: true, + isRelationOwner: true, + foreignKeyMapping: { id: 'ownerId' }, + }, + ownerId: { ...fieldDefaults, type: 'String', name: 'ownerId', isForeignKey: true }, }, uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index fc6c86e26..e25956433 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "2.8.1", + "version": "2.9.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/src/client-helper/index.ts b/packages/plugins/trpc/src/client-helper/index.ts index f296ca1ab..99679c716 100644 --- a/packages/plugins/trpc/src/client-helper/index.ts +++ b/packages/plugins/trpc/src/client-helper/index.ts @@ -41,8 +41,6 @@ export function generateClientTypingForModel( } ); - sf.addStatements([`/* eslint-disable */`]); - generateImports(clientType, sf, options, version); // generate a `ClientType` interface that contains typing for query/mutation operations diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 6574069f8..5f7d8601b 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -122,8 +122,6 @@ function createAppRouter( overwrite: true, }); - appRouter.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(path.dirname(indexFile), options); if (version === 'v10') { @@ -274,8 +272,6 @@ function generateModelCreateRouter( overwrite: true, }); - modelRouter.addStatements('/* eslint-disable */'); - if (version === 'v10') { modelRouter.addImportDeclarations([ { @@ -386,7 +382,6 @@ function createHelper(outDir: string) { overwrite: true, }); - sf.addStatements('/* eslint-disable */'); sf.addStatements(`import { TRPCError } from '@trpc/server';`); sf.addStatements(`import { isPrismaClientKnownRequestError } from '${RUNTIME_PACKAGE}';`); diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma index e83468bb0..71cd1ce9b 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma @@ -12,26 +12,17 @@ generator client { provider = "prisma-client-js" } -/// @@allow('create', true) -/// @@allow('all', auth() == this) model User { id String @id() @default(cuid()) - /// @email - /// @length(6, 32) email String @unique() - /// @password - /// @omit password String posts Post[] } -/// @@allow('read', auth() != null && published) -/// @@allow('all', author == auth()) model Post { id String @id() @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt() - /// @length(1, 256) title String content String published Boolean @default(false) diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts index 118da4be5..8dad6ebd7 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts index fb4d90c74..dd7e05774 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts index 8bffd4a5b..ee24d694d 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts @@ -1,4 +1,9 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck import type { AnyRouter } from '@trpc/server'; import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts index 8173ebbc0..2d003e1a8 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts @@ -1,6 +1,9 @@ -/* eslint-disable */ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ -// inspired by: https://stackoverflow.com/questions/70632026/generic-to-recursively-modify-a-given-type-interface-in-typescript +/* eslint-disable */ +// @ts-nocheck type Primitive = string | Function | number | boolean | Symbol | undefined | null; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts index 7f292fff2..6b99aa63d 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { TRPCError } from '@trpc/server'; import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts index 1733631f2..7a4417fa7 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; import * as _Schema from '@zenstackhq/runtime/zod/input'; const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts index da606d2da..21f6ea4cd 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; import * as _Schema from '@zenstackhq/runtime/zod/input'; const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts index 076308882..7d811eef4 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { unsetMarker, AnyRouter, AnyRootConfig, CreateRouterInner, Procedure, ProcedureBuilder, ProcedureParams, ProcedureRouterRecord, ProcedureType } from "@trpc/server"; import type { PrismaClient } from "@prisma/client"; import createUserRouter from "./User.router"; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma index e83468bb0..71cd1ce9b 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma @@ -12,26 +12,17 @@ generator client { provider = "prisma-client-js" } -/// @@allow('create', true) -/// @@allow('all', auth() == this) model User { id String @id() @default(cuid()) - /// @email - /// @length(6, 32) email String @unique() - /// @password - /// @omit password String posts Post[] } -/// @@allow('read', auth() != null && published) -/// @@allow('all', author == auth()) model Post { id String @id() @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt() - /// @length(1, 256) title String content String published Boolean @default(false) diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts index bf6e93498..d6ff6ee09 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts index f78f3a5ed..17bad9a1e 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts index 37d06435b..8788515d9 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts @@ -1,4 +1,9 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts index 8173ebbc0..2d003e1a8 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts @@ -1,6 +1,9 @@ -/* eslint-disable */ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ -// inspired by: https://stackoverflow.com/questions/70632026/generic-to-recursively-modify-a-given-type-interface-in-typescript +/* eslint-disable */ +// @ts-nocheck type Primitive = string | Function | number | boolean | Symbol | undefined | null; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts index 7f292fff2..6b99aa63d 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { TRPCError } from '@trpc/server'; import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts index 435b12d83..25ce457f9 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { db } from "."; import { createTRPCRouter } from "../../generated-router-helper"; import { procedure } from "../../generated-router-helper"; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts index 9cba05d4c..f3d98197e 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { db } from "."; import { createTRPCRouter } from "../../generated-router-helper"; import { procedure } from "../../generated-router-helper"; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts index 949b84f3d..ea7eca4ed 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { AnyTRPCRouter as AnyRouter } from "@trpc/server"; import type { PrismaClient } from "@prisma/client"; import { createTRPCRouter } from "../../generated-router-helper"; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts index c73627928..b7ce17bdb 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts index bd0617535..8ce5b7e35 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts index 3b2c10686..55d4c8a78 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts @@ -1,4 +1,9 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck import type { AnyRouter } from '@trpc/server'; import type { NextPageContext } from 'next'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts index 8173ebbc0..2d003e1a8 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts @@ -1,6 +1,9 @@ -/* eslint-disable */ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ -// inspired by: https://stackoverflow.com/questions/70632026/generic-to-recursively-modify-a-given-type-interface-in-typescript +/* eslint-disable */ +// @ts-nocheck type Primitive = string | Function | number | boolean | Symbol | undefined | null; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts index 7f292fff2..6b99aa63d 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { TRPCError } from '@trpc/server'; import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts index 1733631f2..7a4417fa7 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; import * as _Schema from '@zenstackhq/runtime/zod/input'; const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts index da606d2da..21f6ea4cd 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; import * as _Schema from '@zenstackhq/runtime/zod/input'; const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts index 076308882..7d811eef4 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { unsetMarker, AnyRouter, AnyRootConfig, CreateRouterInner, Procedure, ProcedureBuilder, ProcedureParams, ProcedureRouterRecord, ProcedureType } from "@trpc/server"; import type { PrismaClient } from "@prisma/client"; import createUserRouter from "./User.router"; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma index 5199cfaa7..a28fea9fb 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma @@ -12,14 +12,12 @@ generator client { provider = "prisma-client-js" } -/// @@allow('all', true) model User { id Int @id() @default(autoincrement()) email String @unique() posts Post[] } -/// @@allow('all', true) model Post { id Int @id() @default(autoincrement()) name String diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts index 82b8da078..a154285c4 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts index e04628191..37ba687ca 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { Prisma } from '@prisma/client'; import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts index b3b2b2009..351154db9 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts @@ -1,4 +1,9 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; import type { CreateTRPCReactOptions } from '@trpc/react-query/shared'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts index 8173ebbc0..2d003e1a8 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts @@ -1,6 +1,9 @@ -/* eslint-disable */ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ -// inspired by: https://stackoverflow.com/questions/70632026/generic-to-recursively-modify-a-given-type-interface-in-typescript +/* eslint-disable */ +// @ts-nocheck type Primitive = string | Function | number | boolean | Symbol | undefined | null; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts index 7f292fff2..6b99aa63d 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { TRPCError } from '@trpc/server'; import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts index 435b12d83..25ce457f9 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { db } from "."; import { createTRPCRouter } from "../../generated-router-helper"; import { procedure } from "../../generated-router-helper"; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts index 9cba05d4c..f3d98197e 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import { db } from "."; import { createTRPCRouter } from "../../generated-router-helper"; import { procedure } from "../../generated-router-helper"; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts index 949b84f3d..ea7eca4ed 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts @@ -1,4 +1,10 @@ +/****************************************************************************** +* This file was generated by ZenStack CLI 2.9.0. +******************************************************************************/ + /* eslint-disable */ +// @ts-nocheck + import type { AnyTRPCRouter as AnyRouter } from "@trpc/server"; import type { PrismaClient } from "@prisma/client"; import { createTRPCRouter } from "../../generated-router-helper"; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 983fbe248..30264a332 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.8.1", + "version": "2.9.0", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/runtime/src/cross/mutator.ts b/packages/runtime/src/cross/mutator.ts index e38570399..4ec76cbd1 100644 --- a/packages/runtime/src/cross/mutator.ts +++ b/packages/runtime/src/cross/mutator.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { v4 as uuid } from 'uuid'; import { - ModelDataVisitor, + FieldInfo, NestedWriteVisitor, enumerate, getFields, @@ -34,18 +34,37 @@ export async function applyMutation( modelMeta: ModelMeta, logging: boolean ) { - if (['count', 'aggregate', 'groupBy'].includes(queryOp)) { + if (!queryData || (typeof queryData !== 'object' && !Array.isArray(queryData))) { + return undefined; + } + + if (!queryOp.startsWith('find')) { // only findXXX results are applicable return undefined; } + return await doApplyMutation(queryModel, queryData, mutationModel, mutationOp, mutationArgs, modelMeta, logging); +} + +async function doApplyMutation( + queryModel: string, + queryData: any, + mutationModel: string, + mutationOp: PrismaWriteActionType, + mutationArgs: any, + modelMeta: ModelMeta, + logging: boolean +) { let resultData = queryData; let updated = false; const visitor = new NestedWriteVisitor(modelMeta, { create: (model, args) => { - if (model === queryModel) { - const r = createMutate(queryModel, queryOp, resultData, args, modelMeta, logging); + if ( + model === queryModel && + Array.isArray(resultData) // "create" mutation is only relevant for arrays + ) { + const r = createMutate(queryModel, resultData, args, modelMeta, logging); if (r) { resultData = r; updated = true; @@ -54,9 +73,13 @@ export async function applyMutation( }, createMany: (model, args) => { - if (model === queryModel && args?.data) { + if ( + model === queryModel && + args?.data && + Array.isArray(resultData) // "createMany" mutation is only relevant for arrays + ) { for (const oneArg of enumerate(args.data)) { - const r = createMutate(queryModel, queryOp, resultData, oneArg, modelMeta, logging); + const r = createMutate(queryModel, resultData, oneArg, modelMeta, logging); if (r) { resultData = r; updated = true; @@ -66,7 +89,10 @@ export async function applyMutation( }, update: (model, args) => { - if (model === queryModel) { + if ( + model === queryModel && + !Array.isArray(resultData) // array elements will be handled with recursion + ) { const r = updateMutate(queryModel, resultData, model, args, modelMeta, logging); if (r) { resultData = r; @@ -77,25 +103,10 @@ export async function applyMutation( upsert: (model, args) => { if (model === queryModel && args?.where && args?.create && args?.update) { - // first see if a matching update can be applied - const updateResult = updateMutate( - queryModel, - resultData, - model, - { where: args.where, data: args.update }, - modelMeta, - logging - ); - if (updateResult) { - resultData = updateResult; + const r = upsertMutate(queryModel, resultData, model, args, modelMeta, logging); + if (r) { + resultData = r; updated = true; - } else { - // if not, try to apply a create - const createResult = createMutate(queryModel, queryOp, resultData, args.create, modelMeta, logging); - if (createResult) { - resultData = createResult; - updated = true; - } } } }, @@ -113,25 +124,75 @@ export async function applyMutation( await visitor.visit(mutationModel, mutationOp, mutationArgs); + const modelFields = getFields(modelMeta, queryModel); + + if (Array.isArray(resultData)) { + // try to apply mutation to each item in the array, replicate the entire + // array if any item is updated + + let arrayCloned = false; + for (let i = 0; i < resultData.length; i++) { + const item = resultData[i]; + if ( + !item || + typeof item !== 'object' || + item.$optimistic // skip items already optimistically updated + ) { + continue; + } + + const r = await doApplyMutation( + queryModel, + item, + mutationModel, + mutationOp, + mutationArgs, + modelMeta, + logging + ); + + if (r) { + if (!arrayCloned) { + resultData = [...resultData]; + arrayCloned = true; + } + resultData[i] = r; + updated = true; + } + } + } else { + // iterate over each field and apply mutation to nested data models + for (const [key, value] of Object.entries(resultData)) { + const fieldInfo = modelFields[key]; + if (!fieldInfo?.isDataModel) { + continue; + } + + const r = await doApplyMutation( + fieldInfo.type, + value, + mutationModel, + mutationOp, + mutationArgs, + modelMeta, + logging + ); + + if (r) { + resultData = { ...resultData, [key]: r }; + updated = true; + } + } + } + return updated ? resultData : undefined; } -function createMutate( - queryModel: string, - queryOp: string, - currentData: any, - newData: any, - modelMeta: ModelMeta, - logging: boolean -) { +function createMutate(queryModel: string, currentData: any, newData: any, modelMeta: ModelMeta, logging: boolean) { if (!newData) { return undefined; } - if (queryOp !== 'findMany') { - return undefined; - } - const modelFields = getFields(modelMeta, queryModel); if (!modelFields) { return undefined; @@ -141,12 +202,14 @@ function createMutate( const newDataFields = Object.keys(newData); Object.entries(modelFields).forEach(([name, field]) => { - if (field.isDataModel) { - // only include scalar fields + if (field.isDataModel && newData[name]) { + // deal with "connect" + assignForeignKeyFields(field, insert, newData[name]); return; } + if (newDataFields.includes(name)) { - insert[name] = newData[name]; + insert[name] = clone(newData[name]); } else { const defaultAttr = field.attributes?.find((attr) => attr.name === '@default'); if (field.type === 'DateTime') { @@ -197,35 +260,115 @@ function updateMutate( modelMeta: ModelMeta, logging: boolean ) { - if (!currentData) { + if (!currentData || typeof currentData !== 'object') { + return undefined; + } + + if (!mutateArgs?.where || typeof mutateArgs.where !== 'object') { + return undefined; + } + + if (!mutateArgs?.data || typeof mutateArgs.data !== 'object') { return undefined; } - if (!mutateArgs?.where || !mutateArgs?.data) { + if (!idFieldsMatch(mutateModel, currentData, mutateArgs.where, modelMeta)) { + return undefined; + } + + const modelFields = getFields(modelMeta, queryModel); + if (!modelFields) { return undefined; } let updated = false; + let resultData = currentData; - for (const item of enumerate(currentData)) { - const visitor = new ModelDataVisitor(modelMeta); - visitor.visit(queryModel, item, (model, _data, scalarData) => { - if (model === mutateModel && idFieldsMatch(model, scalarData, mutateArgs.where, modelMeta)) { - Object.keys(item).forEach((k) => { - if (mutateArgs.data[k] !== undefined) { - item[k] = mutateArgs.data[k]; - } - }); - item.$optimistic = true; + for (const [key, value] of Object.entries(mutateArgs.data)) { + const fieldInfo = modelFields[key]; + if (!fieldInfo) { + continue; + } + + if (fieldInfo.isDataModel && !value?.connect) { + // relation field but without "connect" + continue; + } + + if (!updated) { + // clone + resultData = { ...currentData }; + } + + if (fieldInfo.isDataModel) { + // deal with "connect" + assignForeignKeyFields(fieldInfo, resultData, value); + } else { + resultData[key] = clone(value); + } + resultData.$optimistic = true; + updated = true; + + if (logging) { + console.log(`Optimistic update for ${queryModel}:`, resultData); + } + } + + return updated ? resultData : undefined; +} + +function upsertMutate( + queryModel: string, + currentData: any, + model: string, + args: { where: object; create: any; update: any }, + modelMeta: ModelMeta, + logging: boolean +) { + let updated = false; + let resultData = currentData; + + if (Array.isArray(resultData)) { + // check if we should create or update + const foundIndex = resultData.findIndex((x) => idFieldsMatch(model, x, args.where, modelMeta)); + if (foundIndex >= 0) { + const updateResult = updateMutate( + queryModel, + resultData[foundIndex], + model, + { where: args.where, data: args.update }, + modelMeta, + logging + ); + if (updateResult) { + // replace the found item with updated item + resultData = [...resultData.slice(0, foundIndex), updateResult, ...resultData.slice(foundIndex + 1)]; + updated = true; + } + } else { + const createResult = createMutate(queryModel, resultData, args.create, modelMeta, logging); + if (createResult) { + resultData = createResult; updated = true; - if (logging) { - console.log(`Optimistic update for ${queryModel}:`, item); - } } - }); + } + } else { + // try update only + const updateResult = updateMutate( + queryModel, + resultData, + model, + { where: args.where, data: args.update }, + modelMeta, + logging + ); + if (updateResult) { + resultData = updateResult; + updated = true; + } } - return updated ? clone(currentData) /* ensures new object identity */ : undefined; + return updated ? resultData : undefined; } function deleteMutate( @@ -282,3 +425,21 @@ function idFieldsMatch(model: string, x: any, y: any, modelMeta: ModelMeta) { } return idFields.every((f) => x[f.name] === y[f.name]); } + +function assignForeignKeyFields(field: FieldInfo, resultData: any, mutationData: any) { + // convert "connect" like `{ connect: { id: '...' } }` to foreign key fields + // assignment: `{ userId: '...' }` + if (!mutationData?.connect) { + return; + } + + if (!field.foreignKeyMapping) { + return; + } + + for (const [idField, fkField] of Object.entries(field.foreignKeyMapping)) { + if (idField in mutationData.connect) { + resultData[fkField] = mutationData.connect[idField]; + } + } +} diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index 78523b837..80fd09f17 100644 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ b/packages/runtime/src/enhancements/node/delegate.ts @@ -587,6 +587,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { let curr = args; let base = this.getBaseModel(model); let sub = this.getModelInfo(model); + const hasDelegateBase = !!base; while (base) { const baseRelationName = this.makeAuxRelationName(base); @@ -615,6 +616,55 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { sub = base; base = this.getBaseModel(base.name); } + + if (hasDelegateBase) { + // A delegate base model creation is added, this can be incompatible if + // the user-provided payload assigns foreign keys directly, because Prisma + // doesn't permit mixed "checked" and "unchecked" fields in a payload. + // + // { + // delegate_aux_base: { ... }, + // [fkField]: value // <- this is not compatible + // } + // + // We need to convert foreign key assignments to `connect`. + this.fkAssignmentToConnect(model, args); + } + } + + // convert foreign key assignments to `connect` payload + // e.g.: { authorId: value } -> { author: { connect: { id: value } } } + private fkAssignmentToConnect(model: string, args: any) { + const keysToDelete: string[] = []; + for (const [key, value] of Object.entries(args)) { + if (value === undefined) { + continue; + } + + const fieldInfo = this.queryUtils.getModelField(model, key); + if ( + !fieldInfo?.inheritedFrom && // fields from delegate base are handled outside + fieldInfo?.isForeignKey + ) { + const relationInfo = this.queryUtils.getRelationForForeignKey(model, key); + if (relationInfo) { + // turn { [fk]: value } into { [relation]: { connect: { [id]: value } } } + const relationName = relationInfo.relation.name; + if (!args[relationName]) { + args[relationName] = {}; + } + if (!args[relationName].connect) { + args[relationName].connect = {}; + } + if (!(relationInfo.idField in args[relationName].connect)) { + args[relationName].connect[relationInfo.idField] = value; + keysToDelete.push(key); + } + } + } + } + + keysToDelete.forEach((key) => delete args[key]); } // inject field data that belongs to base type into proper nesting structure diff --git a/packages/runtime/src/enhancements/node/proxy.ts b/packages/runtime/src/enhancements/node/proxy.ts index ae4105301..cfbc0eb7c 100644 --- a/packages/runtime/src/enhancements/node/proxy.ts +++ b/packages/runtime/src/enhancements/node/proxy.ts @@ -254,6 +254,25 @@ export function makeProxy( } } + if (prop === '$extends') { + // Prisma's `$extends` API returns a new client instance, we need to recreate + // a proxy around it + const $extends = Reflect.get(target, prop, receiver); + if ($extends && typeof $extends === 'function') { + return (...args: any[]) => { + const result = $extends.bind(target)(...args); + if (!result[PRISMA_PROXY_ENHANCER]) { + return makeProxy(result, modelMeta, makeHandler, name + '$ext', errorTransformer); + } else { + // avoid double wrapping + return result; + } + }; + } else { + return $extends; + } + } + if (typeof prop !== 'string' || prop.startsWith('$') || !models.includes(prop.toLowerCase())) { // skip non-model fields return Reflect.get(target, prop, receiver); diff --git a/packages/runtime/src/enhancements/node/query-utils.ts b/packages/runtime/src/enhancements/node/query-utils.ts index 0effd4557..c09fe1f95 100644 --- a/packages/runtime/src/enhancements/node/query-utils.ts +++ b/packages/runtime/src/enhancements/node/query-utils.ts @@ -232,4 +232,25 @@ export class QueryUtils { return model; } + + /** + * Gets relation info for a foreign key field. + */ + getRelationForForeignKey(model: string, fkField: string) { + const modelInfo = getModelInfo(this.options.modelMeta, model); + if (!modelInfo) { + return undefined; + } + + for (const field of Object.values(modelInfo.fields)) { + if (field.foreignKeyMapping) { + const entry = Object.entries(field.foreignKeyMapping).find(([, v]) => v === fkField); + if (entry) { + return { relation: field, idField: entry[0], fkField: entry[1] }; + } + } + } + + return undefined; + } } diff --git a/packages/schema/package.json b/packages/schema/package.json index 5ea95e5d8..a3e2d7f76 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI", - "version": "2.8.1", + "version": "2.9.0", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 43216656e..a0611fac7 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -1,5 +1,5 @@ import { isDataSource, isPlugin, Model } from '@zenstackhq/language/ast'; -import { getDataModels, getLiteral, hasAttribute } from '@zenstackhq/sdk'; +import { getDataModelAndTypeDefs, getLiteral, hasAttribute } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'fs'; import { getDocument, LangiumDocument, LangiumDocuments, linkContentToContainer } from 'langium'; @@ -105,11 +105,10 @@ export async function loadDocument(fileName: string, validateOnly = false): Prom const imported = mergeImportsDeclarations(langiumDocuments, model); // remove imported documents - await services.shared.workspace.DocumentBuilder.update( - [], - imported.map((m) => m.$document!.uri) - ); + imported.forEach((model) => langiumDocuments.deleteDocument(model.$document!.uri)); + services.shared.workspace.IndexManager.remove(imported.map((model) => model.$document!.uri)); + // extra validation after merging imported declarations validationAfterImportMerge(model); // merge fields and attributes from base models @@ -133,10 +132,10 @@ function validationAfterImportMerge(model: Model) { } // at most one `@@auth` model - const dataModels = getDataModels(model, true); - const authModels = dataModels.filter((d) => hasAttribute(d, '@@auth')); - if (authModels.length > 1) { - console.error(colors.red('Validation error: Multiple `@@auth` models are not allowed')); + const decls = getDataModelAndTypeDefs(model, true); + const authDecls = decls.filter((d) => hasAttribute(d, '@@auth')); + if (authDecls.length > 1) { + console.error(colors.red('Validation error: Multiple `@@auth` declarations are not allowed')); throw new CliError('schema validation errors'); } } diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 4158bd256..a13ca2afc 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -136,6 +136,8 @@ export class PluginRunner { let dmmf: DMMF.Document | undefined = undefined; let shortNameMap: Map | undefined; let prismaClientPath = '@prisma/client'; + let prismaClientDtsPath: string | undefined = undefined; + const project = createProject(); for (const { name, description, run, options: pluginOptions } of corePlugins) { const options = { ...pluginOptions, prismaClientPath }; @@ -165,6 +167,7 @@ export class PluginRunner { if (r.prismaClientPath) { // use the prisma client path returned by the plugin prismaClientPath = r.prismaClientPath; + prismaClientDtsPath = r.prismaClientDtsPath; } } @@ -173,13 +176,13 @@ export class PluginRunner { // run user plugins for (const { name, description, run, options: pluginOptions } of userPlugins) { - const options = { ...pluginOptions, prismaClientPath }; + const options = { ...pluginOptions, prismaClientPath, prismaClientDtsPath }; const r = await this.runPlugin( name, description, run, runnerOptions, - options, + options as PluginOptions, dmmf, shortNameMap, project, diff --git a/packages/schema/src/language-server/utils.ts b/packages/schema/src/language-server/utils.ts index 019004a62..2d31975a6 100644 --- a/packages/schema/src/language-server/utils.ts +++ b/packages/schema/src/language-server/utils.ts @@ -1,6 +1,9 @@ import { isArrayExpr, + isDataModel, isReferenceExpr, + isTypeDef, + TypeDef, type DataModel, type DataModelField, type ReferenceExpr, @@ -25,3 +28,10 @@ export function getUniqueFields(model: DataModel) { .map((item) => resolved(item.target) as DataModelField); }); } + +/** + * Checks if the given node can contain resolvable members. + */ +export function isMemberContainer(node: unknown): node is DataModel | TypeDef { + return isDataModel(node) || isTypeDef(node); +} diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 2a7f43c29..a7c0fef9a 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -69,6 +69,10 @@ export default class AttributeApplicationValidator implements AstValidator(); for (const arg of attr.args) { diff --git a/packages/schema/src/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts index f37886c93..8c11a2a72 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -15,7 +15,7 @@ import { } from '@zenstackhq/language/ast'; import { ExpressionContext, - getDataModelFieldReference, + getFieldReference, getFunctionExpressionContext, getLiteral, isDataModelFieldReference, @@ -96,7 +96,7 @@ export default class FunctionInvocationValidator implements AstValidator { } // at most one `@@auth` model - const authModels = model.declarations.filter((d) => isDataModel(d) && hasAttribute(d, '@@auth')); + const decls = getDataModelAndTypeDefs(model, true); + const authModels = decls.filter((d) => isDataModel(d) && hasAttribute(d, '@@auth')); if (authModels.length > 1) { accept('error', 'Multiple `@@auth` models are not allowed', { node: authModels[1] }); } diff --git a/packages/schema/src/language-server/validator/typedef-validator.ts b/packages/schema/src/language-server/validator/typedef-validator.ts index 55c127d7d..70b6ec860 100644 --- a/packages/schema/src/language-server/validator/typedef-validator.ts +++ b/packages/schema/src/language-server/validator/typedef-validator.ts @@ -10,9 +10,14 @@ import { validateDuplicatedDeclarations } from './utils'; export default class TypeDefValidator implements AstValidator { validate(typeDef: TypeDef, accept: ValidationAcceptor): void { validateDuplicatedDeclarations(typeDef, typeDef.fields, accept); + this.validateAttributes(typeDef, accept); this.validateFields(typeDef, accept); } + private validateAttributes(typeDef: TypeDef, accept: ValidationAcceptor) { + typeDef.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); + } + private validateFields(typeDef: TypeDef, accept: ValidationAcceptor) { typeDef.fields.forEach((field) => this.validateField(field, accept)); } diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 3dd5b537b..032bf9a5e 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -1,20 +1,12 @@ import { - AttributeArg, - AttributeParam, BuiltinType, - DataModelAttribute, - DataModelFieldAttribute, Expression, ExpressionType, - InternalAttribute, - isArrayExpr, isDataModelField, - isEnum, isMemberAccessExpr, - isReferenceExpr, isStringLiteral, } from '@zenstackhq/language/ast'; -import { isAuthInvocation, resolved } from '@zenstackhq/sdk'; +import { isAuthInvocation } from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; /** @@ -109,80 +101,6 @@ export function mapBuiltinTypeToExpressionType( } } -/** - * Determines if the given attribute argument is assignable to the given attribute parameter - */ -export function assignableToAttributeParam( - arg: AttributeArg, - param: AttributeParam, - attr: DataModelAttribute | DataModelFieldAttribute | InternalAttribute -): boolean { - const argResolvedType = arg.$resolvedType; - if (!argResolvedType) { - return false; - } - - let dstType = param.type.type; - let dstIsArray = param.type.array; - const dstRef = param.type.reference; - - if (dstType === 'Any' && !dstIsArray) { - return true; - } - - // destination is field reference or transitive field reference, check if - // argument is reference or array or reference - if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { - if (dstIsArray) { - return ( - isArrayExpr(arg.value) && - !arg.value.items.find((item) => !isReferenceExpr(item) || !isDataModelField(item.target.ref)) - ); - } else { - return isReferenceExpr(arg.value) && isDataModelField(arg.value.target.ref); - } - } - - if (isEnum(argResolvedType.decl)) { - // enum type - - let attrArgDeclType = dstRef?.ref; - if (dstType === 'ContextType' && isDataModelField(attr.$container) && attr.$container?.type?.reference) { - // attribute parameter type is ContextType, need to infer type from - // the attribute's container - attrArgDeclType = resolved(attr.$container.type.reference); - dstIsArray = attr.$container.type.array; - } - return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array; - } else if (dstType) { - // scalar type - - if (typeof argResolvedType?.decl !== 'string') { - // destination type is not a reference, so argument type must be a plain expression - return false; - } - - if (dstType === 'ContextType') { - // attribute parameter type is ContextType, need to infer type from - // the attribute's container - if (isDataModelField(attr.$container)) { - if (!attr.$container?.type?.type) { - return false; - } - dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type); - dstIsArray = attr.$container.type.array; - } else { - dstType = 'Any'; - } - } - - return typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array; - } else { - // reference type - return (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array; - } -} - export function isAuthOrAuthMemberAccess(expr: Expression): boolean { return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand)); } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index c2751b921..1e2491bda 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -24,6 +24,7 @@ import { ResolvedShape, StringLiteral, ThisExpr, + TypeDefFieldType, UnaryExpr, isArrayExpr, isBooleanLiteral, @@ -34,8 +35,9 @@ import { isNumberLiteral, isReferenceExpr, isStringLiteral, + isTypeDefField, } from '@zenstackhq/language/ast'; -import { getAuthModel, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; +import { getAuthDecl, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -53,7 +55,8 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { getAllLoadedAndReachableDataModels, getContainingDataModel } from '../utils/ast-utils'; +import { getAllLoadedAndReachableDataModelsAndTypeDefs, getContainingDataModel } from '../utils/ast-utils'; +import { isMemberContainer } from './utils'; import { mapBuiltinTypeToExpressionType } from './validator/utils'; interface DefaultReference extends Reference { @@ -281,14 +284,14 @@ export class ZModelLinker extends DefaultLinker { // auth() function is resolved against all loaded and reachable documents // get all data models from loaded and reachable documents - const allDataModels = getAllLoadedAndReachableDataModels( + const allDecls = getAllLoadedAndReachableDataModelsAndTypeDefs( this.langiumDocuments(), getContainerOfType(node, isDataModel) ); - const authModel = getAuthModel(allDataModels); - if (authModel) { - node.$resolvedType = { decl: authModel, nullable: true }; + const authDecl = getAuthDecl(allDecls); + if (authDecl) { + node.$resolvedType = { decl: authDecl, nullable: true }; } } else if (isFutureExpr(node)) { // future() function is resolved to current model @@ -319,7 +322,7 @@ export class ZModelLinker extends DefaultLinker { this.resolveDefault(node, document, extraScopes); const operandResolved = node.operand.$resolvedType; - if (operandResolved && !operandResolved.array && isDataModel(operandResolved.decl)) { + if (operandResolved && !operandResolved.array && isMemberContainer(operandResolved.decl)) { // member access is resolved only in the context of the operand type if (node.member.ref) { this.resolveToDeclaredType(node, node.member.ref.type); @@ -337,7 +340,7 @@ export class ZModelLinker extends DefaultLinker { this.resolveDefault(node, document, extraScopes); const resolvedType = node.left.$resolvedType; - if (resolvedType && isDataModel(resolvedType.decl) && resolvedType.array) { + if (resolvedType && isMemberContainer(resolvedType.decl) && resolvedType.array) { this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); } else { // error is reported in validation pass @@ -513,9 +516,9 @@ export class ZModelLinker extends DefaultLinker { //#region Utils - private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType) { + private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType | TypeDefFieldType) { let nullable = false; - if (isDataModelFieldType(type)) { + if (isDataModelFieldType(type) || isTypeDefField(type)) { nullable = type.optional; // referencing a field of 'Unsupported' type diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index cde2d4b5a..11cbb4909 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -9,8 +9,10 @@ import { isModel, isReferenceExpr, isThisExpr, + isTypeDef, + isTypeDefField, } from '@zenstackhq/language/ast'; -import { getAuthModel, getModelFieldsWithBases, getRecursiveBases, isAuthInvocation } from '@zenstackhq/sdk'; +import { getAuthDecl, getModelFieldsWithBases, getRecursiveBases, isAuthInvocation } from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -32,12 +34,13 @@ import { import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; import { - getAllLoadedAndReachableDataModels, + getAllLoadedAndReachableDataModelsAndTypeDefs, isCollectionPredicate, isFutureInvocation, resolveImportUri, } from '../utils/ast-utils'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; +import { isAuthOrAuthMemberAccess } from './validator/utils'; /** * Custom Langium ScopeComputation implementation which adds enum fields into global scope @@ -133,22 +136,27 @@ export class ZModelScopeProvider extends DefaultScopeProvider { const globalScope = this.getGlobalScope(referenceType, context); const node = context.container as MemberAccessExpr; + // typedef's fields are only added to the scope if the access starts with `auth().` + // or the member access resides inside a typedef + const allowTypeDefScope = isAuthOrAuthMemberAccess(node.operand) || !!getContainerOfType(node, isTypeDef); + return match(node.operand) .when(isReferenceExpr, (operand) => { - // operand is a reference, it can only be a model field + // operand is a reference, it can only be a model/type-def field const ref = operand.target.ref; - if (isDataModelField(ref)) { - const targetModel = ref.type.reference?.ref; - return this.createScopeForModel(targetModel, globalScope); + if (isDataModelField(ref) || isTypeDefField(ref)) { + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) .when(isMemberAccessExpr, (operand) => { - // operand is a member access, it must be resolved to a non-array data model type + // operand is a member access, it must be resolved to a non-array model/typedef type const ref = operand.member.ref; if (isDataModelField(ref) && !ref.type.array) { - const targetModel = ref.type.reference?.ref; - return this.createScopeForModel(targetModel, globalScope); + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); + } + if (isTypeDefField(ref) && !ref.type.array) { + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) @@ -159,8 +167,8 @@ export class ZModelScopeProvider extends DefaultScopeProvider { .when(isInvocationExpr, (operand) => { // deal with member access from `auth()` and `future() if (isAuthInvocation(operand)) { - // resolve to `User` or `@@auth` model - return this.createScopeForAuthModel(node, globalScope); + // resolve to `User` or `@@auth` decl + return this.createScopeForAuth(node, globalScope); } if (isFutureInvocation(operand)) { // resolve `future()` to the containing model @@ -176,27 +184,28 @@ export class ZModelScopeProvider extends DefaultScopeProvider { const globalScope = this.getGlobalScope(referenceType, context); const collection = collectionPredicate.left; + // typedef's fields are only added to the scope if the access starts with `auth().` + const allowTypeDefScope = isAuthOrAuthMemberAccess(collection); + return match(collection) .when(isReferenceExpr, (expr) => { - // collection is a reference, it can only be a model field + // collection is a reference - model or typedef field const ref = expr.target.ref; - if (isDataModelField(ref)) { - const targetModel = ref.type.reference?.ref; - return this.createScopeForModel(targetModel, globalScope); + if (isDataModelField(ref) || isTypeDefField(ref)) { + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) .when(isMemberAccessExpr, (expr) => { - // collection is a member access, it can only be resolved to a model field + // collection is a member access, it can only be resolved to a model or typedef field const ref = expr.member.ref; - if (isDataModelField(ref)) { - const targetModel = ref.type.reference?.ref; - return this.createScopeForModel(targetModel, globalScope); + if (isDataModelField(ref) || isTypeDefField(ref)) { + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) .when(isAuthInvocation, (expr) => { - return this.createScopeForAuthModel(expr, globalScope); + return this.createScopeForAuth(expr, globalScope); }) .otherwise(() => EMPTY_SCOPE); } @@ -204,30 +213,32 @@ export class ZModelScopeProvider extends DefaultScopeProvider { private createScopeForContainingModel(node: AstNode, globalScope: Scope) { const model = getContainerOfType(node, isDataModel); if (model) { - return this.createScopeForModel(model, globalScope); + return this.createScopeForContainer(model, globalScope); } else { return EMPTY_SCOPE; } } - private createScopeForModel(node: AstNode | undefined, globalScope: Scope) { + private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false) { if (isDataModel(node)) { return this.createScopeForNodes(getModelFieldsWithBases(node), globalScope); + } else if (includeTypeDefScope && isTypeDef(node)) { + return this.createScopeForNodes(node.fields, globalScope); } else { return EMPTY_SCOPE; } } - private createScopeForAuthModel(node: AstNode, globalScope: Scope) { - // get all data models from loaded and reachable documents - const allDataModels = getAllLoadedAndReachableDataModels( + private createScopeForAuth(node: AstNode, globalScope: Scope) { + // get all data models and type defs from loaded and reachable documents + const decls = getAllLoadedAndReachableDataModelsAndTypeDefs( this.services.shared.workspace.LangiumDocuments, getContainerOfType(node, isDataModel) ); - const authModel = getAuthModel(allDataModels); - if (authModel) { - return this.createScopeForModel(authModel, globalScope); + const authDecl = getAuthDecl(decls); + if (authDecl) { + return this.createScopeForContainer(authDecl, globalScope, true); } else { return EMPTY_SCOPE; } diff --git a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts index a4e09fbb2..3736682ed 100644 --- a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts +++ b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts @@ -5,6 +5,7 @@ import { Expression, isDataModel, isMemberAccessExpr, + TypeDef, type Model, } from '@zenstackhq/sdk/ast'; import { streamAst, type AstNode } from 'langium'; @@ -14,7 +15,7 @@ import { isCollectionPredicate } from '../../../utils/ast-utils'; * Generate types for typing the `user` context object passed to the `enhance` call, based * on the fields (potentially deeply) access through `auth()`. */ -export function generateAuthType(model: Model, authModel: DataModel) { +export function generateAuthType(model: Model, authDecl: DataModel | TypeDef) { const types = new Map< string, { @@ -23,7 +24,7 @@ export function generateAuthType(model: Model, authModel: DataModel) { } >(); - types.set(authModel.name, { requiredRelations: [] }); + types.set(authDecl.name, { requiredRelations: [] }); const ensureType = (model: string) => { if (!types.has(model)) { @@ -88,9 +89,9 @@ ${Array.from(types.entries()) .map(([model, fields]) => { let result = `Partial<_P.${model}>`; - if (model === authModel.name) { + if (model === authDecl.name) { // auth model's id fields are always required - const idFields = getIdFields(authModel).map((f) => f.name); + const idFields = getIdFields(authDecl).map((f) => f.name); if (idFields.length > 0) { result = `WithRequired<${result}, ${idFields.map((f) => `'${f}'`).join('|')}>`; } diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 0b69b6a25..404c16cd8 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -3,12 +3,15 @@ import { PluginError, getAttribute, getAttributeArg, - getAuthModel, + getAuthDecl, + getDataModelAndTypeDefs, getDataModels, getLiteral, + getRelationField, isDelegateModel, isDiscriminatorField, normalizedRelative, + saveSourceFile, type PluginOptions, } from '@zenstackhq/sdk'; import { @@ -54,21 +57,32 @@ type DelegateInfo = [DataModel, DataModel[]][]; const LOGICAL_CLIENT_GENERATION_PATH = './.logical-prisma-client'; export class EnhancerGenerator { + // regex for matching "ModelCreateXXXInput" and "ModelUncheckedCreateXXXInput" type + // names for models that use `auth()` in `@default` attribute + private readonly modelsWithAuthInDefaultCreateInputPattern: RegExp; + constructor( private readonly model: Model, private readonly options: PluginOptions, private readonly project: Project, private readonly outDir: string - ) {} + ) { + const modelsWithAuthInDefault = this.model.declarations.filter( + (d): d is DataModel => isDataModel(d) && d.fields.some((f) => f.attributes.some(isDefaultWithAuth)) + ); + this.modelsWithAuthInDefaultCreateInputPattern = new RegExp( + `^(${modelsWithAuthInDefault.map((m) => m.name).join('|')})(Unchecked)?Create.*?Input$` + ); + } - async generate(): Promise<{ dmmf: DMMF.Document | undefined }> { + async generate(): Promise<{ dmmf: DMMF.Document | undefined; newPrismaClientDtsPath: string | undefined }> { let dmmf: DMMF.Document | undefined; const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); let prismaTypesFixed = false; let resultPrismaImport = prismaImport; - if (this.needsLogicalClient || this.needsPrismaClientTypeFixes) { + if (this.needsLogicalClient) { prismaTypesFixed = true; resultPrismaImport = `${LOGICAL_CLIENT_GENERATION_PATH}/index-fixed`; const result = await this.generateLogicalPrisma(); @@ -83,9 +97,9 @@ export class EnhancerGenerator { ); await prismaDts.save(); - const authModel = getAuthModel(getDataModels(this.model)); - const authTypes = authModel ? generateAuthType(this.model, authModel) : ''; - const authTypeParam = authModel ? `auth.${authModel.name}` : 'AuthUser'; + const authDecl = getAuthDecl(getDataModelAndTypeDefs(this.model)); + const authTypes = authDecl ? generateAuthType(this.model, authDecl) : ''; + const authTypeParam = authDecl ? `auth.${authDecl.name}` : 'AuthUser'; const checkerTypes = this.generatePermissionChecker ? generateCheckerType(this.model) : ''; @@ -124,10 +138,15 @@ ${ { overwrite: true } ); - await this.saveSourceFile(enhanceTs); + this.saveSourceFile(enhanceTs); } - return { dmmf }; + return { + dmmf, + newPrismaClientDtsPath: prismaTypesFixed + ? path.resolve(this.outDir, LOGICAL_CLIENT_GENERATION_PATH, 'index-fixed.d.ts') + : undefined, + }; } private getZodImport() { @@ -224,11 +243,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara } private get needsLogicalClient() { - return this.hasDelegateModel(this.model) || this.hasAuthInDefault(this.model); - } - - private get needsPrismaClientTypeFixes() { - return this.hasTypeDef(this.model); + return this.hasDelegateModel(this.model) || this.hasAuthInDefault(this.model) || this.hasTypeDef(this.model); } private hasDelegateModel(model: Model) { @@ -443,11 +458,13 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara const auxFields = this.findAuxDecls(variable); if (auxFields.length > 0) { structure.declarations.forEach((variable) => { - let source = variable.type?.toString(); - auxFields.forEach((f) => { - source = source?.replace(f.getText(), ''); - }); - variable.type = source; + if (variable.type) { + let source = variable.type.toString(); + auxFields.forEach((f) => { + source = this.removeFromSource(source, f.getText()); + }); + variable.type = source; + } }); } @@ -492,6 +509,9 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara // fix delegate payload union type source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); + // fix fk and relation fields related to using `auth()` in `@default` + source = this.fixDefaultAuthType(typeAlias, source); + // fix json field type source = this.fixJsonFieldType(typeAlias, source); @@ -499,65 +519,6 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara return structure; } - private fixJsonFieldType(typeAlias: TypeAliasDeclaration, source: string) { - const modelsWithTypeField = this.model.declarations.filter( - (d): d is DataModel => isDataModel(d) && d.fields.some((f) => isTypeDef(f.type.reference?.ref)) - ); - const typeName = typeAlias.getName(); - - const getTypedJsonFields = (model: DataModel) => { - return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); - }; - - const replacePrismaJson = (source: string, field: DataModelField) => { - return source.replace( - new RegExp(`(${field.name}\\??\\s*):[^\\n]+`), - `$1: ${field.type.reference!.$refText}${field.type.array ? '[]' : ''}${ - field.type.optional ? ' | null' : '' - }` - ); - }; - - // fix "$[Model]Payload" type - const payloadModelMatch = modelsWithTypeField.find((m) => `$${m.name}Payload` === typeName); - if (payloadModelMatch) { - const scalars = typeAlias - .getDescendantsOfKind(SyntaxKind.PropertySignature) - .find((p) => p.getName() === 'scalars'); - if (!scalars) { - return source; - } - - const fieldsToFix = getTypedJsonFields(payloadModelMatch); - for (const field of fieldsToFix) { - source = replacePrismaJson(source, field); - } - } - - // fix input/output types, "[Model]CreateInput", etc. - const inputOutputModelMatch = modelsWithTypeField.find((m) => typeName.startsWith(m.name)); - if (inputOutputModelMatch) { - const relevantTypePatterns = [ - 'GroupByOutputType', - '(Unchecked)?Create(\\S+?)?Input', - '(Unchecked)?Update(\\S+?)?Input', - 'CreateManyInput', - '(Unchecked)?UpdateMany(Mutation)?Input', - ]; - const typeRegex = modelsWithTypeField.map( - (m) => new RegExp(`^(${m.name})(${relevantTypePatterns.join('|')})$`) - ); - if (typeRegex.some((r) => r.test(typeName))) { - const fieldsToFix = getTypedJsonFields(inputOutputModelMatch); - for (const field of fieldsToFix) { - source = replacePrismaJson(source, field); - } - } - } - - 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(); @@ -589,7 +550,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara .getDescendantsOfKind(SyntaxKind.PropertySignature) .filter((p) => ['create', 'createMany', 'connectOrCreate', 'upsert'].includes(p.getName())); toRemove.forEach((r) => { - source = source.replace(r.getText(), ''); + this.removeFromSource(source, r.getText()); }); } return source; @@ -627,7 +588,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara if (isDiscriminatorField(field)) { const fieldDef = this.findNamedProperty(typeAlias, field.name); if (fieldDef) { - source = source.replace(fieldDef.getText(), ''); + source = this.removeFromSource(source, fieldDef.getText()); } } } @@ -640,7 +601,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara const auxDecls = this.findAuxDecls(typeAlias); if (auxDecls.length > 0) { auxDecls.forEach((d) => { - source = source.replace(d.getText(), ''); + source = this.removeFromSource(source, d.getText()); }); } return source; @@ -671,7 +632,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara const fieldDef = this.findNamedProperty(typeAlias, relationFieldName); if (fieldDef) { // remove relation field of delegate type, e.g., `asset` - source = source.replace(fieldDef.getText(), ''); + source = this.removeFromSource(source, fieldDef.getText()); } // remove fk fields related to the delegate type relation, e.g., `assetId` @@ -703,13 +664,103 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara fkFields.forEach((fkField) => { const fieldDef = this.findNamedProperty(typeAlias, fkField); if (fieldDef) { - source = source.replace(fieldDef.getText(), ''); + source = this.removeFromSource(source, fieldDef.getText()); } }); return source; } + private fixDefaultAuthType(typeAlias: TypeAliasDeclaration, source: string) { + const match = typeAlias.getName().match(this.modelsWithAuthInDefaultCreateInputPattern); + if (!match) { + return source; + } + + const modelName = match[1]; + const dataModel = this.model.declarations.find((d): d is DataModel => isDataModel(d) && d.name === modelName); + if (dataModel) { + for (const fkField of dataModel.fields.filter((f) => f.attributes.some(isDefaultWithAuth))) { + // change fk field to optional since it has a default + source = source.replace(new RegExp(`^(\\s*${fkField.name}\\s*):`, 'm'), `$1?:`); + + const relationField = getRelationField(fkField); + if (relationField) { + // change relation field to optional since its fk has a default + source = source.replace(new RegExp(`^(\\s*${relationField.name}\\s*):`, 'm'), `$1?:`); + } + } + } + return source; + } + + private fixJsonFieldType(typeAlias: TypeAliasDeclaration, source: string) { + const modelsWithTypeField = this.model.declarations.filter( + (d): d is DataModel => isDataModel(d) && d.fields.some((f) => isTypeDef(f.type.reference?.ref)) + ); + const typeName = typeAlias.getName(); + + const getTypedJsonFields = (model: DataModel) => { + return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); + }; + + const replacePrismaJson = (source: string, field: DataModelField) => { + return source.replace( + new RegExp(`(${field.name}\\??\\s*):[^\\n]+`), + `$1: ${field.type.reference!.$refText}${field.type.array ? '[]' : ''}${ + field.type.optional ? ' | null' : '' + }` + ); + }; + + // fix "$[Model]Payload" type + const payloadModelMatch = modelsWithTypeField.find((m) => `$${m.name}Payload` === typeName); + if (payloadModelMatch) { + const scalars = typeAlias + .getDescendantsOfKind(SyntaxKind.PropertySignature) + .find((p) => p.getName() === 'scalars'); + if (!scalars) { + return source; + } + + const fieldsToFix = getTypedJsonFields(payloadModelMatch); + for (const field of fieldsToFix) { + source = replacePrismaJson(source, field); + } + } + + // fix input/output types, "[Model]CreateInput", etc. + const inputOutputModelMatch = modelsWithTypeField.find((m) => typeName.startsWith(m.name)); + if (inputOutputModelMatch) { + const relevantTypePatterns = [ + 'GroupByOutputType', + '(Unchecked)?Create(\\S+?)?Input', + '(Unchecked)?Update(\\S+?)?Input', + 'CreateManyInput', + '(Unchecked)?UpdateMany(Mutation)?Input', + ]; + const typeRegex = modelsWithTypeField.map( + (m) => new RegExp(`^(${m.name})(${relevantTypePatterns.join('|')})$`) + ); + if (typeRegex.some((r) => r.test(typeName))) { + const fieldsToFix = getTypedJsonFields(inputOutputModelMatch); + for (const field of fieldsToFix) { + source = replacePrismaJson(source, field); + } + } + } + + return source; + } + + private async generateExtraTypes(sf: SourceFile) { + for (const decl of this.model.declarations) { + if (isTypeDef(decl)) { + generateTypeDefType(sf, decl); + } + } + } + private findNamedProperty(typeAlias: TypeAliasDeclaration, name: string) { return typeAlias.getFirstDescendant((d) => d.isKind(SyntaxKind.PropertySignature) && d.getName() === name); } @@ -729,9 +780,9 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara return isReferenceExpr(arg) ? (arg.target.ref as DataModelField) : undefined; } - private async saveSourceFile(sf: SourceFile) { + private saveSourceFile(sf: SourceFile) { if (this.options.preserveTsFiles) { - await sf.save(); + saveSourceFile(sf); } } @@ -739,11 +790,12 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara return this.options.generatePermissionChecker === true; } - private async generateExtraTypes(sf: SourceFile) { - for (const decl of this.model.declarations) { - if (isTypeDef(decl)) { - generateTypeDefType(sf, decl); - } - } + private removeFromSource(source: string, text: string) { + source = source.replace(text, ''); + return this.trimEmptyLines(source); + } + + private trimEmptyLines(source: string): string { + return source.replace(/^\s*[\r\n]/gm, ''); } } diff --git a/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts b/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts index 21ede7a64..00f16095f 100644 --- a/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts +++ b/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts @@ -17,8 +17,11 @@ export function generateTypeDefType(sourceFile: SourceFile, decl: TypeDef) { field.comments.forEach((c) => writer.writeLine(` * ${unwrapTripleSlashComment(c)}`)); writer.writeLine(` */`); } + // optional fields are also nullable (to be consistent with Prisma) writer.writeLine( - ` ${field.name}${field.type.optional ? '?' : ''}: ${zmodelTypeToTsType(field.type)};` + ` ${field.name}${field.type.optional ? '?' : ''}: ${zmodelTypeToTsType(field.type)}${ + field.type.optional ? ' | null' : '' + };` ); }); }); diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index c0cd7e13d..c0bd93564 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -26,7 +26,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { await generateModelMeta(model, options, project, outDir); await generatePolicy(model, options, project, outDir); - const { dmmf } = await new EnhancerGenerator(model, options, project, outDir).generate(); + const { dmmf, newPrismaClientDtsPath } = await new EnhancerGenerator(model, options, project, outDir).generate(); let prismaClientPath: string | undefined; if (dmmf) { @@ -44,7 +44,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { } } - return { dmmf, warnings: [], prismaClientPath }; + return { dmmf, warnings: [], prismaClientPath, prismaClientDtsPath: newPrismaClientDtsPath }; }; export default run; diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index 62469f744..8206f797b 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -26,6 +26,7 @@ import { hasValidationAttributes, isAuthInvocation, isForeignKeyField, + saveSourceFile, } from '@zenstackhq/sdk'; import { getPrismaClientImportSpec } from '@zenstackhq/sdk/prisma'; import { streamAst } from 'langium'; @@ -57,9 +58,8 @@ import { export class PolicyGenerator { constructor(private options: PluginOptions) {} - async generate(project: Project, model: Model, output: string) { + generate(project: Project, model: Model, output: string) { const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); this.writeImports(model, output, sf); @@ -87,7 +87,7 @@ export class PolicyGenerator { // save ts files if requested explicitly or the user provided const preserveTsFiles = this.options.preserveTsFiles === true || !!this.options.output; if (preserveTsFiles) { - await sf.save(); + saveSourceFile(sf); } } diff --git a/packages/schema/src/plugins/enhancer/policy/utils.ts b/packages/schema/src/plugins/enhancer/policy/utils.ts index f09263dca..fee0cc15a 100644 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ b/packages/schema/src/plugins/enhancer/policy/utils.ts @@ -6,7 +6,8 @@ import { TypeScriptExpressionTransformer, TypeScriptExpressionTransformerError, getAttributeArg, - getAuthModel, + getAuthDecl, + getDataModelAndTypeDefs, getDataModels, getEntityCheckerFunctionName, getIdFields, @@ -519,7 +520,7 @@ export function generateNormalizedAuthRef( const hasAuthRef = [...allows, ...denies].some((rule) => streamAst(rule).some((child) => isAuthInvocation(child))); if (hasAuthRef) { - const authModel = getAuthModel(getDataModels(model.$container, true)); + const authModel = getAuthDecl(getDataModelAndTypeDefs(model.$container, true)); if (!authModel) { throw new PluginError(name, 'Auth model not found'); } @@ -537,16 +538,19 @@ export function generateNormalizedAuthRef( * Check if the given enum is referenced in the model */ export function isEnumReferenced(model: Model, decl: Enum): unknown { - return streamAllContents(model).some((node) => { - if (isDataModelField(node) && node.type.reference?.ref === decl) { - // referenced as field type - return true; - } - if (isEnumFieldReference(node) && node.target.ref?.$container === decl) { - // enum field is referenced - return true; - } - return false; + const dataModels = getDataModels(model); + return dataModels.some((dm) => { + return streamAllContents(dm).some((node) => { + if (isDataModelField(node) && node.type.reference?.ref === decl) { + // referenced as field type + return true; + } + if (isEnumFieldReference(node) && node.target.ref?.$container === decl) { + // enum field is referenced + return true; + } + return false; + }); }); } diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index b443e6ca7..e1bbda408 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -1,9 +1,10 @@ import { DEFAULT_RUNTIME_LOAD_PATH, type PolicyOperationKind } from '@zenstackhq/runtime'; -import { PluginGlobalOptions, ensureEmptyDir, getLiteral } from '@zenstackhq/sdk'; +import { ensureEmptyDir, getLiteral, PluginGlobalOptions } from '@zenstackhq/sdk'; +import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast'; import fs from 'fs'; import path from 'path'; import { PluginRunnerOptions } from '../cli/plugin-runner'; -import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast'; +import { getVersion } from '../utils/version-utils'; export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; @@ -33,8 +34,8 @@ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { ensureEmptyDir(output); if (!options.output) { const pkgJson = { - name: '.zenstack', - version: '1.0.0', + name: 'zenstack-generated', + version: getVersion() ?? '1.0.0', exports: { './enhance': { types: './enhance.d.ts', diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 7fa94cd92..9849cc938 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -28,8 +28,16 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { const mergedOptions = { ...options, output } as unknown as PluginOptions; const { warnings, shortNameMap } = await new PrismaSchemaGenerator(model).generate(mergedOptions); + + // the path to import the prisma client from let prismaClientPath = '@prisma/client'; + // the real path where the prisma client was generated + let clientOutputDir = '.prisma/client'; + + // the path to the prisma client dts file + let prismaClientDtsPath: string | undefined = undefined; + if (options.generateClient !== false) { let generateCmd = `prisma generate --schema "${output}"`; if (typeof options.generateArgs === 'string') { @@ -68,6 +76,23 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { // then make it relative to the zmodel schema location prismaClientPath = normalizedRelative(path.dirname(options.schemaPath), absPath); } + + // record custom location where the prisma client was generated + clientOutputDir = prismaClientPath; + } + + // get PrismaClient dts path + try { + const prismaClientResolvedPath = require.resolve(clientOutputDir, { + paths: [path.dirname(options.schemaPath)], + }); + prismaClientDtsPath = path.join(path.dirname(prismaClientResolvedPath), 'index.d.ts'); + } catch (err) { + console.warn( + colors.yellow( + `Could not resolve PrismaClient type declaration path. This may break plugins that depend on it.` + ) + ); } } else { console.warn( @@ -82,7 +107,7 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { datamodel: fs.readFileSync(output, 'utf-8'), }); - return { warnings, dmmf, prismaClientPath, shortNameMap }; + return { warnings, dmmf, prismaClientPath, prismaClientDtsPath, shortNameMap }; }; function getDefaultPrismaOutputFile(schemaPath: string) { diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index b3d382795..cdc37cc81 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -30,9 +30,9 @@ import { ReferenceExpr, StringLiteral, } from '@zenstackhq/language/ast'; +import { getIdFields } from '@zenstackhq/sdk'; import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; -import { match, P } from 'ts-pattern'; -import { getIdFields } from '../../utils/ast-utils'; +import { match } from 'ts-pattern'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { @@ -838,14 +838,6 @@ export class PrismaSchemaGenerator { const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)]; const result = model.addField(field.name, type, attributes, docs, 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); - } - } - return result; } @@ -856,23 +848,6 @@ export class PrismaSchemaGenerator { } } - 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); } diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 01a5920ff..46e6505fe 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,11 +1,18 @@ import { + ExpressionContext, PluginError, PluginGlobalOptions, PluginOptions, RUNTIME_PACKAGE, + TypeScriptExpressionTransformer, + TypeScriptExpressionTransformerError, ensureEmptyDir, + getAttributeArg, + getAttributeArgLiteral, getDataModels, + getLiteralArray, hasAttribute, + isDataModelFieldReference, isDiscriminatorField, isEnumFieldReference, isForeignKeyField, @@ -13,8 +20,9 @@ import { isIdField, parseOptionAsStrings, resolvePath, + saveSourceFile, } from '@zenstackhq/sdk'; -import { DataModel, EnumField, Model, TypeDef, isDataModel, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; +import { DataModel, EnumField, Model, TypeDef, isArrayExpr, isDataModel, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; import { streamAllContents } from 'langium'; @@ -25,7 +33,7 @@ import { name } from '.'; import { getDefaultOutputFolder } from '../plugin-utils'; import Transformer from './transformer'; import { ObjectMode } from './types'; -import { makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; +import { makeFieldSchema } from './utils/schema-gen'; export class ZodSchemaGenerator { private readonly sourceFiles: SourceFile[] = []; @@ -143,12 +151,7 @@ export class ZodSchemaGenerator { if (this.options.preserveTsFiles === true || this.options.output) { // if preserveTsFiles is true or the user provided a custom output directory, // save the generated files - await Promise.all( - this.sourceFiles.map(async (sf) => { - await sf.formatText(); - await sf.save(); - }) - ); + this.sourceFiles.forEach(saveSourceFile); } } @@ -298,7 +301,7 @@ export class ZodSchemaGenerator { sf.replaceWithText((writer) => { this.addPreludeAndImports(typeDef, writer, output); - writer.write(`export const ${typeDef.name}Schema = z.object(`); + writer.write(`const baseSchema = z.object(`); writer.inlineBlock(() => { typeDef.fields.forEach((field) => { writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); @@ -317,15 +320,29 @@ export class ZodSchemaGenerator { writer.writeLine(').strict();'); break; } - }); - // TODO: "@@validate" refinements + // compile "@@validate" to a function calling zod's `.refine()` + const refineFuncName = this.createRefineFunction(typeDef, writer); + + if (refineFuncName) { + // export a schema without refinement for extensibility: `[Model]WithoutRefineSchema` + const noRefineSchema = `${upperCaseFirst(typeDef.name)}WithoutRefineSchema`; + writer.writeLine(` +/** + * \`${typeDef.name}\` schema prior to calling \`.refine()\` for extensibility. + */ +export const ${noRefineSchema} = baseSchema; +export const ${typeDef.name}Schema = ${refineFuncName}(${noRefineSchema}); +`); + } else { + writer.writeLine(`export const ${typeDef.name}Schema = baseSchema;`); + } + }); return schemaName; } private addPreludeAndImports(decl: DataModel | TypeDef, writer: CodeBlockWriter, output: string) { - writer.writeLine('/* eslint-disable */'); writer.writeLine(`import { z } from 'zod';`); // import user-defined enums from Prisma as they might be referenced in the expressions @@ -441,22 +458,7 @@ export class ZodSchemaGenerator { } // compile "@@validate" to ".refine" - const refinements = makeValidationRefinements(model); - let refineFuncName: string | undefined; - if (refinements.length > 0) { - refineFuncName = `refine${upperCaseFirst(model.name)}`; - writer.writeLine( - ` -/** - * Schema refinement function for applying \`@@validate\` rules. - */ -export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join( - '\n' - )}; -} -` - ); - } + const refineFuncName = this.createRefineFunction(model, writer); // delegate discriminator fields are to be excluded from mutation schemas const delegateDiscriminatorFields = model.fields.filter((field) => isDiscriminatorField(field)); @@ -663,6 +665,74 @@ export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema}; return schemaName; } + private createRefineFunction(decl: DataModel | TypeDef, writer: CodeBlockWriter) { + const refinements = this.makeValidationRefinements(decl); + let refineFuncName: string | undefined; + if (refinements.length > 0) { + refineFuncName = `refine${upperCaseFirst(decl.name)}`; + writer.writeLine( + ` + /** + * Schema refinement function for applying \`@@validate\` rules. + */ + export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join( + '\n' + )}; + } + ` + ); + return refineFuncName; + } else { + return undefined; + } + } + + private makeValidationRefinements(decl: DataModel | TypeDef) { + const attrs = decl.attributes.filter((attr) => attr.decl.ref?.name === '@@validate'); + const refinements = attrs + .map((attr) => { + const valueArg = getAttributeArg(attr, 'value'); + if (!valueArg) { + return undefined; + } + + const messageArg = getAttributeArgLiteral(attr, 'message'); + const message = messageArg ? `message: ${JSON.stringify(messageArg)},` : ''; + + const pathArg = getAttributeArg(attr, 'path'); + const path = + pathArg && isArrayExpr(pathArg) + ? `path: ['${getLiteralArray(pathArg)?.join(`', '`)}'],` + : ''; + + const options = `, { ${message} ${path} }`; + + try { + let expr = new TypeScriptExpressionTransformer({ + context: ExpressionContext.ValidationRule, + fieldReferenceContext: 'value', + }).transform(valueArg); + + if (isDataModelFieldReference(valueArg)) { + // if the expression is a simple field reference, treat undefined + // as true since the all fields are optional in validation context + expr = `${expr} ?? true`; + } + + return `.refine((value: any) => ${expr}${options})`; + } catch (err) { + if (err instanceof TypeScriptExpressionTransformerError) { + throw new PluginError(name, err.message); + } else { + throw err; + } + } + }) + .filter((r) => !!r); + + return refinements; + } + private makePartial(schema: string, fields?: string[]) { if (fields) { if (fields.length === 0) { diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 081dcaf4a..db0c2a7bb 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { indentString, isDiscriminatorField, type PluginOptions } from '@zenstackhq/sdk'; -import { DataModel, Enum, isDataModel, isEnum, isTypeDef, type Model } from '@zenstackhq/sdk/ast'; +import { + getForeignKeyFields, + hasAttribute, + indentString, + isDiscriminatorField, + type PluginOptions, +} from '@zenstackhq/sdk'; +import { DataModel, DataModelField, Enum, isDataModel, isEnum, isTypeDef, type Model } from '@zenstackhq/sdk/ast'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; import { supportCreateMany, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import path from 'path'; @@ -59,7 +65,7 @@ export default class Transformer { for (const enumType of this.enumTypes) { const name = upperCaseFirst(enumType.name); const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); - const content = `/* eslint-disable */\n${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( + const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( `${name}`, `z.enum(${JSON.stringify(enumType.values)})` )}`; @@ -72,7 +78,7 @@ export default class Transformer { for (const enumDecl of extraEnums) { const name = upperCaseFirst(enumDecl.name); const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); - const content = `/* eslint-disable */\n${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( + const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( `${name}`, `z.enum(${JSON.stringify(enumDecl.fields.map((f) => f.name))})` )}`; @@ -107,7 +113,7 @@ export default class Transformer { const objectSchema = this.prepareObjectSchema(schemaFields, options); const filePath = path.join(Transformer.outputPath, `objects/${this.name}.schema.ts`); - const content = '/* eslint-disable */\n' + extraImports.join('\n\n') + objectSchema; + const content = extraImports.join('\n\n') + objectSchema; this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); return `${this.name}.schema`; } @@ -241,7 +247,8 @@ export default class Transformer { this.addSchemaImport(inputType.type); } - result.push(this.generatePrismaStringLine(field, inputType, lines.length)); + const contextField = contextDataModel?.fields.find((f) => f.name === field.name); + result.push(this.generatePrismaStringLine(field, inputType, lines.length, contextField)); } } @@ -315,7 +322,12 @@ export default class Transformer { this.schemaImports.add(upperCaseFirst(name)); } - generatePrismaStringLine(field: PrismaDMMF.SchemaArg, inputType: PrismaDMMF.InputTypeRef, inputsLength: number) { + generatePrismaStringLine( + field: PrismaDMMF.SchemaArg, + inputType: PrismaDMMF.InputTypeRef, + inputsLength: number, + contextField: DataModelField | undefined + ) { const isEnum = inputType.location === 'enumTypes'; const { isModelQueryType, modelName, queryName } = this.checkIsModelQueryType(inputType.type as string); @@ -330,11 +342,36 @@ export default class Transformer { const arr = inputType.isList ? '.array()' : ''; - const opt = !field.isRequired ? '.optional()' : ''; + const optional = + !field.isRequired || + // also check if the zmodel field infers the field as optional + (contextField && this.isFieldOptional(contextField)); return inputsLength === 1 - ? ` ${field.name}: z.lazy(() => ${schema})${arr}${opt}` - : `z.lazy(() => ${schema})${arr}${opt}`; + ? ` ${field.name}: z.lazy(() => ${schema})${arr}${optional ? '.optional()' : ''}` + : `z.lazy(() => ${schema})${arr}${optional ? '.optional()' : ''}`; + } + + private isFieldOptional(dmField: DataModelField) { + if (hasAttribute(dmField, '@default')) { + // it's possible that ZModel field has a default but it's transformed away + // when generating Prisma schema, e.g.: `@default(auth().id)` + return true; + } + + if (isDataModel(dmField.type.reference?.ref)) { + // if field is a relation, we need to check if the corresponding fk field has a default + // { + // authorId Int @default(auth().id) + // author User @relation(...) // <- author should be optional + // } + const fkFields = getForeignKeyFields(dmField); + if (fkFields.every((fkField) => hasAttribute(fkField, '@default'))) { + return true; + } + } + + return false; } generateFieldValidators(zodStringWithMainType: string, field: PrismaDMMF.SchemaArg) { @@ -773,7 +810,6 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; const filePath = path.join(Transformer.outputPath, `input/${modelName}Input.schema.ts`); const content = ` - /* eslint-disable */ ${imports.join(';\n')} type ${modelName}InputSchemaType = { @@ -794,7 +830,6 @@ ${operations const indexFilePath = path.join(Transformer.outputPath, 'input/index.ts'); const indexContent = ` -/* eslint-disable */ ${globalExports.join(';\n')} `; this.sourceFiles.push(this.project.createSourceFile(indexFilePath, indexContent, { overwrite: true })); diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index c130934b2..37466adbb 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -1,20 +1,7 @@ +import { getLiteral, isFromStdlib } from '@zenstackhq/sdk'; import { - ExpressionContext, - getAttributeArg, - getAttributeArgLiteral, - getLiteral, - getLiteralArray, - isDataModelFieldReference, - isFromStdlib, - PluginError, - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '@zenstackhq/sdk'; -import { - DataModel, DataModelField, DataModelFieldAttribute, - isArrayExpr, isBooleanLiteral, isDataModel, isEnum, @@ -25,7 +12,6 @@ import { TypeDefField, } from '@zenstackhq/sdk/ast'; import { upperCaseFirst } from 'upper-case-first'; -import { name } from '..'; import { isDefaultWithAuth } from '../../enhancer/enhancer-utils'; export function makeFieldSchema(field: DataModelField | TypeDefField) { @@ -222,50 +208,6 @@ function makeZodSchema(field: DataModelField | TypeDefField) { return schema; } -export function makeValidationRefinements(model: DataModel) { - const attrs = model.attributes.filter((attr) => attr.decl.ref?.name === '@@validate'); - const refinements = attrs - .map((attr) => { - const valueArg = getAttributeArg(attr, 'value'); - if (!valueArg) { - return undefined; - } - - const messageArg = getAttributeArgLiteral(attr, 'message'); - const message = messageArg ? `message: ${JSON.stringify(messageArg)},` : ''; - - const pathArg = getAttributeArg(attr, 'path'); - const path = - pathArg && isArrayExpr(pathArg) ? `path: ['${getLiteralArray(pathArg)?.join(`', '`)}'],` : ''; - - const options = `, { ${message} ${path} }`; - - try { - let expr = new TypeScriptExpressionTransformer({ - context: ExpressionContext.ValidationRule, - fieldReferenceContext: 'value', - }).transform(valueArg); - - if (isDataModelFieldReference(valueArg)) { - // if the expression is a simple field reference, treat undefined - // as true since the all fields are optional in validation context - expr = `${expr} ?? true`; - } - - return `.refine((value: any) => ${expr}${options})`; - } catch (err) { - if (err instanceof TypeScriptExpressionTransformerError) { - throw new PluginError(name, err.message); - } else { - throw err; - } - } - }) - .filter((r) => !!r); - - return refinements; -} - function getAttrLiteralArg(attr: DataModelFieldAttribute, paramName: string) { const arg = attr.args.find((arg) => arg.$resolvedParam?.name === paramName); return arg && getLiteral(arg.value); diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 62cefd36e..3316a90a9 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -209,7 +209,7 @@ attribute @@@completionHint(_ values: String[]) * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. */ -attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma +attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma @@@supportTypeDef /** * Defines a default value for a field. @@ -536,7 +536,7 @@ attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'u * Used to specify the model for resolving `auth()` function call in access policies. A Zmodel * can have at most one model with this attribute. By default, the model named "User" is used. */ -attribute @@auth() +attribute @@auth() @@@supportTypeDef /** * Indicates that the field is a password field and needs to be hashed before persistence. @@ -639,7 +639,7 @@ attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, Float /** * Validates the entity with a complex condition. */ -attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation +attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation @@@supportTypeDef /** * Validates length of a string field. diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index effd472f0..a6fab7ea5 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -2,28 +2,19 @@ import { BinaryExpr, DataModel, DataModelAttribute, - DataModelField, Expression, InheritableNode, - isArrayExpr, isBinaryExpr, isDataModel, isDataModelField, isInvocationExpr, - isMemberAccessExpr, isModel, - isReferenceExpr, + isTypeDef, Model, ModelImport, - ReferenceExpr, + TypeDef, } from '@zenstackhq/language/ast'; -import { - getInheritanceChain, - getModelFieldsWithBases, - getRecursiveBases, - isDelegateModel, - isFromStdlib, -} from '@zenstackhq/sdk'; +import { getInheritanceChain, getRecursiveBases, isDelegateModel, isFromStdlib } from '@zenstackhq/sdk'; import { AstNode, copyAstNode, @@ -149,29 +140,6 @@ function cloneAst( return clone; } -export function getIdFields(dataModel: DataModel) { - const fieldLevelId = getModelFieldsWithBases(dataModel).find((f) => - f.attributes.some((attr) => attr.decl.$refText === '@id') - ); - if (fieldLevelId) { - return [fieldLevelId]; - } else { - // get model level @@id attribute - const modelIdAttr = dataModel.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); - if (modelIdAttr) { - // get fields referenced in the attribute: @@id([field1, field2]]) - if (!isArrayExpr(modelIdAttr.args[0]?.value)) { - return []; - } - const argValue = modelIdAttr.args[0].value; - return argValue.items - .filter((expr): expr is ReferenceExpr => isReferenceExpr(expr) && !!getDataModelFieldReference(expr)) - .map((expr) => expr.target.ref as DataModelField); - } - } - return []; -} - export function isAuthInvocation(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); } @@ -184,16 +152,6 @@ export function isCheckInvocation(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'check' && isFromStdlib(node.function.ref); } -export function getDataModelFieldReference(expr: Expression): DataModelField | undefined { - if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) { - return expr.target.ref; - } else if (isMemberAccessExpr(expr) && isDataModelField(expr.member.ref)) { - return expr.member.ref; - } else { - return undefined; - } -} - export function resolveImportUri(imp: ModelImport): URI | undefined { if (!imp.path) return undefined; // This will return true if imp.path is undefined, null, or an empty string (""). @@ -302,21 +260,24 @@ export function findUpAst(node: AstNode, predicate: (node: AstNode) => boolean): } /** - * Gets all data models from all loaded documents + * Gets all data models and type defs from all loaded documents */ -export function getAllLoadedDataModels(langiumDocuments: LangiumDocuments) { +export function getAllLoadedDataModelsAndTypeDefs(langiumDocuments: LangiumDocuments) { return langiumDocuments.all .map((doc) => doc.parseResult.value as Model) - .flatMap((model) => model.declarations.filter(isDataModel)) + .flatMap((model) => model.declarations.filter((d): d is DataModel | TypeDef => isDataModel(d) || isTypeDef(d))) .toArray(); } /** - * Gets all data models from loaded and reachable documents + * Gets all data models and type defs from loaded and reachable documents */ -export function getAllLoadedAndReachableDataModels(langiumDocuments: LangiumDocuments, fromModel?: DataModel) { +export function getAllLoadedAndReachableDataModelsAndTypeDefs( + langiumDocuments: LangiumDocuments, + fromModel?: DataModel +) { // get all data models from loaded documents - const allDataModels = getAllLoadedDataModels(langiumDocuments); + const allDataModels = getAllLoadedDataModelsAndTypeDefs(langiumDocuments); if (fromModel) { // merge data models transitively reached from the current model diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 4d86837d0..0133f452c 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1151,7 +1151,7 @@ describe('Attribute tests', () => { @@allow('all', auth().email != null) } `) - ).toContain(`Could not resolve reference to DataModelField named 'email'.`); + ).toContain(`Could not resolve reference to MemberAccessTarget named 'email'.`); }); it('collection predicate expression check', async () => { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b2be27d13..426c1e79b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.8.1", + "version": "2.9.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { @@ -48,6 +48,7 @@ "./dmmf-helpers": { "types": "./dmmf-helpers/index.d.ts", "default": "./dmmf-helpers/index.js" - } + }, + "./package.json": "./package.json" } } diff --git a/packages/sdk/src/code-gen.ts b/packages/sdk/src/code-gen.ts index 3f80e7e4d..68106343d 100644 --- a/packages/sdk/src/code-gen.ts +++ b/packages/sdk/src/code-gen.ts @@ -1,4 +1,5 @@ -import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget } from 'ts-morph'; +import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget, SourceFile } from 'ts-morph'; +import pkgJson from './package.json'; import { PluginError } from './types'; /** @@ -14,16 +15,32 @@ export function createProject(options?: CompilerOptions) { strict: true, skipLibCheck: true, noEmitOnError: true, + noImplicitAny: false, ...options, }, }); } +export function saveSourceFile(sourceFile: SourceFile) { + sourceFile.replaceWithText( + `/****************************************************************************** +* This file was generated by ZenStack CLI ${pkgJson.version}. +******************************************************************************/ + +/* eslint-disable */ +// @ts-nocheck + + ${sourceFile.getText()}` + ); + sourceFile.formatText(); + sourceFile.saveSync(); +} + /** * Persists a TS project to disk. */ export async function saveProject(project: Project) { - project.getSourceFiles().forEach((sf) => sf.formatText()); + project.getSourceFiles().forEach(saveSourceFile); await project.save(); } diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 859399673..71b4246ca 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -26,7 +26,7 @@ import { getAttributeArg, getAttributeArgLiteral, getAttributeArgs, - getAuthModel, + getAuthDecl, getDataModels, getInheritedFromDelegate, getLiteral, @@ -38,6 +38,7 @@ import { isForeignKeyField, isIdField, resolved, + saveSourceFile, TypeScriptExpressionTransformer, } from '.'; @@ -66,14 +67,13 @@ export type ModelMetaGeneratorOptions = { shortNameMap?: Map; }; -export async function generate( +export function generate( project: Project, models: DataModel[], typeDefs: TypeDef[], options: ModelMetaGeneratorOptions ) { const sf = project.createSourceFile(options.output, undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); sf.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, declarations: [ @@ -83,7 +83,7 @@ export async function generate( sf.addStatements('export default metadata;'); if (options.preserveTsFiles) { - await sf.save(); + saveSourceFile(sf); } return sf; @@ -101,7 +101,7 @@ function generateModelMetadata( writeTypeDefs(sourceFile, writer, typeDefs, options); writeDeleteCascade(writer, dataModels); writeShortNameMap(options, writer); - writeAuthModel(writer, dataModels); + writeAuthModel(writer, dataModels, typeDefs); }); } @@ -162,8 +162,8 @@ function writeBaseTypes(writer: CodeBlockWriter, model: DataModel) { } } -function writeAuthModel(writer: CodeBlockWriter, dataModels: DataModel[]) { - const authModel = getAuthModel(dataModels); +function writeAuthModel(writer: CodeBlockWriter, dataModels: DataModel[], typeDefs: TypeDef[]) { + const authModel = getAuthDecl([...dataModels, ...typeDefs]); if (authModel) { writer.writeLine(`authModel: '${authModel.name}'`); } diff --git a/packages/sdk/src/package.json b/packages/sdk/src/package.json new file mode 120000 index 000000000..4e26811d4 --- /dev/null +++ b/packages/sdk/src/package.json @@ -0,0 +1 @@ +../package.json \ No newline at end of file diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 92c099717..29160c283 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -31,6 +31,12 @@ export type PluginOptions = { */ prismaClientPath?: string; + /** + * PrismaClient's TypeScript declaration file's path + * @private + */ + prismaClientDtsPath?: string; + /** * An optional map of full names to shortened names * @private @@ -74,6 +80,12 @@ export type PluginResult = { */ prismaClientPath?: string; + /** + * PrismaClient's TypeScript declaration file's path + * @private + */ + prismaClientDtsPath?: string; + /** * An optional Prisma DMMF document that a plugin can generate * @private diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 6b2bfe868..46c2a82c1 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -27,9 +27,12 @@ import { isModel, isObjectExpr, isReferenceExpr, + isTypeDef, + isTypeDefField, Model, Reference, ReferenceExpr, + TypeDef, TypeDefField, } from '@zenstackhq/language/ast'; import fs from 'node:fs'; @@ -49,6 +52,18 @@ export function getDataModels(model: Model, includeIgnored = false) { } } +/** + * Gets data models and type defs in the ZModel schema. + */ +export function getDataModelAndTypeDefs(model: Model, includeIgnored = false) { + const r = model.declarations.filter((d): d is DataModel | TypeDef => isDataModel(d) || isTypeDef(d)); + if (includeIgnored) { + return r; + } else { + return r.filter((model) => !hasAttribute(model, '@@ignore')); + } +} + export function resolved(ref: Reference): T { if (!ref.ref) { throw new Error(`Reference not resolved: ${ref.$refText}`); @@ -117,14 +132,23 @@ export function indentString(string: string, count = 4): string { } export function hasAttribute( - decl: DataModel | DataModelField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam, + decl: DataModel | TypeDef | DataModelField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam, name: string ) { return !!getAttribute(decl, name); } export function getAttribute( - decl: DataModel | DataModelField | TypeDefField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam, + decl: + | DataModel + | TypeDef + | DataModelField + | TypeDefField + | Enum + | EnumField + | FunctionDecl + | Attribute + | AttributeParam, name: string ) { return (decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[]).find( @@ -358,6 +382,13 @@ export function getRelationField(fkField: DataModelField) { }); } +/** + * Gets the foreign key fields of the given relation field. + */ +export function getForeignKeyFields(relationField: DataModelField) { + return getRelationKeyPairs(relationField).map((pair) => pair.foreignKey); +} + export function resolvePath(_path: string, options: Pick) { if (path.isAbsolute(_path)) { return _path; @@ -448,10 +479,10 @@ export function getPreviewFeatures(model: Model) { return [] as string[]; } -export function getAuthModel(dataModels: DataModel[]) { - let authModel = dataModels.find((m) => hasAttribute(m, '@@auth')); +export function getAuthDecl(decls: (DataModel | TypeDef)[]) { + let authModel = decls.find((m) => hasAttribute(m, '@@auth')); if (!authModel) { - authModel = dataModels.find((m) => m.name === 'User'); + authModel = decls.find((m) => m.name === 'User'); } return authModel; } @@ -473,15 +504,14 @@ export function isDiscriminatorField(field: DataModelField) { return isDataModelFieldReference(arg) && arg.target.$refText === field.name; } -export function getIdFields(dataModel: DataModel) { - const fieldLevelId = getModelFieldsWithBases(dataModel).find((f) => - f.attributes.some((attr) => attr.decl.$refText === '@id') - ); +export function getIdFields(decl: DataModel | TypeDef) { + const fields = isDataModel(decl) ? getModelFieldsWithBases(decl) : decl.fields; + const fieldLevelId = fields.find((f) => f.attributes.some((attr) => attr.decl.$refText === '@id')); if (fieldLevelId) { return [fieldLevelId]; } else { // get model level @@id attribute - const modelIdAttr = dataModel.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); + const modelIdAttr = decl.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); if (modelIdAttr) { // get fields referenced in the attribute: @@id([field1, field2]]) if (!isArrayExpr(modelIdAttr.args[0].value)) { @@ -489,17 +519,17 @@ export function getIdFields(dataModel: DataModel) { } const argValue = modelIdAttr.args[0].value; return argValue.items - .filter((expr): expr is ReferenceExpr => isReferenceExpr(expr) && !!getDataModelFieldReference(expr)) + .filter((expr): expr is ReferenceExpr => isReferenceExpr(expr) && !!getFieldReference(expr)) .map((expr) => expr.target.ref as DataModelField); } } return []; } -export function getDataModelFieldReference(expr: Expression): DataModelField | undefined { - if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) { +export function getFieldReference(expr: Expression): DataModelField | TypeDefField | undefined { + if (isReferenceExpr(expr) && (isDataModelField(expr.target.ref) || isTypeDefField(expr.target.ref))) { return expr.target.ref; - } else if (isMemberAccessExpr(expr) && isDataModelField(expr.member.ref)) { + } else if (isMemberAccessExpr(expr) && (isDataModelField(expr.member.ref) || isTypeDefField(expr.member.ref))) { return expr.member.ref; } else { return undefined; diff --git a/packages/server/package.json b/packages/server/package.json index d21932ca4..8aed97c10 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.8.1", + "version": "2.9.0", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index c03a28af8..627bcfe9c 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.8.1", + "version": "2.9.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, diff --git a/tests/integration/tests/enhancements/json/crud.test.ts b/tests/integration/tests/enhancements/json/crud.test.ts index 9cd7ff8a4..12c35ed09 100644 --- a/tests/integration/tests/enhancements/json/crud.test.ts +++ b/tests/integration/tests/enhancements/json/crud.test.ts @@ -191,6 +191,46 @@ describe('Json field CRUD', () => { ).toResolveTruthy(); }); + it('respects refine validation rules', async () => { + const params = await loadSchema( + ` + type Address { + city String @length(2, 10) + } + + type Profile { + age Int @gte(18) + address Address? + @@validate(age > 18 && length(address.city, 2, 2)) + } + + model User { + id Int @id @default(autoincrement()) + profile Profile @json + @@allow('all', true) + } + `, + { + provider: 'postgresql', + dbUrl, + } + ); + + prisma = params.prisma; + const schema = params.zodSchemas.models.ProfileSchema; + + expect(schema.safeParse({ age: 10, address: { city: 'NY' } })).toMatchObject({ success: false }); + expect(schema.safeParse({ age: 20, address: { city: 'NYC' } })).toMatchObject({ success: false }); + expect(schema.safeParse({ age: 20, address: { city: 'NY' } })).toMatchObject({ success: true }); + + const db = params.enhance(); + await expect(db.user.create({ data: { profile: { age: 10 } } })).toBeRejectedByPolicy(); + await expect( + db.user.create({ data: { profile: { age: 20, address: { city: 'NYC' } } } }) + ).toBeRejectedByPolicy(); + await expect(db.user.create({ data: { profile: { age: 20, address: { city: 'NY' } } } })).toResolveTruthy(); + }); + it('respects enums used by data models', async () => { const params = await loadSchema( ` @@ -344,4 +384,49 @@ describe('Json field CRUD', () => { expect(u2.profile.ownerId).toBe(2); expect(u2.profile.nested.userId).toBe(3); }); + + it('works with recursive types', async () => { + const params = await loadSchema( + ` + type Content { + type String + content Content[]? + text String? + } + + model Post { + id Int @id @default(autoincrement()) + content Content @json + @@allow('all', true) + } + `, + { + provider: 'postgresql', + dbUrl, + } + ); + + prisma = params.prisma; + const db = params.enhance(); + const post = await db.post.create({ + data: { + content: { + type: 'text', + content: [ + { + type: 'text', + content: [ + { + type: 'text', + text: 'hello', + }, + ], + }, + ], + }, + }, + }); + + await expect(post.content.content[0].content[0].text).toBe('hello'); + }); }); diff --git a/tests/integration/tests/enhancements/json/typing.test.ts b/tests/integration/tests/enhancements/json/typing.test.ts index a73e04f03..a2053770c 100644 --- a/tests/integration/tests/enhancements/json/typing.test.ts +++ b/tests/integration/tests/enhancements/json/typing.test.ts @@ -325,6 +325,59 @@ async function main() { dateTime: new Date(), json: { a: 1 }, } +} + `, + }, + ], + } + ); + }); + + it('supports recursive definition', async () => { + await loadSchema( + ` + type Content { + type String + content Content[]? + text String? + } + + model Post { + id Int @id @default(autoincrement()) + content Content @json + } + `, + { + provider: 'postgresql', + pushDb: false, + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import type { Content } from '.zenstack/models'; +import { enhance } from '.zenstack/enhance'; +import { PrismaClient } from '@prisma/client'; + +async function main() { + const content: Content = { + type: 'text', + content: [ + { + type: 'text', + content: [ + { + type: 'text', + text: 'hello', + }, + ], + }, + ], + } + + const db = enhance(new PrismaClient()); + const post = await db.post.create({ data: { content } }); + console.log(post.content.content?.[0].content?.[0].text); } `, }, diff --git a/tests/integration/tests/enhancements/json/validation.test.ts b/tests/integration/tests/enhancements/json/validation.test.ts index 27f5e5067..df5dfc281 100644 --- a/tests/integration/tests/enhancements/json/validation.test.ts +++ b/tests/integration/tests/enhancements/json/validation.test.ts @@ -1,4 +1,4 @@ -import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; +import { loadModel, loadModelWithError, loadSchema } from '@zenstackhq/testtools'; describe('JSON field typing', () => { it('is only supported by postgres', async () => { @@ -36,4 +36,124 @@ describe('JSON field typing', () => { ) ).resolves.toContain('Custom-typed field must have @json attribute'); }); + + it('disallows normal member accesses in policy rules', async () => { + await expect( + loadModelWithError( + ` + type Profile { + age Int @gt(0) + } + + model User { + id Int @id @default(autoincrement()) + profile Profile @json + @@allow('all', profile.age > 18) + } + ` + ) + ).resolves.toContain(`Could not resolve reference to MemberAccessTarget named 'age'.`); + }); + + it('allows auth member accesses in policy rules', async () => { + await expect( + loadModel( + ` + type Profile { + age Int @gt(0) + } + + model User { + id Int @id @default(autoincrement()) + profile Profile @json + @@allow('all', auth().profile.age > 18) + } + ` + ) + ).toResolveTruthy(); + }); + + it('disallows normal collection accesses in policy rules', async () => { + await expect( + loadModelWithError( + ` + type Profile { + roles Role[] + } + + type Role { + name String + } + + model User { + id Int @id @default(autoincrement()) + profile Profile @json + @@allow('all', profile.roles?[name == 'ADMIN']) + } + ` + ) + ).resolves.toContain(`Could not resolve reference to MemberAccessTarget named 'roles'.`); + + await expect( + loadModelWithError( + ` + type Profile { + role String + } + + model User { + id Int @id @default(autoincrement()) + profiles Profile[] @json + @@allow('all', profiles?[role == 'ADMIN']) + } + ` + ) + ).resolves.toContain(`Could not resolve reference to ReferenceTarget named 'role'.`); + }); + + it('disallows auth collection accesses in policy rules', async () => { + await expect( + loadModel( + ` + type Profile { + roles Role[] + } + + type Role { + name String + } + + model User { + id Int @id @default(autoincrement()) + profile Profile @json + @@allow('all', auth().profile.roles?[name == 'ADMIN']) + } + ` + ) + ).toResolveTruthy(); + }); + + it('only allows whitelisted type-level attributes', async () => { + await expect( + loadModel( + ` + type User { + id Int @id + @@auth + } + ` + ) + ).toResolveTruthy(); + + await expect( + loadModelWithError( + ` + type User { + id Int @id + @@allow('all', true) + } + ` + ) + ).resolves.toContain('attribute "@@allow" cannot be used on type declarations'); + }); }); diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index 02c3959d0..296eefee7 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -864,4 +864,60 @@ describe('auth() compile-time test', () => { } ); }); + + it('"User" type as auth', async () => { + const { enhance } = await loadSchema( + ` + type Profile { + age Int + } + + type Role { + name String + permissions String[] + } + + type User { + myId Int @id + banned Boolean + profile Profile + roles Role[] + } + + model Foo { + id Int @id @default(autoincrement()) + @@allow('read', true) + @@allow('create', auth().myId == 1 && !auth().banned) + @@allow('delete', auth().roles?['DELETE' in permissions]) + @@deny('all', auth().profile.age < 18) + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { enhance } from ".zenstack/enhance"; + import { PrismaClient } from '@prisma/client'; + enhance(new PrismaClient(), { user: { myId: 1, profile: { age: 20 } } }); + `, + }, + ], + } + ); + + await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); + await expect(enhance({ myId: 1, banned: true }).foo.create({ data: {} })).toBeRejectedByPolicy(); + await expect(enhance({ myId: 1, profile: { age: 16 } }).foo.create({ data: {} })).toBeRejectedByPolicy(); + const r = await enhance({ myId: 1, profile: { age: 20 } }).foo.create({ data: {} }); + await expect( + enhance({ myId: 1, profile: { age: 20 } }).foo.delete({ where: { id: r.id } }) + ).toBeRejectedByPolicy(); + await expect( + enhance({ myId: 1, profile: { age: 20 }, roles: [{ name: 'ADMIN', permissions: ['DELETE'] }] }).foo.delete({ + where: { id: r.id }, + }) + ).toResolveTruthy(); + }); }); diff --git a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts index 13f05aa51..1d907a4f2 100644 --- a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts +++ b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts @@ -44,13 +44,9 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - await expect(db.model.getAll()).resolves.toHaveLength(2); - - // FIXME: extending an enhanced client doesn't work for this case - // const db1 = enhance(prisma).$extends(ext); - // await expect(db1.model.getAll()).resolves.toHaveLength(2); + await expect(prisma.$extends(ext).model.getAll()).resolves.toHaveLength(3); + await expect(enhanceRaw(prisma.$extends(ext)).model.getAll()).resolves.toHaveLength(2); + await expect(enhanceRaw(prisma).$extends(ext).model.getAll()).resolves.toHaveLength(2); }); it('one model new method', async () => { @@ -84,9 +80,9 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - await expect(db.model.getAll()).resolves.toHaveLength(2); + await expect(prisma.$extends(ext).model.getAll()).resolves.toHaveLength(3); + await expect(enhanceRaw(prisma.$extends(ext)).model.getAll()).resolves.toHaveLength(2); + await expect(enhanceRaw(prisma).$extends(ext).model.getAll()).resolves.toHaveLength(2); }); it('add client method', async () => { @@ -115,8 +111,11 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - xprisma.$log('abc'); + enhanceRaw(prisma).$extends(ext).$log('abc'); + expect(logged).toBeTruthy(); + + logged = false; + enhanceRaw(prisma.$extends(ext)).$log('abc'); expect(logged).toBeTruthy(); }); @@ -143,7 +142,6 @@ describe('With Policy: client extensions', () => { query: { model: { async findMany({ args, query }: any) { - // take incoming `where` and set `age` args.where = { ...args.where, y: { lt: 300 } }; return query(args); }, @@ -152,9 +150,8 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - await expect(db.model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); }); it('query override all models', async () => { @@ -180,7 +177,6 @@ describe('With Policy: client extensions', () => { query: { $allModels: { async findMany({ args, query }: any) { - // take incoming `where` and set `age` args.where = { ...args.where, y: { lt: 300 } }; console.log('findMany args:', args); return query(args); @@ -190,9 +186,8 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - await expect(db.model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); }); it('query override all operations', async () => { @@ -218,7 +213,6 @@ describe('With Policy: client extensions', () => { query: { model: { async $allOperations({ operation, args, query }: any) { - // take incoming `where` and set `age` args.where = { ...args.where, y: { lt: 300 } }; console.log(`${operation} args:`, args); return query(args); @@ -228,9 +222,8 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - await expect(db.model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); }); it('query override everything', async () => { @@ -255,7 +248,6 @@ describe('With Policy: client extensions', () => { name: 'prisma-extension-queryOverride', query: { async $allOperations({ operation, args, query }: any) { - // take incoming `where` and set `age` args.where = { ...args.where, y: { lt: 300 } }; console.log(`${operation} args:`, args); return query(args); @@ -264,9 +256,8 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - await expect(db.model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); + await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); }); it('result mutation', async () => { @@ -301,11 +292,9 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - const r = await db.model.findMany(); - expect(r).toHaveLength(1); - expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ value: 2 })])); + const expected = [expect.objectContaining({ value: 2 })]; + await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toEqual(expected); + await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toEqual(expected); }); it('result custom fields', async () => { @@ -339,10 +328,8 @@ describe('With Policy: client extensions', () => { }); }); - const xprisma = prisma.$extends(ext); - const db = enhanceRaw(xprisma); - const r = await db.model.findMany(); - expect(r).toHaveLength(1); - expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ doubleValue: 2 })])); + const expected = [expect.objectContaining({ doubleValue: 2 })]; + await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toEqual(expected); + await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toEqual(expected); }); }); diff --git a/tests/regression/tests/issue-1835.test.ts b/tests/regression/tests/issue-1835.test.ts new file mode 100644 index 000000000..f29efcbbb --- /dev/null +++ b/tests/regression/tests/issue-1835.test.ts @@ -0,0 +1,28 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1835', () => { + it('regression', async () => { + await loadSchema( + ` + enum Enum { + SOME_VALUE + ANOTHER_VALUE + } + + model Model { + id String @id @default(cuid()) + value Enum + @@ignore + } + + model AnotherModel { + id String @id @default(cuid()) + } + `, + { + provider: 'postgresql', + pushDb: false, + } + ); + }); +}); diff --git a/tests/regression/tests/issue-1843.test.ts b/tests/regression/tests/issue-1843.test.ts new file mode 100644 index 000000000..518262857 --- /dev/null +++ b/tests/regression/tests/issue-1843.test.ts @@ -0,0 +1,108 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1843', () => { + it('regression', async () => { + const { zodSchemas, enhance, prisma } = await loadSchema( + ` + model User { + id String @id @default(cuid()) + email String @unique @email @length(6, 32) + password String @password @omit + contents Content[] + postsCoauthored PostWithCoauthor[] + + @@allow('all', true) + } + + abstract model Owner { + owner User @relation(fields: [ownerId], references: [id]) + ownerId String @default(auth().id) + } + + abstract model BaseContent extends Owner { + published Boolean @default(false) + + @@index([published]) + } + + model Content extends BaseContent { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + contentType String + @@allow('all', true) + + @@delegate(contentType) + } + + model PostWithCoauthor extends Content { + title String + + coauthor User @relation(fields: [coauthorId], references: [id]) + coauthorId String + + @@allow('all', true) + } + + model Post extends Content { + title String + + @@allow('all', true) + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + async function main() { + const enhanced = enhance(new PrismaClient()); + await enhanced.postWithCoauthor.create({ + data: { + title: "new post", + coauthor: { + connect: { + id: "1" + } + }, + } + }); + + await enhanced.postWithCoauthor.create({ + data: { + title: "new post", + coauthorId: "1" + } + }); + } + `, + }, + ], + } + ); + + const user = await prisma.user.create({ data: { email: 'abc', password: '123' } }); + const db = enhance({ id: user.id }); + + // connect + await expect( + db.postWithCoauthor.create({ data: { title: 'new post', coauthor: { connect: { id: user.id } } } }) + ).toResolveTruthy(); + + // fk setting + await expect( + db.postWithCoauthor.create({ data: { title: 'new post', coauthorId: user.id } }) + ).toResolveTruthy(); + + // zod validation + zodSchemas.models.PostWithCoauthorCreateSchema.parse({ + title: 'new post', + coauthorId: '1', + }); + }); +}); diff --git a/tests/regression/tests/issue-1849.test.ts b/tests/regression/tests/issue-1849.test.ts new file mode 100644 index 000000000..2362ac668 --- /dev/null +++ b/tests/regression/tests/issue-1849.test.ts @@ -0,0 +1,24 @@ +import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1849', () => { + it('regression', async () => { + await loadSchema( + `schema.zmodel + import './enum' + + model Post { + id Int @id + status Status @default(PUBLISHED) + } + + ${FILE_SPLITTER}enum.zmodel + + enum Status { + PENDING + PUBLISHED + } + `, + { provider: 'postgresql', pushDb: false } + ); + }); +}); diff --git a/tests/regression/tests/issue-1857.test.ts b/tests/regression/tests/issue-1857.test.ts new file mode 100644 index 000000000..fcd730017 --- /dev/null +++ b/tests/regression/tests/issue-1857.test.ts @@ -0,0 +1,45 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1857', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` + type JSONContent { + type String + text String? + } + + model Post { + id String @id @default(uuid()) + content JSONContent @json + @@allow('all', true) + } + `, + { + provider: 'postgresql', + pushDb: false, + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + async function main() { + const prisma = new PrismaClient(); + await prisma.post.create({ + data: { + content: { type: 'foo', text: null } + } + }); + } + `, + }, + ], + } + ); + + zodSchemas.models.JSONContentSchema.parse({ type: 'foo', text: null }); + }); +}); diff --git a/tests/regression/tests/issue-1859.test.ts b/tests/regression/tests/issue-1859.test.ts new file mode 100644 index 000000000..2b9d4538b --- /dev/null +++ b/tests/regression/tests/issue-1859.test.ts @@ -0,0 +1,90 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1859', () => { + it('extend enhanced client', async () => { + const { enhance, prisma } = await loadSchema( + ` + model Post { + id Int @id + title String + published Boolean + + @@allow('create', true) + @@allow('read', published) + } + ` + ); + + await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); + await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); + + const db = enhance(); + await expect(db.post.findMany()).resolves.toHaveLength(1); + + const extended = db.$extends({ + model: { + post: { + findManyListView: async (args: any) => { + return { view: true, data: await db.post.findMany(args) }; + }, + }, + }, + }); + + await expect(extended.post.findManyListView()).resolves.toMatchObject({ + view: true, + data: [{ id: 1, title: 'post1', published: true }], + }); + await expect(extended.post.findMany()).resolves.toHaveLength(1); + }); + + it('enhance extended client', async () => { + const { enhanceRaw, prisma, prismaModule } = await loadSchema( + ` + model Post { + id Int @id + title String + published Boolean + + @@allow('create', true) + @@allow('read', published) + } + ` + ); + + await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); + await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); + + const ext = prismaModule.defineExtension((_prisma: any) => { + return _prisma.$extends({ + model: { + post: { + findManyListView: async (args: any) => { + return { view: true, data: await prisma.post.findMany(args) }; + }, + }, + }, + }); + }); + + await expect(prisma.$extends(ext).post.findMany()).resolves.toHaveLength(2); + await expect(prisma.$extends(ext).post.findManyListView()).resolves.toMatchObject({ + view: true, + data: [ + { id: 1, title: 'post1', published: true }, + { id: 2, title: 'post2', published: false }, + ], + }); + + const enhanced = enhanceRaw(prisma.$extends(ext)); + await expect(enhanced.post.findMany()).resolves.toHaveLength(1); + // findManyListView internally uses the un-enhanced client + await expect(enhanced.post.findManyListView()).resolves.toMatchObject({ + view: true, + data: [ + { id: 1, title: 'post1', published: true }, + { id: 2, title: 'post2', published: false }, + ], + }); + }); +}); diff --git a/tests/regression/tests/issue-756.test.ts b/tests/regression/tests/issue-756.test.ts index 9f6750ea9..dd1a10ccf 100644 --- a/tests/regression/tests/issue-756.test.ts +++ b/tests/regression/tests/issue-756.test.ts @@ -28,6 +28,6 @@ describe('Regression: issue 756', () => { } ` ) - ).toContain(`Could not resolve reference to DataModelField named 'authorId'.`); + ).toContain(`Could not resolve reference to MemberAccessTarget named 'authorId'.`); }); }); diff --git a/tests/regression/tests/issue-prisma-extension.test.ts b/tests/regression/tests/issue-prisma-extension.test.ts new file mode 100644 index 000000000..fa041a18a --- /dev/null +++ b/tests/regression/tests/issue-prisma-extension.test.ts @@ -0,0 +1,90 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue prisma extension', () => { + it('extend enhanced client', async () => { + const { enhance, prisma } = await loadSchema( + ` + model Post { + id Int @id + title String + published Boolean + + @@allow('create', true) + @@allow('read', published) + } + ` + ); + + await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); + await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); + + const db = enhance(); + await expect(db.post.findMany()).resolves.toHaveLength(1); + + const extended = db.$extends({ + model: { + post: { + findManyListView: async (args: any) => { + return { view: true, data: await db.post.findMany(args) }; + }, + }, + }, + }); + + await expect(extended.post.findManyListView()).resolves.toMatchObject({ + view: true, + data: [{ id: 1, title: 'post1', published: true }], + }); + await expect(extended.post.findMany()).resolves.toHaveLength(1); + }); + + it('enhance extended client', async () => { + const { enhanceRaw, prisma, prismaModule } = await loadSchema( + ` + model Post { + id Int @id + title String + published Boolean + + @@allow('create', true) + @@allow('read', published) + } + ` + ); + + await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); + await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); + + const ext = prismaModule.defineExtension((_prisma: any) => { + return _prisma.$extends({ + model: { + post: { + findManyListView: async (args: any) => { + return { view: true, data: await prisma.post.findMany(args) }; + }, + }, + }, + }); + }); + + await expect(prisma.$extends(ext).post.findMany()).resolves.toHaveLength(2); + await expect(prisma.$extends(ext).post.findManyListView()).resolves.toMatchObject({ + view: true, + data: [ + { id: 1, title: 'post1', published: true }, + { id: 2, title: 'post2', published: false }, + ], + }); + + const enhanced = enhanceRaw(prisma.$extends(ext)); + await expect(enhanced.post.findMany()).resolves.toHaveLength(1); + // findManyListView internally uses the un-enhanced client + await expect(enhanced.post.findManyListView()).resolves.toMatchObject({ + view: true, + data: [ + { id: 1, title: 'post1', published: true }, + { id: 2, title: 'post2', published: false }, + ], + }); + }); +});