diff --git a/package.json b/package.json index 2f21ed022..8ea863fae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.2.3", + "version": "2.2.4", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 92880227f..de7dd059f 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.2.3" +version = "2.2.4" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 14928f3cc..49d2503cc 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.2.3", + "version": "2.2.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 08fc1e2a7..ae6922b81 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.2.3", + "version": "2.2.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 19fcdc682..956503807 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.2.3", + "version": "2.2.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 6ae9664f8..ae5736220 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.2.3", + "version": "2.2.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 bb8ee9168..47e46917a 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.2.3", + "version": "2.2.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 884f20c26..ca43c5bb5 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.2.3", + "version": "2.2.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 4572f631d..8f19d7c24 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.2.3", + "version": "2.2.4", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 7ab829c8d..a08d3cf42 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.2.3", + "version": "2.2.4", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index edbaed78d..5785be1d9 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -194,15 +194,13 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { if (this.injectBaseFieldSelect(model, field, value, args, kind)) { delete args[kind][field]; - } else { - if (fieldInfo && this.isDelegateOrDescendantOfDelegate(fieldInfo.type)) { - let nextValue = value; - if (nextValue === true) { - // make sure the payload is an object - args[kind][field] = nextValue = {}; - } - this.injectSelectIncludeHierarchy(fieldInfo.type, nextValue); + } else if (fieldInfo.isDataModel) { + let nextValue = value; + if (nextValue === true) { + // make sure the payload is an object + args[kind][field] = nextValue = {}; } + this.injectSelectIncludeHierarchy(fieldInfo.type, nextValue); } } } @@ -392,8 +390,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { ); } - // note that we can't call `createMany` directly because it doesn't support - // nested created, which is needed for creating base entities + // `createMany` doesn't support nested create, which is needed for creating entities + // inheriting a delegate base, so we need to convert it to a regular `create` here. + // Note that the main difference is `create` doesn't support `skipDuplicates` as + // `createMany` does. + return this.queryUtils.transaction(this.prisma, async (tx) => { const r = await Promise.all( enumerate(args.data).map(async (item) => { @@ -425,17 +426,33 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { this.doProcessCreatePayload(model, args); }, - createMany: (model, args, _context) => { - if (args.skipDuplicates) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - '`createMany` with `skipDuplicates` set to true is not supported for delegated models' - ); - } + createMany: (model, args, context) => { + // `createMany` doesn't support nested create, which is needed for creating entities + // inheriting a delegate base, so we need to convert it to a regular `create` here. + // Note that the main difference is `create` doesn't support `skipDuplicates` as + // `createMany` does. - for (const item of enumerate(args?.data)) { - this.doProcessCreatePayload(model, item); + if (this.isDelegateOrDescendantOfDelegate(model)) { + if (args.skipDuplicates) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + '`createMany` with `skipDuplicates` set to true is not supported for delegated models' + ); + } + + // convert to regular `create` + let createPayload = context.parent.create ?? []; + if (!Array.isArray(createPayload)) { + createPayload = [createPayload]; + } + + for (const item of enumerate(args.data)) { + this.doProcessCreatePayload(model, item); + createPayload.push(item); + } + context.parent.create = createPayload; + delete context.parent['createMany']; } }, }); @@ -460,8 +477,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { } // ensure the full nested "create" structure is created for base types - private ensureBaseCreateHierarchy(model: string, result: any) { - let curr = result; + private ensureBaseCreateHierarchy(model: string, args: any) { + let curr = args; let base = this.getBaseModel(model); let sub = this.getModelInfo(model); @@ -478,6 +495,16 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { curr[baseRelationName].create[base.discriminator] = sub.name; } } + + // Look for base id field assignments in the current level, and push + // them down to the base level + for (const idField of getIdFields(this.options.modelMeta, base.name)) { + if (curr[idField.name] !== undefined) { + curr[baseRelationName].create[idField.name] = curr[idField.name]; + delete curr[idField.name]; + } + } + curr = curr[baseRelationName].create; sub = base; base = this.getBaseModel(base.name); diff --git a/packages/schema/package.json b/packages/schema/package.json index 1c5e0d711..f958ec03f 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": "2.2.3", + "version": "2.2.4", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2d1229fdb..547cefbb6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.2.3", + "version": "2.2.4", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 730f58d4d..8bc7500c4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.2.3", + "version": "2.2.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 75cd550d0..688d069f6 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.2.3", + "version": "2.2.4", "description": "ZenStack Test Tools", "main": "index.js", "private": true, diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index 8acc832c6..ea9b8efca 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -158,6 +158,22 @@ describe('Polymorphism Test', () => { ).resolves.toMatchObject({ count: 2 }); }); + it('create concrete with explicit id', async () => { + const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); + const db = enhance(); + + await expect( + db.ratedVideo.create({ data: { id: 1, duration: 100, url: 'xyz', rating: 5 } }) + ).resolves.toMatchObject({ + id: 1, + duration: 100, + url: 'xyz', + rating: 5, + assetType: 'Video', + videoType: 'RatedVideo', + }); + }); + it('read with concrete', async () => { const { db, user, video } = await setup(); diff --git a/tests/regression/tests/issue-1518.test.ts b/tests/regression/tests/issue-1518.test.ts new file mode 100644 index 000000000..83517a5ca --- /dev/null +++ b/tests/regression/tests/issue-1518.test.ts @@ -0,0 +1,31 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('issue 1518', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Activity { + id String @id @default(uuid()) + title String + type String + @@delegate(type) + @@allow('all', true) + } + + model TaskActivity extends Activity { + description String + @@map("task_activity") + @@allow('all', true) + } + ` + ); + + const db = enhance(); + await db.taskActivity.create({ + data: { + id: '00000000-0000-0000-0000-111111111111', + title: 'Test Activity', + description: 'Description of task', + }, + }); + }); +}); diff --git a/tests/regression/tests/issue-1520.test.ts b/tests/regression/tests/issue-1520.test.ts new file mode 100644 index 000000000..02ee1318c --- /dev/null +++ b/tests/regression/tests/issue-1520.test.ts @@ -0,0 +1,70 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1520', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Course { + id Int @id @default(autoincrement()) + title String + addedToNotifications AddedToCourseNotification[] + } + + model Group { + id Int @id @default(autoincrement()) + addedToNotifications AddedToGroupNotification[] + } + + model Notification { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + type String + senderId Int + receiverId Int + @@delegate (type) + } + + model AddedToGroupNotification extends Notification { + groupId Int + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + } + + model AddedToCourseNotification extends Notification { + courseId Int + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + } + `, + { enhancements: ['delegate'] } + ); + + const db = enhance(); + const r = await db.course.create({ + data: { + title: 'English classes', + addedToNotifications: { + createMany: { + data: [ + { + id: 1, + receiverId: 1, + senderId: 2, + }, + ], + }, + }, + }, + include: { addedToNotifications: true }, + }); + + expect(r.addedToNotifications).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + courseId: 1, + receiverId: 1, + senderId: 2, + }), + ]) + ); + }); +}); diff --git a/tests/regression/tests/issue-1522.test.ts b/tests/regression/tests/issue-1522.test.ts new file mode 100644 index 000000000..bc6a9fb34 --- /dev/null +++ b/tests/regression/tests/issue-1522.test.ts @@ -0,0 +1,92 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('issue 1522', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Course { + id String @id @default(uuid()) + title String + description String + sections Section[] + activities Activity[] + @@allow('all', true) + } + + model Section { + id String @id @default(uuid()) + title String + courseId String + idx Int @default(0) + course Course @relation(fields: [courseId], references: [id]) + activities Activity[] + } + + model Activity { + id String @id @default(uuid()) + title String + courseId String + sectionId String + idx Int @default(0) + type String + course Course @relation(fields: [courseId], references: [id]) + section Section @relation(fields: [sectionId], references: [id]) + @@delegate(type) + } + + model UrlActivity extends Activity { + url String + } + + model TaskActivity extends Activity { + description String + } + `, + { enhancements: ['delegate'] } + ); + + const db = enhance(); + const course = await db.course.create({ + data: { + title: 'Test Course', + description: 'Description of course', + sections: { + create: { + id: '00000000-0000-0000-0000-000000000002', + title: 'Test Section', + idx: 0, + }, + }, + }, + include: { + sections: true, + }, + }); + + const section = course.sections[0]; + await db.taskActivity.create({ + data: { + title: 'Test Activity', + description: 'Description of task', + idx: 0, + courseId: course.id, + sectionId: section.id, + }, + }); + + const found = await db.course.findFirst({ + where: { id: course.id }, + include: { + sections: { + orderBy: { idx: 'asc' }, + include: { + activities: { orderBy: { idx: 'asc' } }, + }, + }, + }, + }); + + expect(found.sections[0].activities[0]).toMatchObject({ + description: 'Description of task', + }); + }); +});