diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..46a34ef4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ZenStack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NEW-FEATURES.md b/NEW-FEATURES.md index 639fdbbf..167ac913 100644 --- a/NEW-FEATURES.md +++ b/NEW-FEATURES.md @@ -1,2 +1,2 @@ - Cross-field comparison (for read and mutations) -- Custom policy functions +- Computed fields diff --git a/packages/cli/package.json b/packages/cli/package.json index dabb0499..8d3d01e1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,7 +30,6 @@ "dependencies": { "@types/node": "^20.0.0", "@zenstackhq/language": "workspace:*", - "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "async-exit-hook": "^2.0.1", "colors": "1.4.0", @@ -50,6 +49,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/semver": "^7.3.13", "@types/tmp": "^0.2.6", + "@zenstackhq/runtime": "workspace:*", "@zenstackhq/testtools": "workspace:*", "better-sqlite3": "^11.8.1", "tmp": "^0.2.3" diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 594681e9..de19daaa 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -4,11 +4,12 @@ "description": "ZenStack Runtime", "type": "module", "scripts": { - "build": "tsup-node && pnpm test-typecheck", - "test-typecheck": "tsc --project tsconfig.test.json", + "build": "tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", - "test": "vitest", + "test": "vitest && pnpm test:generate && pnpm test:typecheck", + "test:generate": "tsx test/typing/generate.ts", + "test:typecheck": "tsc --project tsconfig.test.json", "pack": "pnpm pack" }, "keywords": [], @@ -117,6 +118,7 @@ "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", "@zenstackhq/testtools": "workspace:*", - "tmp": "^0.2.3" + "tmp": "^0.2.3", + "tsx": "^4.19.2" } } diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index c020648f..760b94c0 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -64,15 +64,19 @@ type ModelSelectResult< Select, Omit > = { - [Key in keyof Select & GetFields as Select[Key] extends - | false - | undefined + [Key in keyof Select as Select[Key] extends false | undefined ? never : Key extends keyof Omit ? Omit[Key] extends true ? never : Key - : Key]: Key extends NonRelationFields + : Key extends '_count' + ? Select[Key] extends SelectCount + ? Key + : never + : Key]: Key extends '_count' + ? SelectCountResult + : Key extends NonRelationFields ? MapFieldType : Key extends RelationFields ? Select[Key] extends FindArgs< @@ -97,6 +101,12 @@ type ModelSelectResult< : never; }; +type SelectCountResult = C extends true + ? number + : C extends { select: infer S } + ? { [Key in keyof S]: number } + : never; + export type ModelResult< Schema extends SchemaDef, Model extends GetModels, @@ -366,7 +376,7 @@ export type SelectIncludeOmit< Model extends GetModels, AllowCount extends boolean > = { - select?: Select; + select?: Select; include?: Include; omit?: OmitFields; }; @@ -382,14 +392,15 @@ type Cursor> = { type Select< Schema extends SchemaDef, Model extends GetModels, - AllowCount extends Boolean + AllowCount extends Boolean, + AllowRelation extends boolean = true > = { [Key in NonRelationFields]?: true; -} & Include & +} & (AllowRelation extends true ? Include : {}) & // relation fields // relation count - (AllowCount extends true ? { _count?: RelationCount } : {}); + (AllowCount extends true ? { _count?: SelectCount } : {}); -type RelationCount> = +type SelectCount> = | true | { select: { @@ -619,8 +630,10 @@ export type CreateManyArgs< export type CreateManyAndReturnArgs< Schema extends SchemaDef, Model extends GetModels -> = CreateManyPayload & - Omit, 'include'>; +> = CreateManyPayload & { + select?: Select; + omit?: OmitFields; +}; type OptionalWrap< Schema extends SchemaDef, @@ -712,10 +725,11 @@ type CreateWithRelationInput< type ConnectOrCreatePayload< Schema extends SchemaDef, - Model extends GetModels + Model extends GetModels, + Without extends string = never > = { where: WhereUniqueInput; - create: CreateInput; + create: CreateInput; }; type CreateManyPayload< @@ -1132,10 +1146,15 @@ type ConnectOrCreateInput< ? OrArray< ConnectOrCreatePayload< Schema, - RelationFieldType + RelationFieldType, + OppositeRelationAndFK > > - : ConnectOrCreatePayload>; + : ConnectOrCreatePayload< + Schema, + RelationFieldType, + OppositeRelationAndFK + >; type DisconnectInput< Schema extends SchemaDef, @@ -1280,11 +1299,9 @@ export type ModelOperations< createMany(args?: CreateManyPayload): Promise; - createManyAndReturn( - args?: CreateManyAndReturnArgs - ): Promise< - ModelResult>[] - >; + createManyAndReturn>( + args?: SelectSubset> + ): Promise[]>; update>( args: SelectSubset> diff --git a/packages/runtime/src/client/crud/operations/create.ts b/packages/runtime/src/client/crud/operations/create.ts index 48c9a3ab..5ecd5283 100644 --- a/packages/runtime/src/client/crud/operations/create.ts +++ b/packages/runtime/src/client/crud/operations/create.ts @@ -55,7 +55,7 @@ export class CreateOperationHandler< }); }); - if (!result) { + if (!result && this.hasPolicyEnabled) { throw new RejectedByPolicyError( this.model, `result is not allowed to be read back` diff --git a/packages/runtime/src/client/crud/operations/update.ts b/packages/runtime/src/client/crud/operations/update.ts index 56ff4fbe..c3e29385 100644 --- a/packages/runtime/src/client/crud/operations/update.ts +++ b/packages/runtime/src/client/crud/operations/update.ts @@ -107,7 +107,7 @@ export class UpdateOperationHandler< }); }); - if (!result) { + if (!result && this.hasPolicyEnabled) { throw new RejectedByPolicyError( this.model, 'result is not allowed to be read back' diff --git a/packages/runtime/src/client/index.ts b/packages/runtime/src/client/index.ts index 9d1fa153..e0f29a7d 100644 --- a/packages/runtime/src/client/index.ts +++ b/packages/runtime/src/client/index.ts @@ -1,5 +1,7 @@ export { ZenStackClient } from './client-impl'; -export type * from './crud-types'; export type { ClientConstructor, ClientContract } from './contract'; +export type * from './crud-types'; +export * from './errors'; +export type { ClientOptions } from './options'; export type { CliGenerator } from './plugin'; export type { ToKysely } from './query-builder'; diff --git a/packages/runtime/src/plugins/policy/index.ts b/packages/runtime/src/plugins/policy/index.ts index 1110b645..9958cffb 100644 --- a/packages/runtime/src/plugins/policy/index.ts +++ b/packages/runtime/src/plugins/policy/index.ts @@ -1 +1,2 @@ +export * from './errors'; export * from './plugin'; diff --git a/packages/runtime/test/client-api/many-to-many.test.ts b/packages/runtime/test/client-api/many-to-many.test.ts deleted file mode 100644 index de60f9dc..00000000 --- a/packages/runtime/test/client-api/many-to-many.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describe } from 'vitest'; - -describe.skip('Many-to-many relation tests', () => {}); diff --git a/packages/runtime/test/test-schema.ts b/packages/runtime/test/test-schema.ts index 790a997f..65bea2b1 100644 --- a/packages/runtime/test/test-schema.ts +++ b/packages/runtime/test/test-schema.ts @@ -1,6 +1,9 @@ import Sqlite from 'better-sqlite3'; -import type { DataSourceProviderType, SchemaDef } from '../src/schema'; -import { ExpressionUtils } from '../src/schema/expression'; +import { + ExpressionUtils, + type DataSourceProviderType, + type SchemaDef, +} from '../src/schema'; export const schema = { provider: { diff --git a/packages/runtime/test/typing/generate.ts b/packages/runtime/test/typing/generate.ts new file mode 100644 index 00000000..869d9fd3 --- /dev/null +++ b/packages/runtime/test/typing/generate.ts @@ -0,0 +1,22 @@ +import { TsSchemaGenerator } from '@zenstackhq/sdk'; +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +async function main() { + const generator = new TsSchemaGenerator(); + const dir = path.dirname(fileURLToPath(import.meta.url)); + const zmodelPath = path.join(dir, 'typing-test.zmodel'); + const tsPath = path.join(dir, 'schema.ts'); + await generator.generate(zmodelPath, [], tsPath); + + const content = fs.readFileSync(tsPath, 'utf-8'); + fs.writeFileSync( + tsPath, + content.replace(/@zenstackhq\/runtime/g, '../../dist') + ); + + console.log('TS schema generated at:', tsPath); +} + +main(); diff --git a/packages/runtime/test/typing/schema.ts b/packages/runtime/test/typing/schema.ts new file mode 100644 index 00000000..6cc4110c --- /dev/null +++ b/packages/runtime/test/typing/schema.ts @@ -0,0 +1,241 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +import { type SchemaDef, ExpressionUtils } from "../../dist/schema"; +import path from "node:path"; +import url from "node:url"; +import { toDialectConfig } from "../../dist/utils/sqlite-utils"; +export const schema = { + provider: { + type: "sqlite", + dialectConfigProvider: function () { + return toDialectConfig("./test.db", typeof __dirname !== 'undefined' ? __dirname : path.dirname(url.fileURLToPath(import.meta.url))); + } + }, + models: { + User: { + fields: { + id: { + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + createdAt: { + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + name: { + type: "String" + }, + email: { + type: "String", + unique: true, + attributes: [{ name: "@unique" }] + }, + posts: { + type: "Post", + array: true, + relation: { opposite: "author" } + }, + profile: { + type: "Profile", + optional: true, + relation: { opposite: "user" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + email: { type: "String" } + } + }, + Post: { + fields: { + id: { + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + title: { + type: "String" + }, + content: { + type: "String" + }, + author: { + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + relation: { opposite: "posts", fields: ["authorId"], references: ["id"] } + }, + authorId: { + type: "Int", + foreignKeyFor: [ + "author" + ] + }, + tags: { + type: "Tag", + array: true, + relation: { opposite: "posts" } + }, + meta: { + type: "Meta", + optional: true, + relation: { opposite: "post" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Profile: { + fields: { + id: { + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + age: { + type: "Int" + }, + region: { + type: "Region", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("regionCountry"), ExpressionUtils.field("regionCity")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] }], + relation: { opposite: "profiles", fields: ["regionCountry", "regionCity"], references: ["country", "city"] } + }, + regionCountry: { + type: "String", + optional: true, + foreignKeyFor: [ + "region" + ] + }, + regionCity: { + type: "String", + optional: true, + foreignKeyFor: [ + "region" + ] + }, + user: { + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + relation: { opposite: "profile", fields: ["userId"], references: ["id"] } + }, + userId: { + type: "Int", + unique: true, + attributes: [{ name: "@unique" }], + foreignKeyFor: [ + "user" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + userId: { type: "Int" } + } + }, + Tag: { + fields: { + id: { + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + name: { + type: "String" + }, + posts: { + type: "Post", + array: true, + relation: { opposite: "tags" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Region: { + fields: { + country: { + type: "String", + id: true + }, + city: { + type: "String", + id: true + }, + zip: { + type: "String", + optional: true + }, + profiles: { + type: "Profile", + array: true, + relation: { opposite: "region" } + } + }, + attributes: [ + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] } + ], + idFields: ["country", "city"], + uniqueFields: { + country_city: { country: { type: "String" }, city: { type: "String" } } + } + }, + Meta: { + fields: { + id: { + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + reviewed: { + type: "Boolean" + }, + published: { + type: "Boolean" + }, + post: { + type: "Post", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + relation: { opposite: "meta", fields: ["postId"], references: ["id"] } + }, + postId: { + type: "Int", + unique: true, + attributes: [{ name: "@unique" }], + foreignKeyFor: [ + "post" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + postId: { type: "Int" } + } + } + }, + authType: "User", + plugins: {} +} as const satisfies SchemaDef; +export type SchemaType = typeof schema; diff --git a/packages/runtime/test/typing/typing-test.zmodel b/packages/runtime/test/typing/typing-test.zmodel new file mode 100644 index 00000000..2f3a4bb4 --- /dev/null +++ b/packages/runtime/test/typing/typing-test.zmodel @@ -0,0 +1,56 @@ +datasource db { + provider = "sqlite" + url = "file:./test.db" +} + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + email String @unique + posts Post[] + profile Profile? +} + +model Post { + id Int @id @default(autoincrement()) + title String + content String + author User @relation(fields: [authorId], references: [id]) + authorId Int + tags Tag[] + meta Meta? +} + +model Profile { + id Int @id @default(autoincrement()) + age Int + region Region? @relation(fields: [regionCountry, regionCity], references: [country, city]) + regionCountry String? + regionCity String? + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + +model Tag { + id Int @id @default(autoincrement()) + name String + posts Post[] +} + +model Region { + country String + city String + zip String? + profiles Profile[] + @@id([country, city]) +} + +model Meta { + id Int @id @default(autoincrement()) + reviewed Boolean + published Boolean + post Post @relation(fields: [postId], references: [id]) + postId Int @unique +} diff --git a/packages/runtime/test/typing/verify-typing.ts b/packages/runtime/test/typing/verify-typing.ts new file mode 100644 index 00000000..2ab78f08 --- /dev/null +++ b/packages/runtime/test/typing/verify-typing.ts @@ -0,0 +1,535 @@ +import { ZenStackClient } from '../../dist'; +import { schema } from './schema'; + +const client = new ZenStackClient(schema); + +async function main() { + await find(); + await create(); + await update(); + await del(); + await count(); + await aggregate(); + await groupBy(); +} + +async function find() { + const user1 = await client.user.findFirst({ + where: { + name: 'Alex', + }, + }); + console.log(user1?.name); + + const users = await client.user.findMany({ + include: { posts: true }, + omit: { email: true }, + }); + console.log(users.length); + console.log(users[0]?.name); + console.log(users[0]?.posts.length); + // @ts-expect-error + console.log(users[0]?.email); + + // @ts-expect-error select/omit are not allowed together + await client.user.findMany({ + select: { posts: true }, + omit: { email: true }, + }); + + // @ts-expect-error select/include are not allowed together + await client.user.findMany({ + select: { email: true }, + include: { posts: true }, + }); + + const user2 = await client.user.findUniqueOrThrow({ + where: { email: 'alex@zenstack.dev' }, + select: { email: true, profile: true }, + }); + console.log(user2.email); + console.log(user2.profile?.age); + + await client.user.findUnique({ + // @ts-expect-error expect unique filter + where: { name: 'Alex' }, + }); + + await client.user.findMany({ + skip: 1, + take: 1, + orderBy: { + email: 'asc', + name: 'desc', + }, + distinct: ['name'], + cursor: { id: 1 }, + }); + + const user3 = await client.user.findFirstOrThrow({ + select: { + _count: true, + posts: { + select: { _count: true }, + }, + }, + }); + console.log(user3._count); + console.log(user3.posts[0]?._count); + + ( + await client.user.findFirstOrThrow({ + select: { + _count: { + select: { posts: true }, + }, + }, + }) + )._count.posts; + + ( + await client.user.findFirstOrThrow({ + select: { + _count: { + select: { + posts: { + where: { title: { contains: 'Hello' } }, + }, + }, + }, + }, + }) + )._count.posts; + + ( + await client.user.findFirstOrThrow({ + select: { + posts: { + select: { + _count: { + select: { tags: true }, + }, + }, + }, + }, + }) + ).posts[0]?._count.tags; + + ( + await client.user.findFirstOrThrow({ + select: { + profile: { + include: { + region: true, + }, + }, + }, + }) + ).profile?.region?.city; +} + +async function create() { + await client.user.create({ + // @ts-expect-error email is required + data: { name: 'Alex' }, + }); + + await client.user.create({ + data: { + name: 'Alex', + email: 'alex@zenstack.dev', + profile: { + create: { + age: 20, + // @ts-expect-error userId is not allowed + userId: 1, + }, + }, + }, + }); + + // createMany + const { count: createCount } = await client.user.createMany({ + data: [ + { + name: 'Alex3', + email: 'alex3@zenstack.dev', + }, + { + name: 'Alex4', + email: 'alex4@zenstack.dev', + }, + ], + }); + console.log(createCount); + + // createManyAndReturn + await client.user.createManyAndReturn({ + data: [ + { + name: 'Alex5', + email: 'alex5@zenstack.dev', + }, + ], + // @ts-expect-error include is not allowed + include: { posts: true }, + }); + await client.user.createManyAndReturn({ + data: [ + { + name: 'Alex5', + email: 'alex5@zenstack.dev', + }, + ], + // @ts-expect-error selecting relation is not allowed + select: { posts: true }, + }); + const createdUsers = await client.user.createManyAndReturn({ + data: [ + { + name: 'Alex5', + email: 'alex5@zenstack.dev', + }, + ], + select: { email: true }, + }); + console.log(createdUsers.length); + console.log(createdUsers[0]?.email); + // @ts-expect-error + console.log(createdUsers[0]?.name); + + // connect + const region = await client.region.create({ + data: { + country: 'US', + city: 'Seattle', + }, + }); + + await client.profile.create({ + data: { + age: 20, + user: { connect: { id: 1 } }, + region: { + connect: { + country_city: { + country: region.country, + city: region.city, + }, + }, + }, + }, + }); + await client.profile.create({ + data: { + age: 20, + user: { connect: { id: 1 } }, + region: { + connect: { + // @ts-expect-error city is required + country_city: { + country: region.country, + }, + }, + }, + }, + }); + await client.profile.create({ + data: { + age: 20, + userId: 1, + regionCountry: region.country, + regionCity: region.city, + }, + }); + + // many-to-many + await client.post.create({ + data: { + title: 'Hello World', + content: 'This is a test post', + author: { connect: { id: 1 } }, + tags: { create: { name: 'tag1' } }, + }, + }); + await client.post.create({ + data: { + title: 'Hello World', + content: 'This is a test post', + author: { connect: { id: 1 } }, + tags: { create: [{ name: 'tag2' }, { name: 'tag3' }] }, + }, + }); + await client.tag.create({ + data: { + name: 'tag4', + posts: { + connectOrCreate: { + where: { id: 1 }, + create: { + title: 'Hello World', + content: 'This is a test post', + author: { connect: { id: 1 } }, + }, + }, + }, + }, + }); +} + +async function update() { + // @ts-expect-error where is required + await client.user.update({ + data: { + name: 'Alex', + email: 'alex@zenstack.dev', + }, + }); + + await client.user.update({ + where: { id: 1, AND: [{ name: 'Alex' }] }, + data: { + name: 'Alex', + email: 'alex@zenstack.dev', + }, + }); + + await client.user.update({ + where: { id: 1 }, + data: { + posts: { + create: { + title: 'Hello World', + content: 'This is a test post', + }, + createMany: { + data: [ + { + title: 'Hello World', + content: 'This is a test post', + }, + ], + skipDuplicates: true, + }, + connect: { id: 1 }, + connectOrCreate: { + where: { id: 1 }, + create: { + title: 'Hello World', + content: 'This is a test post', + }, + }, + set: [{ id: 1 }], + disconnect: [{ id: 1 }], + delete: [{ id: 1 }], + deleteMany: [{ id: 1 }], + update: { + where: { id: 1 }, + data: { + title: 'Hello World', + content: 'This is a test post', + }, + }, + upsert: { + where: { id: 1 }, + create: { + title: 'Hello World', + content: 'This is a test post', + }, + update: { + title: 'Hello World', + content: 'This is a test post', + }, + }, + updateMany: { + where: { id: 1 }, + data: { + title: 'Hello World', + content: 'This is a test post', + }, + }, + }, + }, + }); + + await client.user.update({ + where: { id: 1 }, + data: { + profile: { + connect: { id: 1 }, + connectOrCreate: { + where: { id: 1 }, + create: { age: 20 }, + }, + create: { age: 20 }, + delete: true, + disconnect: true, + update: { + age: 30, + }, + upsert: { + where: { id: 1 }, + create: { age: 20 }, + update: { age: 30 }, + }, + }, + }, + }); + + await client.user.update({ + where: { id: 1 }, + data: { + profile: { + delete: { age: { gt: 10 } }, + disconnect: { age: { gt: 10 } }, + }, + }, + }); + + await client.profile.update({ + where: { id: 1 }, + data: { + user: { + // @ts-expect-error delete is not allowed + delete: true, + }, + }, + }); + + await client.profile.update({ + where: { id: 1 }, + data: { + user: { + // @ts-expect-error disconnect is not allowed + disconnect: true, + }, + }, + }); + + // many-to-many + await client.post.update({ + where: { id: 1 }, + data: { + tags: { + connect: { id: 1 }, + }, + }, + }); + + // compound id + await client.profile.update({ + where: { id: 1 }, + data: { + region: { + connect: { + country_city: { + country: 'US', + city: 'Seattle', + }, + }, + }, + }, + }); + + await client.user.upsert({ + where: { id: 1 }, + create: { + name: 'Alex', + email: 'alex@zenstack.dev', + }, + update: { + name: 'Alex New', + email: 'alex@zenstack.dev', + }, + }); +} + +async function del() { + // @ts-expect-error where is required + await client.user.delete({}); + + // @ts-expect-error unique filter is required + await client.user.delete({ where: { name: 'Alex' } }); + + ( + await client.user.delete({ + where: { id: 1 }, + }) + ).email; + + (await client.user.deleteMany({})).count; +} + +async function count() { + await client.user.count(); + await client.user.count({ + where: { + name: 'Alex', + }, + }); + + const r = await client.user.count({ + select: { + _all: true, + email: true, + }, + }); + console.log(r._all); + console.log(r.email); +} + +async function aggregate() { + const r = await client.profile.aggregate({ + _count: true, + _avg: { age: true }, + _sum: { age: true }, + _min: { age: true }, + _max: { age: true }, + skip: 1, + take: 1, + orderBy: { + age: 'asc', + }, + }); + console.log(r._count); + console.log(r._avg.age); + + const r1 = await client.profile.aggregate({ + _count: { + _all: true, + regionCity: true, + }, + }); + console.log(r1._count._all); + console.log(r1._count.regionCity); +} + +async function groupBy() { + const r = await client.profile.groupBy({ + by: ['regionCountry', 'regionCity'], + _count: true, + _avg: { age: true }, + _sum: { age: true }, + _min: { age: true }, + _max: { age: true }, + skip: 1, + take: 1, + orderBy: { + age: 'asc', + }, + having: { + regionCity: { not: 'Seattle' }, + }, + }); + console.log(r[0]?._count); + console.log(r[0]?._avg.age); + console.log(r[0]?._sum.age); + console.log(r[0]?._min.age); + console.log(r[0]?._max.age); + console.log(r[0]?.regionCountry); + console.log(r[0]?.regionCity); + // @ts-expect-error age is not in the groupBy + console.log(r[0]?.age); +} + +main(); diff --git a/packages/runtime/test/utils.ts b/packages/runtime/test/utils.ts index b352be5d..90662c1f 100644 --- a/packages/runtime/test/utils.ts +++ b/packages/runtime/test/utils.ts @@ -7,8 +7,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { Client as PGClient, Pool } from 'pg'; import invariant from 'tiny-invariant'; +import type { ClientOptions } from '../src/client'; import { ZenStackClient } from '../src/client'; -import type { ClientOptions } from '../src/client/options'; import type { SchemaDef } from '../src/schema'; type SqliteSchema = SchemaDef & { provider: { type: 'sqlite' } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bfa8fd3..4432b4af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,9 +47,6 @@ importers: '@zenstackhq/language': specifier: workspace:* version: link:../language - '@zenstackhq/runtime': - specifier: workspace:* - version: link:../runtime '@zenstackhq/sdk': specifier: workspace:* version: link:../sdk @@ -96,6 +93,9 @@ importers: '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../runtime '@zenstackhq/testtools': specifier: workspace:* version: link:../testtools @@ -219,6 +219,9 @@ importers: tmp: specifier: ^0.2.3 version: 0.2.3 + tsx: + specifier: ^4.19.2 + version: 4.19.2 packages/sdk: dependencies: