From e3fb73ab5a54a51170e9f9c7f13e9d6384dacce1 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 7 Apr 2024 09:24:21 +0800 Subject: [PATCH 1/8] chore: bump version (#1219) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 686c94f72..210b3bc26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.12.0", + "version": "1.12.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index a24b3cb4a..657247bbb 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.12.0" +version = "1.12.1" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index bca28240f..6b0f54ce6 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.12.0", + "version": "1.12.1", "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 b26a5badd..a67823340 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.12.0", + "version": "1.12.1", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 64126d931..dc1ae4fa4 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": "1.12.0", + "version": "1.12.1", "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 67717aeca..1e9601db0 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": "1.12.0", + "version": "1.12.1", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 4612e4040..1f2117dfa 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": "1.12.0", + "version": "1.12.1", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index de3a02ce7..b7ba53b4b 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": "1.12.0", + "version": "1.12.1", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 51c93d85b..a1fd04eb6 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": "1.12.0", + "version": "1.12.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 24d24178c..6914796a1 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.12.0", + "version": "1.12.1", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index b7d284889..78f016aec 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.12.0", + "version": "1.12.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 992604f8a..f6608efb3 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.12.0", + "version": "1.12.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 13891e920..24d3c0b47 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.12.0", + "version": "1.12.1", "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 1acb82f59..5ddfc0fe7 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.12.0", + "version": "1.12.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 5fe85ffa50d012c65db542602448d5522b71ef9b Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 11 Apr 2024 11:55:36 +0800 Subject: [PATCH 2/8] fix: post-update rule for id field is not effective if id is updated (#1237) --- .../routers/generated/routers/Post.router.ts | 27 ++++++++++++++ .../routers/generated/routers/User.router.ts | 27 ++++++++++++++ .../src/enhancements/policy/handler.ts | 25 ++++++++----- .../access-policy/expression-writer.ts | 2 ++ .../access-policy/policy-guard-generator.ts | 35 +++++-------------- .../typescript-expression-transformer.ts | 4 +-- .../tests/regression/issue-1235.test.ts | 35 +++++++++++++++++++ 7 files changed, 117 insertions(+), 38 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1235.test.ts 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 6827584d1..62e570e6d 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 @@ -23,6 +23,10 @@ export default function createRouter( .input($Schema.PostInputSchema.aggregate) .query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), + createMany: procedure + .input($Schema.PostInputSchema.createMany) + .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.createMany(input as any))), + create: procedure .input($Schema.PostInputSchema.create) .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), @@ -88,6 +92,29 @@ export interface ClientType, Error>, ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; }; + createMany: { + useMutation: ( + opts?: UseTRPCMutationOptions< + Prisma.PostCreateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >, + ) => Omit< + UseTRPCMutationResult< + Prisma.BatchPayload, + TRPCClientErrorLike, + Prisma.SelectSubset, + Context + >, + 'mutateAsync' + > & { + mutateAsync: ( + variables: T, + opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, + ) => Promise; + }; + }; create: { useMutation: ( opts?: UseTRPCMutationOptions< 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 06ce01f31..4c686b057 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 @@ -23,6 +23,10 @@ export default function createRouter( .input($Schema.UserInputSchema.aggregate) .query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), + createMany: procedure + .input($Schema.UserInputSchema.createMany) + .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.createMany(input as any))), + create: procedure .input($Schema.UserInputSchema.create) .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), @@ -88,6 +92,29 @@ export interface ClientType, Error>, ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; }; + createMany: { + useMutation: ( + opts?: UseTRPCMutationOptions< + Prisma.UserCreateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >, + ) => Omit< + UseTRPCMutationResult< + Prisma.BatchPayload, + TRPCClientErrorLike, + Prisma.SelectSubset, + Context + >, + 'mutateAsync' + > & { + mutateAsync: ( + variables: T, + opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, + ) => Promise; + }; + }; create: { useMutation: ( opts?: UseTRPCMutationOptions< diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index ef48f7f38..6b7e67bea 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -690,16 +690,25 @@ export class PolicyProxyHandler implements Pr const postWriteChecks: PostWriteCheckRecord[] = []; // registers a post-update check task - const _registerPostUpdateCheck = async (model: string, uniqueFilter: any) => { + const _registerPostUpdateCheck = async ( + model: string, + preUpdateLookupFilter: any, + postUpdateLookupFilter: any + ) => { // both "post-update" rules and Zod schemas require a post-update check if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) { // select pre-update field values let preValue: any; const preValueSelect = this.utils.getPreValueSelect(model); if (preValueSelect && Object.keys(preValueSelect).length > 0) { - preValue = await db[model].findFirst({ where: uniqueFilter, select: preValueSelect }); + preValue = await db[model].findFirst({ where: preUpdateLookupFilter, select: preValueSelect }); } - postWriteChecks.push({ model, operation: 'postUpdate', uniqueFilter, preValue }); + postWriteChecks.push({ + model, + operation: 'postUpdate', + uniqueFilter: postUpdateLookupFilter, + preValue, + }); } }; @@ -826,7 +835,7 @@ export class PolicyProxyHandler implements Pr await this.utils.checkPolicyForUnique(model, args, 'update', db, checkArgs); // register post-update check - await _registerPostUpdateCheck(model, args); + await _registerPostUpdateCheck(model, args, args); } } }; @@ -873,7 +882,7 @@ export class PolicyProxyHandler implements Pr await this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); // handles the case where id fields are updated - const ids = this.utils.clone(existing); + const postUpdateIds = this.utils.clone(existing); for (const key of Object.keys(existing)) { const updateValue = (args as any).data ? (args as any).data[key] : (args as any)[key]; if ( @@ -881,12 +890,12 @@ export class PolicyProxyHandler implements Pr typeof updateValue === 'number' || typeof updateValue === 'bigint' ) { - ids[key] = updateValue; + postUpdateIds[key] = updateValue; } } // register post-update check - await _registerPostUpdateCheck(model, ids); + await _registerPostUpdateCheck(model, existing, postUpdateIds); } }, @@ -978,7 +987,7 @@ export class PolicyProxyHandler implements Pr await this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); // register post-update check - await _registerPostUpdateCheck(model, uniqueFilter); + await _registerPostUpdateCheck(model, uniqueFilter, uniqueFilter); // convert upsert to update const convertedUpdate = { diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index 2ab3e2bdd..a5de026f0 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -70,6 +70,8 @@ export class ExpressionWriter { this.plainExprBuilder = new TypeScriptExpressionTransformer({ context: ExpressionContext.AccessPolicy, isPostGuard: this.isPostGuard, + // in post-guard context, `this` references pre-update value + thisExprContext: this.isPostGuard ? 'context.preValue' : undefined, }); } diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts index 2025c3d5c..20893da10 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -6,7 +6,6 @@ import { Enum, Expression, Model, - isBinaryExpr, isDataModel, isDataModelField, isEnum, @@ -15,7 +14,6 @@ import { isMemberAccessExpr, isReferenceExpr, isThisExpr, - isUnaryExpr, } from '@zenstackhq/language/ast'; import { FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX, @@ -281,30 +279,6 @@ export default class PolicyGenerator { } } - private visitPolicyExpression(expr: Expression, postUpdate: boolean): Expression | undefined { - if (isBinaryExpr(expr) && (expr.operator === '&&' || expr.operator === '||')) { - const left = this.visitPolicyExpression(expr.left, postUpdate); - const right = this.visitPolicyExpression(expr.right, postUpdate); - if (!left) return right; - if (!right) return left; - return { ...expr, left, right }; - } - - if (isUnaryExpr(expr) && expr.operator === '!') { - const operand = this.visitPolicyExpression(expr.operand, postUpdate); - if (!operand) return undefined; - return { ...expr, operand }; - } - - if (postUpdate && !this.hasFutureReference(expr)) { - return undefined; - } else if (!postUpdate && this.hasFutureReference(expr)) { - return undefined; - } - - return expr; - } - private hasFutureReference(expr: Expression) { for (const node of streamAst(expr)) { if (isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref)) { @@ -599,13 +573,19 @@ export default class PolicyGenerator { // visit a reference or member access expression to build a // selection path const visit = (node: Expression): string[] | undefined => { + if (isThisExpr(node)) { + return []; + } + if (isReferenceExpr(node)) { const target = resolved(node.target); if (isDataModelField(target)) { // a field selection, it's a terminal return [target.name]; } - } else if (isMemberAccessExpr(node)) { + } + + if (isMemberAccessExpr(node)) { if (forAuthContext && isAuthInvocation(node.operand)) { return [node.member.$refText]; } @@ -621,6 +601,7 @@ export default class PolicyGenerator { return [...inner, node.member.$refText]; } } + return undefined; }; diff --git a/packages/schema/src/utils/typescript-expression-transformer.ts b/packages/schema/src/utils/typescript-expression-transformer.ts index ec4f89fcb..27e018aa1 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/schema/src/utils/typescript-expression-transformer.ts @@ -112,9 +112,7 @@ export class TypeScriptExpressionTransformer { throw new TypeScriptExpressionTransformerError(`Unresolved MemberAccessExpr`); } - if (isThisExpr(expr.operand)) { - return expr.member.ref.name; - } else if (isFutureExpr(expr.operand)) { + if (isFutureExpr(expr.operand)) { if (this.options?.isPostGuard !== true) { throw new TypeScriptExpressionTransformerError(`future() is only supported in postUpdate rules`); } diff --git a/tests/integration/tests/regression/issue-1235.test.ts b/tests/integration/tests/regression/issue-1235.test.ts new file mode 100644 index 000000000..1e9f80f86 --- /dev/null +++ b/tests/integration/tests/regression/issue-1235.test.ts @@ -0,0 +1,35 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1235', () => { + it('regression1', async () => { + const { enhance } = await loadSchema( + ` + model Post { + id Int @id @default(autoincrement()) + @@deny("update", future().id != id) + @@allow("all", true) + } + ` + ); + + const db = enhance(); + const post = await db.post.create({ data: {} }); + await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).toBeRejectedByPolicy(); + }); + + it('regression2', async () => { + const { enhance } = await loadSchema( + ` + model Post { + id Int @id @default(autoincrement()) + @@deny("update", future().id != this.id) + @@allow("all", true) + } + ` + ); + + const db = enhance(); + const post = await db.post.create({ data: {} }); + await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).toBeRejectedByPolicy(); + }); +}); From 1ab9d6eb63a9293e6e0cd133c42a2ab8569f03e7 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 11 Apr 2024 11:57:00 +0800 Subject: [PATCH 3/8] chore: bump version (#1238) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 210b3bc26..627428d5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.12.1", + "version": "1.12.2", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 657247bbb..f62ecdc48 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.12.1" +version = "1.12.2" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 6b0f54ce6..36c8a9dba 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.12.1", + "version": "1.12.2", "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 a67823340..e5d8f2c0a 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.12.1", + "version": "1.12.2", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index dc1ae4fa4..e4ccd33b2 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": "1.12.1", + "version": "1.12.2", "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 1e9601db0..2dbe524db 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": "1.12.1", + "version": "1.12.2", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 1f2117dfa..4eada7fc4 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": "1.12.1", + "version": "1.12.2", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index b7ba53b4b..2a7bb7bcb 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": "1.12.1", + "version": "1.12.2", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index a1fd04eb6..e32edfdff 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": "1.12.1", + "version": "1.12.2", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 6914796a1..3aa0543c0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.12.1", + "version": "1.12.2", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 78f016aec..ea559e4ff 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.12.1", + "version": "1.12.2", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f6608efb3..16ffeac15 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.12.1", + "version": "1.12.2", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 24d3c0b47..862ea6f20 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.12.1", + "version": "1.12.2", "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 5ddfc0fe7..d3ec5018b 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.12.1", + "version": "1.12.2", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 4c7fbd480214f1e2508fc9a520c571f6274dce8f Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 12 Apr 2024 09:43:42 +0800 Subject: [PATCH 4/8] fix: model meta generator doesn't correctly identify relation names (#1244) --- packages/sdk/src/model-meta-generator.ts | 2 +- .../tests/regression/issue-1241.test.ts | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/integration/tests/regression/issue-1241.test.ts diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 99029e610..15e3abe40 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -206,7 +206,7 @@ function getBackLink(field: DataModelField) { } function getRelationName(field: DataModelField) { - const relAttr = field.attributes.find((attr) => attr.decl.ref?.name === 'relation'); + const relAttr = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); const relName = relAttr && relAttr.args?.[0] && getLiteral(relAttr.args?.[0].value); return relName; } diff --git a/tests/integration/tests/regression/issue-1241.test.ts b/tests/integration/tests/regression/issue-1241.test.ts new file mode 100644 index 000000000..3a53f567c --- /dev/null +++ b/tests/integration/tests/regression/issue-1241.test.ts @@ -0,0 +1,88 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { randomBytes } from 'crypto'; + +describe('issue 1241', () => { + it('regression', async () => { + const { enhance, prisma } = await loadSchema( + ` + model User { + id String @id @default(uuid()) + todos Todo[] + + @@auth + @@allow('all', true) + } + + model Todo { + id String @id @default(uuid()) + + user_id String + user User @relation(fields: [user_id], references: [id]) + + images File[] @relation("todo_images") + documents File[] @relation("todo_documents") + + @@allow('all', true) + } + + model File { + id String @id @default(uuid()) + s3_key String @unique + label String + + todo_image_id String? + todo_image Todo? @relation("todo_images", fields: [todo_image_id], references: [id]) + + todo_document_id String? + todo_document Todo? @relation("todo_documents", fields: [todo_document_id], references: [id]) + + @@allow('all', true) + } + `, + { logPrismaQuery: true } + ); + + const user = await prisma.user.create({ + data: {}, + }); + await prisma.todo.create({ + data: { + user_id: user.id, + + images: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `img-label-${i + 1}`, + })), + }, + + documents: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `doc-label-${i + 1}`, + })), + }, + }, + }); + + const db = enhance(); + + const todo = await db.todo.findFirst({ where: {}, include: { documents: true } }); + await expect( + db.todo.update({ + where: { id: todo.id }, + data: { + documents: { + update: todo.documents.map((doc: any) => { + return { + where: { s3_key: doc.s3_key }, + data: { label: 'updated' }, + }; + }), + }, + }, + include: { documents: true }, + }) + ).toResolveTruthy(); + }); +}); From 3e3e44c9de1d0afbaf02aebe70156ba778c2bfdf Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 12 Apr 2024 09:44:13 +0800 Subject: [PATCH 5/8] chore: bump version (#1245) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 627428d5e..bb6220a60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.12.2", + "version": "1.12.3", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index f62ecdc48..b57e88259 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.12.2" +version = "1.12.3" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 36c8a9dba..4770c785a 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.12.2", + "version": "1.12.3", "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 e5d8f2c0a..9fe87f55b 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.12.2", + "version": "1.12.3", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index e4ccd33b2..dc1c5eb81 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": "1.12.2", + "version": "1.12.3", "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 2dbe524db..72a3d5109 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": "1.12.2", + "version": "1.12.3", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 4eada7fc4..68ebc3d0d 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": "1.12.2", + "version": "1.12.3", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 2a7bb7bcb..13ed04b1e 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": "1.12.2", + "version": "1.12.3", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index e32edfdff..f8e7cd354 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": "1.12.2", + "version": "1.12.3", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 3aa0543c0..c351314d9 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.12.2", + "version": "1.12.3", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index ea559e4ff..f2e62b4bc 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.12.2", + "version": "1.12.3", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 16ffeac15..87e9bffb5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.12.2", + "version": "1.12.3", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 862ea6f20..9cdfbeda5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.12.2", + "version": "1.12.3", "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 d3ec5018b..0b6907977 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.12.2", + "version": "1.12.3", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 813748160e35913f5b26b79b81886ab9ddb02070 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 12 Apr 2024 15:04:23 +0800 Subject: [PATCH 6/8] fix(openapi): `CreateManyArgs` does not take array as input (#1246) --- packages/plugins/openapi/src/rpc-generator.ts | 10 ++++++- .../tests/baseline/rpc-3.0.0.baseline.yaml | 30 +++++++++++++++++-- .../tests/baseline/rpc-3.1.0.baseline.yaml | 30 +++++++++++++++++-- .../rpc-type-coverage-3.0.0.baseline.yaml | 10 ++++++- .../rpc-type-coverage-3.1.0.baseline.yaml | 10 ++++++- .../schema/src/plugins/zod/transformer.ts | 2 +- 6 files changed, 82 insertions(+), 10 deletions(-) diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 13bb91272..e9c66792c 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -176,7 +176,15 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['data'], properties: { - data: this.ref(`${modelName}CreateManyInput`), + data: this.oneOf( + this.ref(`${modelName}CreateManyInput`), + this.array(this.ref(`${modelName}CreateManyInput`)) + ), + skipDuplicates: { + type: 'boolean', + description: + 'Do not insert records with unique fields or ID fields that already exist.', + }, meta: this.ref('_Meta'), }, }, diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml index 0f936771e..eacf4d6d7 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml @@ -3194,7 +3194,15 @@ components: - data properties: data: - $ref: '#/components/schemas/UserCreateManyInput' + oneOf: + - $ref: '#/components/schemas/UserCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/UserCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' UserFindUniqueArgs: @@ -3374,7 +3382,15 @@ components: - data properties: data: - $ref: '#/components/schemas/ProfileCreateManyInput' + oneOf: + - $ref: '#/components/schemas/ProfileCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/ProfileCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' ProfileFindUniqueArgs: @@ -3554,7 +3570,15 @@ components: - data properties: data: - $ref: '#/components/schemas/Post_ItemCreateManyInput' + oneOf: + - $ref: '#/components/schemas/Post_ItemCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' Post_ItemFindUniqueArgs: diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml index d842234ab..9608839dd 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml @@ -3248,7 +3248,15 @@ components: - data properties: data: - $ref: '#/components/schemas/UserCreateManyInput' + oneOf: + - $ref: '#/components/schemas/UserCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/UserCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' UserFindUniqueArgs: @@ -3428,7 +3436,15 @@ components: - data properties: data: - $ref: '#/components/schemas/ProfileCreateManyInput' + oneOf: + - $ref: '#/components/schemas/ProfileCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/ProfileCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' ProfileFindUniqueArgs: @@ -3608,7 +3624,15 @@ components: - data properties: data: - $ref: '#/components/schemas/Post_ItemCreateManyInput' + oneOf: + - $ref: '#/components/schemas/Post_ItemCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' Post_ItemFindUniqueArgs: diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml index 6e219e6e3..bcb1e6d4c 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml @@ -2017,7 +2017,15 @@ components: - data properties: data: - $ref: '#/components/schemas/FooCreateManyInput' + oneOf: + - $ref: '#/components/schemas/FooCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/FooCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' FooFindUniqueArgs: diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml index e777f7580..21524fad5 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml @@ -2049,7 +2049,15 @@ components: - data properties: data: - $ref: '#/components/schemas/FooCreateManyInput' + oneOf: + - $ref: '#/components/schemas/FooCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/FooCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' FooFindUniqueArgs: diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 878eff82b..e5066c1e4 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -485,7 +485,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; imports.push( `import { ${modelName}CreateManyInputObjectSchema } from '../objects/${modelName}CreateManyInput.schema'` ); - codeBody += `createMany: z.object({ data: z.union([${modelName}CreateManyInputObjectSchema, z.array(${modelName}CreateManyInputObjectSchema)]) }),`; + codeBody += `createMany: z.object({ data: z.union([${modelName}CreateManyInputObjectSchema, z.array(${modelName}CreateManyInputObjectSchema)]), skipDuplicates: z.boolean().optional() }),`; operations.push(['createMany', origModelName]); } From d8c15135a7edb75b459b6f5f1736e5fa2d96a9fa Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 21 Apr 2024 22:36:18 +0800 Subject: [PATCH 7/8] fix(runtime): always use id fields to address existing entity during upsert (#1273) --- packages/plugins/swr/tests/test-model-meta.ts | 5 +- .../tanstack-query/tests/test-model-meta.ts | 5 +- packages/runtime/src/cross/utils.ts | 20 +- .../src/enhancements/policy/handler.ts | 101 +++++++-- .../src/enhancements/policy/policy-utils.ts | 30 +++ packages/sdk/src/model-meta-generator.ts | 45 +++- .../with-policy/multi-id-fields.test.ts | 166 ++++++++++++++- .../with-policy/nested-to-many.test.ts | 95 +++++++++ .../with-policy/nested-to-one.test.ts | 58 ++++++ .../with-policy/toplevel-operations.test.ts | 76 +++++++ .../tests/regression/issue-1271.test.ts | 192 ++++++++++++++++++ 11 files changed, 742 insertions(+), 51 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1271.test.ts diff --git a/packages/plugins/swr/tests/test-model-meta.ts b/packages/plugins/swr/tests/test-model-meta.ts index 41731ad18..9eb4c0e2f 100644 --- a/packages/plugins/swr/tests/test-model-meta.ts +++ b/packages/plugins/swr/tests/test-model-meta.ts @@ -43,7 +43,10 @@ export const modelMeta: ModelMeta = { ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, }, }, - uniqueConstraints: {}, + uniqueConstraints: { + user: { id: { name: 'id', fields: ['id'] } }, + post: { id: { name: 'id', fields: ['id'] } }, + }, deleteCascade: { user: ['Post'], }, diff --git a/packages/plugins/tanstack-query/tests/test-model-meta.ts b/packages/plugins/tanstack-query/tests/test-model-meta.ts index 41731ad18..9eb4c0e2f 100644 --- a/packages/plugins/tanstack-query/tests/test-model-meta.ts +++ b/packages/plugins/tanstack-query/tests/test-model-meta.ts @@ -43,7 +43,10 @@ export const modelMeta: ModelMeta = { ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, }, }, - uniqueConstraints: {}, + uniqueConstraints: { + user: { id: { name: 'id', fields: ['id'] } }, + post: { id: { name: 'id', fields: ['id'] } }, + }, deleteCascade: { user: ['Post'], }, diff --git a/packages/runtime/src/cross/utils.ts b/packages/runtime/src/cross/utils.ts index e4237dbc7..08cb29a7e 100644 --- a/packages/runtime/src/cross/utils.ts +++ b/packages/runtime/src/cross/utils.ts @@ -1,5 +1,5 @@ import { lowerCaseFirst } from 'lower-case-first'; -import { ModelMeta } from '.'; +import { ModelMeta, requireField } from '.'; /** * Gets field names in a data model entity, filtering out internal fields. @@ -47,17 +47,15 @@ export function zip(x: Enumerable, y: Enumerable): Array<[T1, T2 } export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) { - let fields = modelMeta.fields[lowerCaseFirst(model)]; - if (!fields) { + const uniqueConstraints = modelMeta.uniqueConstraints[lowerCaseFirst(model)] ?? {}; + + const entries = Object.values(uniqueConstraints); + if (entries.length === 0) { if (throwIfNotFound) { - throw new Error(`Unable to load fields for ${model}`); - } else { - fields = {}; + throw new Error(`Model ${model} does not have any id field`); } + return []; } - const result = Object.values(fields).filter((f) => f.isId); - if (result.length === 0 && throwIfNotFound) { - throw new Error(`model ${model} does not have an id field`); - } - return result; + + return entries[0].fields.map((f) => requireField(modelMeta, model, f)); } diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 6b7e67bea..34c74f9dd 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -420,7 +420,7 @@ export class PolicyProxyHandler implements Pr }); // return only the ids of the top-level entity - const ids = this.utils.getEntityIds(this.model, result); + const ids = this.utils.getEntityIds(model, result); return { result: ids, postWriteChecks: [...postCreateChecks.values()] }; } @@ -792,8 +792,10 @@ export class PolicyProxyHandler implements Pr } // proceed with the create and collect post-create checks - const { postWriteChecks: checks } = await this.doCreate(model, { data: createData }, db); + const { postWriteChecks: checks, result } = await this.doCreate(model, { data: createData }, db); postWriteChecks.push(...checks); + + return result; }; const _createMany = async ( @@ -881,18 +883,10 @@ export class PolicyProxyHandler implements Pr // check pre-update guard await this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); - // handles the case where id fields are updated - const postUpdateIds = this.utils.clone(existing); - for (const key of Object.keys(existing)) { - const updateValue = (args as any).data ? (args as any).data[key] : (args as any)[key]; - if ( - typeof updateValue === 'string' || - typeof updateValue === 'number' || - typeof updateValue === 'bigint' - ) { - postUpdateIds[key] = updateValue; - } - } + // handle the case where id fields are updated + const _args: any = args; + const updatePayload = _args.data && typeof _args.data === 'object' ? _args.data : _args; + const postUpdateIds = this.calculatePostUpdateIds(model, existing, updatePayload); // register post-update check await _registerPostUpdateCheck(model, existing, postUpdateIds); @@ -984,10 +978,13 @@ export class PolicyProxyHandler implements Pr // update case // check pre-update guard - await this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); + await this.utils.checkPolicyForUnique(model, existing, 'update', db, args); + + // handle the case where id fields are updated + const postUpdateIds = this.calculatePostUpdateIds(model, existing, args.update); // register post-update check - await _registerPostUpdateCheck(model, uniqueFilter, uniqueFilter); + await _registerPostUpdateCheck(model, existing, postUpdateIds); // convert upsert to update const convertedUpdate = { @@ -1021,9 +1018,22 @@ export class PolicyProxyHandler implements Pr if (existing) { // connect await _connectDisconnect(model, args.where, context); + return true; } else { // create - await _create(model, args.create, context); + const created = await _create(model, args.create, context); + + const upperContext = context.nestingPath[context.nestingPath.length - 2]; + if (upperContext?.where && context.field) { + // check if the where clause of the upper context references the id + // of the connected entity, if so, we need to update it + this.overrideForeignKeyFields(upperContext.model, upperContext.where, context.field, created); + } + + // remove the payload from the parent + this.removeFromParent(context.parent, 'connectOrCreate', args); + + return false; } }, @@ -1093,6 +1103,52 @@ export class PolicyProxyHandler implements Pr return { result, postWriteChecks }; } + // calculate id fields used for post-update check given an update payload + private calculatePostUpdateIds(_model: string, currentIds: any, updatePayload: any) { + const result = this.utils.clone(currentIds); + for (const key of Object.keys(currentIds)) { + const updateValue = updatePayload[key]; + if (typeof updateValue === 'string' || typeof updateValue === 'number' || typeof updateValue === 'bigint') { + result[key] = updateValue; + } + } + return result; + } + + // updates foreign key fields inside `payload` based on relation id fields in `newIds` + private overrideForeignKeyFields( + model: string, + payload: any, + relation: FieldInfo, + newIds: Record + ) { + if (!relation.foreignKeyMapping || Object.keys(relation.foreignKeyMapping).length === 0) { + return; + } + + // override foreign key values + for (const [id, fk] of Object.entries(relation.foreignKeyMapping)) { + if (payload[fk] !== undefined && newIds[id] !== undefined) { + payload[fk] = newIds[id]; + } + } + + // deal with compound id fields + const uniqueConstraints = this.utils.getUniqueConstraints(model); + for (const [name, constraint] of Object.entries(uniqueConstraints)) { + if (constraint.fields.length > 1) { + const target = payload[name]; + if (target) { + for (const [id, fk] of Object.entries(relation.foreignKeyMapping)) { + if (target[fk] !== undefined && newIds[id] !== undefined) { + target[fk] = newIds[id]; + } + } + } + } + } + } + // Validates the given update payload against Zod schema if any private validateUpdateInputSchema(model: string, data: any) { const schema = this.utils.getZodSchema(model, 'update'); @@ -1224,11 +1280,18 @@ export class PolicyProxyHandler implements Pr const { result, error } = await this.transaction(async (tx) => { const { where, create, update, ...rest } = args; - const existing = await this.utils.checkExistence(tx, this.model, args.where); + const existing = await this.utils.checkExistence(tx, this.model, where); if (existing) { // update case - const { result, postWriteChecks } = await this.doUpdate({ where, data: update, ...rest }, tx); + const { result, postWriteChecks } = await this.doUpdate( + { + where: this.utils.composeCompoundUniqueField(this.model, existing), + data: update, + ...rest, + }, + tx + ); await this.runPostWriteChecks(postWriteChecks, tx); return this.utils.readBack(tx, this.model, 'update', args, result); } else { diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 5a04c0430..4ff3044b8 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -569,6 +569,27 @@ export class PolicyUtil { } } + composeCompoundUniqueField(model: string, fieldData: any) { + const uniqueConstraints = this.modelMeta.uniqueConstraints?.[lowerCaseFirst(model)]; + if (!uniqueConstraints) { + return fieldData; + } + + // e.g.: { a: '1', b: '1' } => { a_b: { a: '1', b: '1' } } + const result: any = this.clone(fieldData); + for (const [name, constraint] of Object.entries(uniqueConstraints)) { + if (constraint.fields.length > 1 && constraint.fields.every((f) => fieldData[f] !== undefined)) { + // multi-field unique constraint, compose it + result[name] = constraint.fields.reduce( + (prev, field) => ({ ...prev, [field]: fieldData[field] }), + {} + ); + constraint.fields.forEach((f) => delete result[f]); + } + } + return result; + } + /** * Gets unique constraints for the given model. */ @@ -642,6 +663,15 @@ export class PolicyUtil { // preserve the original structure currQuery[currField.backLink] = { ...visitWhere }; } + + if (forMutationPayload && currQuery[currField.backLink]) { + // reconstruct compound unique field + currQuery[currField.backLink] = this.composeCompoundUniqueField( + backLinkField.type, + currQuery[currField.backLink] + ); + } + currQuery = currQuery[currField.backLink]; } currField = field; diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 15e3abe40..bb7cfd4b8 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -1,6 +1,7 @@ import { ArrayExpr, DataModel, + DataModelAttribute, DataModelField, isArrayExpr, isBooleanLiteral, @@ -239,10 +240,7 @@ function getFieldAttributes(field: DataModelField): RuntimeAttribute[] { function getUniqueConstraints(model: DataModel) { const constraints: Array<{ name: string; fields: string[] }> = []; - // model-level constraints - for (const attr of model.attributes.filter( - (attr) => attr.decl.ref?.name === '@@unique' || attr.decl.ref?.name === '@@id' - )) { + const extractConstraint = (attr: DataModelAttribute) => { const argsMap = getAttributeArgs(attr); if (argsMap.fields) { const fieldNames = (argsMap.fields as ArrayExpr).items.map( @@ -253,14 +251,45 @@ function getUniqueConstraints(model: DataModel) { // default constraint name is fields concatenated with underscores constraintName = fieldNames.join('_'); } - constraints.push({ name: constraintName, fields: fieldNames }); + return { name: constraintName, fields: fieldNames }; + } else { + return undefined; + } + }; + + const addConstraint = (constraint: { name: string; fields: string[] }) => { + if (!constraints.some((c) => c.name === constraint.name)) { + constraints.push(constraint); + } + }; + + // field-level @id first + for (const field of model.fields) { + if (hasAttribute(field, '@id')) { + addConstraint({ name: field.name, fields: [field.name] }); } } - // field-level constraints + // then model-level @@id + for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@id')) { + const constraint = extractConstraint(attr); + if (constraint) { + addConstraint(constraint); + } + } + + // then field-level @unique for (const field of model.fields) { - if (hasAttribute(field, '@id') || hasAttribute(field, '@unique')) { - constraints.push({ name: field.name, fields: [field.name] }); + if (hasAttribute(field, '@unique')) { + addConstraint({ name: field.name, fields: [field.name] }); + } + } + + // then model-level @@unique + for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@unique')) { + const constraint = extractConstraint(attr); + if (constraint) { + addConstraint(constraint); } } diff --git a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts index f48cdba45..0abb45559 100644 --- a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts +++ b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts @@ -12,8 +12,8 @@ describe('With Policy: multiple id fields', () => { process.chdir(origDir); }); - it('multi-id fields', async () => { - const { prisma, withPolicy } = await loadSchema( + it('multi-id fields crud', async () => { + const { prisma, enhance } = await loadSchema( ` model A { x String @@ -43,7 +43,7 @@ describe('With Policy: multiple id fields', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); @@ -69,8 +69,77 @@ describe('With Policy: multiple id fields', () => { ).toResolveTruthy(); }); + it('multi-id fields id update', async () => { + const { prisma, enhance } = await loadSchema( + ` + model A { + x String + y Int + value Int + b B? + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 0 && future().value > 1) + } + + model B { + b1 String + b2 String + value Int + a A @relation(fields: [ax, ay], references: [x, y]) + ax String + ay Int + + @@allow('read', value > 2) + @@allow('create', value > 1) + + @@unique([ax, ay]) + @@id([b1, b2]) + } + ` + ); + + const db = enhance(); + + await db.a.create({ data: { x: '1', y: 2, value: 1 } }); + + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 0 } }) + ).toBeRejectedByPolicy(); + + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }) + ).resolves.toMatchObject({ + x: '2', + y: 3, + value: 2, + }); + + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 0 }, + create: { x: '4', y: 5, value: 5 }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 3 }, + create: { x: '4', y: 5, value: 5 }, + }) + ).resolves.toMatchObject({ + x: '3', + y: 4, + value: 3, + }); + }); + it('multi-id auth', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { x String @@ -124,7 +193,7 @@ describe('With Policy: multiple id fields', () => { await prisma.user.create({ data: { x: '1', y: '1' } }); await prisma.user.create({ data: { x: '1', y: '2' } }); - const anonDb = withPolicy(); + const anonDb = enhance(); await expect( anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) @@ -139,7 +208,7 @@ describe('With Policy: multiple id fields', () => { anonDb.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) ).toBeRejectedByPolicy(); - const db = withPolicy({ x: '1', y: '1' }); + const db = enhance({ x: '1', y: '1' }); await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); @@ -149,13 +218,13 @@ describe('With Policy: multiple id fields', () => { await expect(db.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); await expect( - withPolicy(undefined).q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) + enhance(undefined).q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) ).toBeRejectedByPolicy(); await expect(db.q.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); }); it('multi-id to-one nested write', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model A { x Int @@ -177,7 +246,7 @@ describe('With Policy: multiple id fields', () => { } ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.b.create({ data: { @@ -205,7 +274,7 @@ describe('With Policy: multiple id fields', () => { }); it('multi-id to-many nested write', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model A { x Int @@ -237,7 +306,7 @@ describe('With Policy: multiple id fields', () => { } ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.b.create({ data: { @@ -270,4 +339,79 @@ describe('With Policy: multiple id fields', () => { expect(await db.b.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 5 })); expect(await db.c.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 6 })); }); + + it('multi-id fields nested id update', async () => { + const { enhance } = await loadSchema( + ` + model A { + x String + y Int + value Int + b B @relation(fields: [bId], references: [id]) + bId Int + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 0 && future().value > 1) + } + + model B { + id Int @id @default(autoincrement()) + a A[] + @@allow('all', true) + } + ` + ); + + const db = enhance(); + + await db.b.create({ data: { id: 1, a: { create: { x: '1', y: 1, value: 1 } } } }); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 0 } } } }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, + include: { a: true }, + }) + ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '2', y: 2, value: 2 })]) }); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 0 }, + create: { x: '4', y: '4', value: 4 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 3 }, + create: { x: '4', y: '4', value: 4 }, + }, + }, + }, + include: { a: true }, + }) + ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '3', y: 3, value: 3 })]) }); + }); }); diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts index b112aeeb1..664f3256f 100644 --- a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts +++ b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts @@ -284,6 +284,101 @@ describe('With Policy:nested to-many', () => { expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); }); + it('update id field', async () => { + const { withPolicy } = await loadSchema( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', true) + @@allow('update', value > 1 && future().value > 2) + } + ` + ); + + const db = withPolicy(); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 2 }, + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { id: '2', value: 1 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + let r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { id: '2', value: 3 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); + + await expect( + db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + upsert: { + where: { id: '2' }, + create: { id: '4', value: 4 }, + update: { id: '3', value: 1 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + upsert: { + where: { id: '2' }, + create: { id: '4', value: 4 }, + update: { id: '3', value: 4 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '3', value: 4 })])); + }); + it('update with create from one to many', async () => { const { withPolicy } = await loadSchema( ` diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts index 2e14b6d02..c510e6bb5 100644 --- a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts +++ b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts @@ -212,6 +212,64 @@ describe('With Policy:nested to-one', () => { ).toBeRejectedByPolicy(); }); + it('nested update id tests', async () => { + const { withPolicy } = await loadSchema( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1 && future().value > 2) + } + ` + ); + + const db = withPolicy(); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 2 }, + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 1 }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 3 }, + }, + }, + include: { m2: true }, + }) + ).resolves.toMatchObject({ m2: expect.objectContaining({ id: '2', value: 3 }) }); + }); + it('nested create', async () => { const { withPolicy } = await loadSchema( ` diff --git a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts index 99179e015..0ebf2a182 100644 --- a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts +++ b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts @@ -147,6 +147,82 @@ describe('With Policy: toplevel operations', () => { ).toBeTruthy(); }); + it('update id tests', async () => { + const { withPolicy } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 1) + @@allow('create', value > 0) + @@allow('update', value > 1 && future().value > 2) + } + ` + ); + + const db = withPolicy(); + + await db.model.create({ + data: { + id: '1', + value: 2, + }, + }); + + // update denied + await expect( + db.model.update({ + where: { id: '1' }, + data: { + id: '2', + value: 1, + }, + }) + ).toBeRejectedByPolicy(); + + // update success + await expect( + db.model.update({ + where: { id: '1' }, + data: { + id: '2', + value: 3, + }, + }) + ).resolves.toMatchObject({ id: '2', value: 3 }); + + // upsert denied + await expect( + db.model.upsert({ + where: { id: '2' }, + update: { + id: '3', + value: 1, + }, + create: { + id: '4', + value: 5, + }, + }) + ).toBeRejectedByPolicy(); + + // upsert success + await expect( + db.model.upsert({ + where: { id: '2' }, + update: { + id: '3', + value: 4, + }, + create: { + id: '4', + value: 5, + }, + }) + ).resolves.toMatchObject({ id: '3', value: 4 }); + }); + it('delete tests', async () => { const { withPolicy, prisma } = await loadSchema( ` diff --git a/tests/integration/tests/regression/issue-1271.test.ts b/tests/integration/tests/regression/issue-1271.test.ts new file mode 100644 index 000000000..d25cabb3b --- /dev/null +++ b/tests/integration/tests/regression/issue-1271.test.ts @@ -0,0 +1,192 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1271', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id @default(uuid()) + + @@auth + @@allow('all', true) + } + + model Test { + id String @id @default(uuid()) + linkingTable LinkingTable[] + key String @default('test') + locale String @default('EN') + + @@unique([key, locale]) + @@allow("all", true) + } + + model LinkingTable { + test_id String + test Test @relation(fields: [test_id], references: [id]) + + another_test_id String + another_test AnotherTest @relation(fields: [another_test_id], references: [id]) + + @@id([test_id, another_test_id]) + @@allow("all", true) + } + + model AnotherTest { + id String @id @default(uuid()) + status String + linkingTable LinkingTable[] + + @@allow("all", true) + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const test = await db.test.create({ + data: { + key: 'test1', + }, + }); + const anotherTest = await db.anotherTest.create({ + data: { + status: 'available', + }, + }); + + const updated = await db.test.upsert({ + where: { + key_locale: { + key: test.key, + locale: test.locale, + }, + }, + create: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + include: { + linkingTable: true, + }, + }); + + expect(updated.linkingTable).toHaveLength(1); + expect(updated.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const test2 = await db.test.upsert({ + where: { + key_locale: { + key: 'test2', + locale: 'locale2', + }, + }, + create: { + key: 'test2', + locale: 'locale2', + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + include: { + linkingTable: true, + }, + }); + expect(test2).toMatchObject({ key: 'test2', locale: 'locale2' }); + expect(test2.linkingTable).toHaveLength(1); + expect(test2.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const linkingTable = test2.linkingTable[0]; + + // connectOrCreate: connect case + const test3 = await db.test.create({ + data: { + key: 'test3', + locale: 'locale3', + }, + }); + console.log('test3 created:', test3); + const updated2 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: linkingTable.test_id, + another_test_id: linkingTable.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: test3.key, + locale: test3.locale, + }, + }, + create: { + key: 'test4', + locale: 'locale4', + }, + }, + }, + another_test: { connect: { id: anotherTest.id } }, + }, + include: { test: true }, + }); + expect(updated2).toMatchObject({ + test: expect.objectContaining({ key: 'test3', locale: 'locale3' }), + another_test_id: anotherTest.id, + }); + + // connectOrCreate: create case + const updated3 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: updated2.test_id, + another_test_id: updated2.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: 'test4', + locale: 'locale4', + }, + }, + create: { + key: 'test4', + locale: 'locale4', + }, + }, + }, + another_test: { connect: { id: anotherTest.id } }, + }, + include: { test: true }, + }); + expect(updated3).toMatchObject({ + test: expect.objectContaining({ key: 'test4', locale: 'locale4' }), + another_test_id: anotherTest.id, + }); + }); +}); From 69b781e43b3d04af17879b459f4f8c518886ab37 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 21 Apr 2024 22:37:48 +0800 Subject: [PATCH 8/8] chore: bump version (#1274) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index bb6220a60..af774f8c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.12.3", + "version": "1.12.4", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index b57e88259..474b361fc 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.12.3" +version = "1.12.4" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 4770c785a..c0bf7dcf1 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.12.3", + "version": "1.12.4", "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 9fe87f55b..3eb95b144 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.12.3", + "version": "1.12.4", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index dc1c5eb81..c5eb9d74d 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": "1.12.3", + "version": "1.12.4", "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 72a3d5109..7c03491e1 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": "1.12.3", + "version": "1.12.4", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 68ebc3d0d..7902164b1 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": "1.12.3", + "version": "1.12.4", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 13ed04b1e..25a5a94b7 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": "1.12.3", + "version": "1.12.4", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index f8e7cd354..880ac1066 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": "1.12.3", + "version": "1.12.4", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index c351314d9..893e1e5e0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.12.3", + "version": "1.12.4", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index f2e62b4bc..a7f201540 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.12.3", + "version": "1.12.4", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 87e9bffb5..51716a4a6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.12.3", + "version": "1.12.4", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 9cdfbeda5..39a34bf39 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.12.3", + "version": "1.12.4", "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 0b6907977..02c05d54a 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.12.3", + "version": "1.12.4", "description": "ZenStack Test Tools", "main": "index.js", "private": true,