diff --git a/package.json b/package.json index e76375caf..18434b628 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.12.0", + "version": "2.12.1", "description": "", "scripts": { "build": "pnpm -r --filter=\"!./packages/ide/*\" build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index ea42d3c2c..b788fb693 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.12.0" +version = "2.12.1" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 1756b93c7..76239e2bc 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.12.0", + "version": "2.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 cb604c634..c37178a38 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.12.0", + "version": "2.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 185fc341f..a4141f7e2 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.12.0", + "version": "2.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 18f44c96a..fc0b2c290 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.12.0", + "version": "2.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 470848279..4caeeef39 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.12.0", + "version": "2.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 d1f4b5852..0103e506c 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.12.0", + "version": "2.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 88accf0d6..bfe95e0a3 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.12.0", + "version": "2.12.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 894ca7541..79bc5322f 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.12.0", + "version": "2.12.1", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts index 3b27f1686..727038fd4 100644 --- a/packages/runtime/src/cross/model-meta.ts +++ b/packages/runtime/src/cross/model-meta.ts @@ -86,8 +86,7 @@ export type FieldInfo = { relationField?: string; /** - * Mapping from foreign key field names to relation field names. - * Only available on relation fields. + * Mapping from relation's pk to fk. Only available on relation fields. */ foreignKeyMapping?: Record; diff --git a/packages/runtime/src/enhancements/node/default-auth.ts b/packages/runtime/src/enhancements/node/default-auth.ts index f151d014f..8bdd76490 100644 --- a/packages/runtime/src/enhancements/node/default-auth.ts +++ b/packages/runtime/src/enhancements/node/default-auth.ts @@ -126,7 +126,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { return; } - if (context.field?.backLink && context.nestingPath.length > 1) { + if (context.field?.backLink) { // if the fk field is in a creation context where its implied by the parent, // we should not set the default value, e.g.: // @@ -134,23 +134,16 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { // parent.create({ data: { child: { create: {} } } }) // ``` // - // event if child's fk to parent has a default value, we should not set default + // even if child's fk to parent has a default value, we should not set default // value here - // fetch parent model from the parent context - const parentModel = getModelInfo( - this.options.modelMeta, - context.nestingPath[context.nestingPath.length - 2].model - ); - - if (parentModel) { - // get the opposite side of the relation for the current create context - const oppositeRelationField = requireField(this.options.modelMeta, model, context.field.backLink); - if (parentModel.name === oppositeRelationField.type) { - // if the opposite side matches the parent model, it means we currently in a creation context - // that implicitly sets this fk field - return; - } + // get the opposite side of the relation for the current create context + const oppositeRelationField = requireField(this.options.modelMeta, model, context.field.backLink); + if ( + oppositeRelationField.foreignKeyMapping && + Object.values(oppositeRelationField.foreignKeyMapping).includes(fieldInfo.name) + ) { + return; } } diff --git a/packages/runtime/src/enhancements/node/proxy.ts b/packages/runtime/src/enhancements/node/proxy.ts index e063f002b..c940c9af6 100644 --- a/packages/runtime/src/enhancements/node/proxy.ts +++ b/packages/runtime/src/enhancements/node/proxy.ts @@ -267,12 +267,7 @@ export function makeProxy( 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; - } + return makeProxy(result, modelMeta, makeHandler, name + '$ext', errorTransformer); }; } else { return $extends; @@ -289,7 +284,7 @@ export function makeProxy( return propVal; } - return createHandlerProxy(makeHandler(target, prop), propVal, prop, errorTransformer); + return createHandlerProxy(makeHandler(target, prop), propVal, prop, proxy, errorTransformer); }, }); @@ -303,10 +298,15 @@ function createHandlerProxy( handler: T, origTarget: any, model: string, + dbOrTx: any, errorTransformer?: ErrorTransformer ): T { return new Proxy(handler, { get(target, propKey) { + if (propKey === '$parent') { + return dbOrTx; + } + const prop = target[propKey as keyof T]; if (typeof prop !== 'function') { // the proxy handler doesn't have this method, fall back to the original target diff --git a/packages/schema/package.json b/packages/schema/package.json index 98c337e03..1980506f2 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.12.0", + "version": "2.12.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 46f969299..146755e2d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.12.0", + "version": "2.12.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 46720f463..a3fe81e73 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.12.0", + "version": "2.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 51533ab1f..0e2b4fba5 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.12.0", + "version": "2.12.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, diff --git a/tests/integration/tests/enhancements/proxy/extension-context.test.ts b/tests/integration/tests/enhancements/proxy/extension-context.test.ts new file mode 100644 index 000000000..f84fd1e84 --- /dev/null +++ b/tests/integration/tests/enhancements/proxy/extension-context.test.ts @@ -0,0 +1,78 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Proxy Extension Context', () => { + it('works', async () => { + const { enhance } = await loadSchema( + ` + model Counter { + model String @unique + value Int + + @@allow('all', true) + } + + model Address { + id String @id @default(cuid()) + city String + + @@allow('all', true) + } + ` + ); + + const db = enhance(); + const dbExtended = db.$extends({ + client: { + $one() { + return 1; + } + }, + model: { + $allModels: { + async createWithCounter(this: any, args: any) { + const modelName = this.$name; + const dbOrTx = this.$parent; + + // prisma exposes some internal properties, makes sure these are still preserved + expect(dbOrTx._engine).toBeDefined(); + + const fn = async (tx: any) => { + const counter = await tx.counter.findUnique({ + where: { model: modelName }, + }); + + await tx.counter.upsert({ + where: { model: modelName }, + update: { value: (counter?.value ?? 0) + tx.$one() }, + create: { model: modelName, value: tx.$one() }, + }); + + return tx[modelName].create(args); + }; + + if (dbOrTx['$transaction']) { + // not running in a transaction, so we need to create a new transaction + return dbOrTx.$transaction(fn); + } + + return fn(dbOrTx); + }, + }, + }, + }); + + const cities = ['Vienna', 'New York', 'Delhi']; + + await Promise.all([ + ...cities.map((city) => dbExtended.address.createWithCounter({ data: { city } })), + ...cities.map((city) => + dbExtended.$transaction((tx: any) => tx.address.createWithCounter({ data: { city: `${city}$tx` } })) + ), + ]); + + await expect(dbExtended.counter.findUniqueOrThrow({ where: { model: 'Address' } })).resolves.toMatchObject({ + model: 'Address', + value: cities.length * 2, + }); + }); +}); diff --git a/tests/regression/tests/issue-2014.test.ts b/tests/regression/tests/issue-2014.test.ts new file mode 100644 index 000000000..4ebdb2b4e --- /dev/null +++ b/tests/regression/tests/issue-2014.test.ts @@ -0,0 +1,39 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2014', () => { + it('regression', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Tenant { + id Int @id @default(autoincrement()) + + users User[] + } + + model User { + id Int @id @default(autoincrement()) + tenantId Int @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@allow('all', true) + } + `, + { logPrismaQuery: true } + ); + + const tenant = await prisma.tenant.create({ data: {} }); + const user = await prisma.user.create({ data: { tenantId: tenant.id } }); + + const db = enhance(user); + const extendedDb = db.$extends({}); + + await expect( + extendedDb.user.create({ + data: {}, + }) + ).resolves.toEqual({ + id: 2, + tenantId: tenant.id, + }); + }); +}); diff --git a/tests/regression/tests/issue-2019.test.ts b/tests/regression/tests/issue-2019.test.ts new file mode 100644 index 000000000..e5eea9254 --- /dev/null +++ b/tests/regression/tests/issue-2019.test.ts @@ -0,0 +1,87 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2019', () => { + it('regression', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Tenant { + id String @id @default(uuid()) + + users User[] + content Content[] + } + + model User { + id String @id @default(uuid()) + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + posts Post[] + likes PostUserLikes[] + + @@allow('all', true) + } + + model Content { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @id @default(uuid()) + contentType String + + @@delegate(contentType) + @@allow('all', true) + } + + model Post extends Content { + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + comments Comment[] + likes PostUserLikes[] + + @@allow('all', true) + } + + model PostUserLikes extends Content { + userId String + user User @relation(fields: [userId], references: [id]) + + postId String + post Post @relation(fields: [postId], references: [id]) + + @@unique([userId, postId]) + + @@allow('all', true) + } + + model Comment extends Content { + postId String + post Post @relation(fields: [postId], references: [id]) + + @@allow('all', true) + } + `, + { logPrismaQuery: true } + ); + + const tenant = await prisma.tenant.create({ data: {} }); + const user = await prisma.user.create({ data: { tenantId: tenant.id } }); + const db = enhance({ id: user.id, tenantId: tenant.id }); + const result = await db.post.create({ + data: { + likes: { + createMany: { + data: [ + { + userId: user.id, + }, + ], + }, + }, + }, + include: { + likes: true, + }, + }); + expect(result.likes[0].tenantId).toBe(tenant.id); + }); +});