diff --git a/package.json b/package.json index c88ee776..1c0ec9ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 69214b57..1d4c8d69 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 9e4fae3e..72c236a2 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index e1fb4fa5..6e4c5b5b 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index df6765ae..cd15a02a 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index d061db42..957042e4 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "type": "module", "private": true, "license": "MIT" diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index d8472cc3..0359609b 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack", "publisher": "zenstack", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "displayName": "ZenStack Language Tools", "description": "VSCode extension for ZenStack ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index 8e0c1585..e7797f08 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/runtime/package.json b/packages/runtime/package.json index ab3a8b1c..401e064f 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "ZenStack Runtime", "type": "module", "scripts": { diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index b41cf728..728082d7 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -631,16 +631,15 @@ export type FindArgs< skip?: number; take?: number; orderBy?: OrArray>; - } + } & Distinct & + Cursor : {}) & (AllowFilter extends true ? { where?: WhereInput; } : {}) & - SelectIncludeOmit & - Distinct & - Cursor; + SelectIncludeOmit; export type FindManyArgs> = FindArgs; export type FindFirstArgs> = FindArgs; diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 24874d69..f11ad80c 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -68,6 +68,8 @@ export type CrudOperation = | 'aggregate' | 'groupBy'; +export type AllCrudOperation = CrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow'; + export type FromRelationContext = { model: GetModels; field: string; diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index 5b660aee..be83d102 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -44,7 +44,7 @@ export class InputValidator { return this.validate, true>, Parameters[1]>( model, 'find', - { unique, collection: true }, + { unique }, (model, options) => this.makeFindSchema(model, options), args, ); @@ -196,7 +196,7 @@ export class InputValidator { // #region Find - private makeFindSchema(model: string, options: { unique: boolean; collection: boolean }) { + private makeFindSchema(model: string, options: { unique: boolean }) { const fields: Record = {}; const where = this.makeWhereSchema(model, options.unique); if (options.unique) { @@ -208,13 +208,13 @@ export class InputValidator { fields['select'] = this.makeSelectSchema(model).optional(); fields['include'] = this.makeIncludeSchema(model).optional(); fields['omit'] = this.makeOmitSchema(model).optional(); - fields['distinct'] = this.makeDistinctSchema(model).optional(); - fields['cursor'] = this.makeCursorSchema(model).optional(); - if (options.collection) { + if (!options.unique) { fields['skip'] = this.makeSkipSchema().optional(); fields['take'] = this.makeTakeSchema().optional(); fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); + fields['cursor'] = this.makeCursorSchema(model).optional(); + fields['distinct'] = this.makeDistinctSchema(model).optional(); } let result: ZodType = z.strictObject(fields); @@ -589,15 +589,7 @@ export class InputValidator { for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = z - .union([ - z.literal(true), - z.strictObject({ - select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), - include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), - }), - ]) - .optional(); + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); } else { fields[field] = z.boolean().optional(); } @@ -634,6 +626,33 @@ export class InputValidator { return z.strictObject(fields); } + private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { + return z.union([ + z.boolean(), + z.strictObject({ + ...(fieldDef.array || fieldDef.optional + ? { + // to-many relations and optional to-one relations are filterable + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + } + : {}), + select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), + include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), + omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), + ...(fieldDef.array + ? { + // to-many relations can be ordered, skipped, taken, and cursor-located + orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + cursor: this.makeCursorSchema(fieldDef.type).optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), + } + : {}), + }), + ]); + } + private makeOmitSchema(model: string) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; @@ -652,21 +671,7 @@ export class InputValidator { for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = z - .union([ - z.literal(true), - z.strictObject({ - select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), - include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), - omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), - where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), - orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - distinct: this.makeDistinctSchema(fieldDef.type).optional(), - }), - ]) - .optional(); + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); } } diff --git a/packages/runtime/src/client/plugin.ts b/packages/runtime/src/client/plugin.ts index 7f087a5d..99ee2d92 100644 --- a/packages/runtime/src/client/plugin.ts +++ b/packages/runtime/src/client/plugin.ts @@ -2,7 +2,7 @@ import type { OperationNode, QueryResult, RootOperationNode, UnknownRow } from ' import type { ClientContract, ToKysely } from '.'; import type { GetModels, SchemaDef } from '../schema'; import type { MaybePromise } from '../utils/type-utils'; -import type { CrudOperation } from './crud/operations/base'; +import type { AllCrudOperation } from './crud/operations/base'; /** * ZenStack runtime plugin. @@ -61,7 +61,7 @@ type OnQueryHookContext = { /** * The operation that is being performed. */ - operation: CrudOperation; + operation: AllCrudOperation; /** * The query arguments. diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index 9b588a03..70cc400c 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -637,6 +637,97 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', expect(r?.posts[0]?.createdAt).toBeInstanceOf(Date); expect(r?.posts[0]?.published).toBeTypeOf('boolean'); + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + posts: { where: { published: true }, select: { title: true }, orderBy: { createdAt: 'desc' } }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post1' })], + }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + posts: { where: { published: true }, select: { title: true }, orderBy: { createdAt: 'desc' } }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post1' })], + }); + + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post2' })], + }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] }, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post2' })], + }); + + await expect( + client.post.findFirst({ + select: { author: { select: { email: true } } }, + }), + ).resolves.toMatchObject({ + author: { email: expect.any(String) }, + }); + await expect( + client.post.findFirst({ + include: { author: { select: { email: true } } }, + }), + ).resolves.toMatchObject({ + author: { email: expect.any(String) }, + }); + + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + profile: { where: { bio: 'My bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: expect.any(Object) }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + profile: { where: { bio: 'My bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: expect.any(Object) }); + + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + profile: { where: { bio: 'Other bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: null }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + profile: { where: { bio: 'Other bio' } }, + }, + }), + ).resolves.toMatchObject({ profile: null }); + await expect( client.user.findUnique({ where: { id: user.id }, @@ -778,7 +869,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', // @ts-expect-error include: { author: { where: { email: user.email } } }, }), - ).rejects.toThrow(`Field "author" doesn't support filtering`); + ).rejects.toThrow(`Invalid find args`); // sorting let u = await client.user.findUniqueOrThrow({ diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 21dec84f..98105ecc 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index c680ae18..0c983361 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 80d9bcf1..216a916d 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 5fd89dd4..5a51d6b9 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index d259c676..516ab27c 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 066824d6..b40b2b24 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "", "type": "module", "main": "index.js", diff --git a/samples/blog/package.json b/samples/blog/package.json index 469161b5..8dfec300 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "", "main": "index.js", "scripts": { diff --git a/tests/e2e/package.json b/tests/e2e/package.json index a08dfe41..928f1127 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "private": true, "type": "module", "scripts": {