diff --git a/packages/cli/package.json b/packages/cli/package.json index ecb796a7..3f38017e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,7 +22,7 @@ "zenstack": "bin/cli" }, "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run", diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 7b9efb7a..bd22b363 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts"] } diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index bb2bbb72..b3eb5964 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -4,7 +4,7 @@ "description": "ZenStack Common Helpers", "type": "module", "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" diff --git a/packages/common-helpers/tsconfig.json b/packages/common-helpers/tsconfig.json index 7b9efb7a..bd22b363 100644 --- a/packages/common-helpers/tsconfig.json +++ b/packages/common-helpers/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts"] } diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index bba1dd12..5c364042 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -4,7 +4,7 @@ "description": "Create a new ZenStack project", "type": "module", "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "lint": "eslint src --ext ts", "pack": "pnpm pack" }, diff --git a/packages/create-zenstack/tsconfig.json b/packages/create-zenstack/tsconfig.json index 7b9efb7a..bd22b363 100644 --- a/packages/create-zenstack/tsconfig.json +++ b/packages/create-zenstack/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts"] } diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index 7302a814..1a722f72 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -4,7 +4,7 @@ "description": "Kysely dialect for sql.js", "type": "module", "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" diff --git a/packages/dialects/sql.js/tsconfig.json b/packages/dialects/sql.js/tsconfig.json index 2125902f..7b457d06 100644 --- a/packages/dialects/sql.js/tsconfig.json +++ b/packages/dialects/sql.js/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, - "include": ["src/**/*", "test/**/*"] + "include": ["src/**/*"] } diff --git a/packages/ide/vscode/tsconfig.json b/packages/ide/vscode/tsconfig.json index 7b9efb7a..bd22b363 100644 --- a/packages/ide/vscode/tsconfig.json +++ b/packages/ide/vscode/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts"] } diff --git a/packages/language/package.json b/packages/language/package.json index e744294d..2a1fd6fb 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -10,7 +10,7 @@ ], "type": "module", "scripts": { - "build": "pnpm langium:generate && tsup-node", + "build": "pnpm langium:generate && tsc --noEmit && tsup-node", "lint": "eslint src --ext ts", "langium:generate": "langium generate", "langium:generate:production": "langium generate --mode=production", diff --git a/packages/language/src/validators/typedef-validator.ts b/packages/language/src/validators/typedef-validator.ts index 259608e5..d029d8ba 100644 --- a/packages/language/src/validators/typedef-validator.ts +++ b/packages/language/src/validators/typedef-validator.ts @@ -1,5 +1,5 @@ import type { ValidationAcceptor } from 'langium'; -import type { TypeDef, TypeDefField } from '../generated/ast'; +import type { DataField, TypeDef } from '../generated/ast'; import { validateAttributeApplication } from './attribute-application-validator'; import { validateDuplicatedDeclarations, type AstValidator } from './common'; @@ -21,7 +21,7 @@ export default class TypeDefValidator implements AstValidator { typeDef.fields.forEach((field) => this.validateField(field, accept)); } - private validateField(field: TypeDefField, accept: ValidationAcceptor): void { + private validateField(field: DataField, accept: ValidationAcceptor): void { field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } } diff --git a/packages/language/tsconfig.json b/packages/language/tsconfig.json index 7b9efb7a..bd22b363 100644 --- a/packages/language/tsconfig.json +++ b/packages/language/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts"] } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a24e2ec7..5648e106 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -4,7 +4,7 @@ "description": "ZenStack Runtime", "type": "module", "scripts": { - "build": "tsup-node && pnpm test:generate", + "build": "tsc --project tsconfig.build.json --noEmit && tsup-node && pnpm test:generate", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run && pnpm test:typecheck", diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index e3d6e3d0..2c1738cd 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -993,8 +993,14 @@ export abstract class BaseCrudDialect { return eb.not(this.and(eb, ...args)); } - fieldRef(model: string, field: string, eb: ExpressionBuilder, modelAlias?: string) { - return buildFieldRef(this.schema, model, field, this.options, eb, modelAlias); + fieldRef( + model: string, + field: string, + eb: ExpressionBuilder, + modelAlias?: string, + inlineComputedField = true, + ) { + return buildFieldRef(this.schema, model, field, this.options, eb, modelAlias, inlineComputedField); } // #endregion diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index 65ff3988..08d07950 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -226,13 +226,13 @@ export class PostgresCrudDialect extends BaseCrudDiale ...Object.entries(relationModelDef.fields) .filter(([, value]) => !value.relation) .filter(([name]) => !(typeof payload === 'object' && (payload.omit as any)?.[name] === true)) - .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, eb)]) + .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, eb, undefined, false)]) .flatMap((v) => v), ); } else if (payload.select) { // select specific fields objArgs.push( - ...Object.entries(payload.select) + ...Object.entries(payload.select) .filter(([, value]) => value) .map(([field, value]) => { if (field === '_count') { @@ -249,7 +249,7 @@ export class PostgresCrudDialect extends BaseCrudDiale ? // reference the synthesized JSON field eb.ref(`${parentAlias}$${relationField}$${field}.$j`) : // reference a plain field - this.fieldRef(relationModel, field, eb); + this.fieldRef(relationModel, field, eb, undefined, false); return [sql.lit(field), fieldValue]; } }) diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 127a13b4..3a2a4868 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -170,7 +170,7 @@ export class SqliteCrudDialect extends BaseCrudDialect ...Object.entries(relationModelDef.fields) .filter(([, value]) => !value.relation) .filter(([name]) => !(typeof payload === 'object' && (payload.omit as any)?.[name] === true)) - .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, eb)]) + .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, eb, undefined, false)]) .flatMap((v) => v), ); } else if (payload.select) { @@ -199,7 +199,10 @@ export class SqliteCrudDialect extends BaseCrudDialect ); return [sql.lit(field), subJson]; } else { - return [sql.lit(field), this.fieldRef(relationModel, field, eb) as ArgsType]; + return [ + sql.lit(field), + this.fieldRef(relationModel, field, eb, undefined, false) as ArgsType, + ]; } } }) diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index d2c7b649..ff690fe1 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -161,11 +161,15 @@ export function buildFieldRef( options: ClientOptions, eb: ExpressionBuilder, modelAlias?: string, + inlineComputedField = true, ): ExpressionWrapper { const fieldDef = requireField(schema, model, field); if (!fieldDef.computed) { return eb.ref(modelAlias ? `${modelAlias}.${field}` : field); } else { + if (!inlineComputedField) { + return eb.ref(modelAlias ? `${modelAlias}.${field}` : field); + } let computer: Function | undefined; if ('computedFields' in options) { const computedFields = options.computedFields as Record; diff --git a/packages/runtime/test/client-api/computed-fields.test.ts b/packages/runtime/test/client-api/computed-fields.test.ts index 353f495f..85897452 100644 --- a/packages/runtime/test/client-api/computed-fields.test.ts +++ b/packages/runtime/test/client-api/computed-fields.test.ts @@ -1,107 +1,121 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; import { createTestClient } from '../utils'; -describe('Computed fields tests', () => { - it('works with non-optional fields', async () => { - const db = await createTestClient( - ` +const TEST_DB = 'client-api-computed-fields'; + +describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as const }])( + 'Computed fields tests', + ({ provider }) => { + let db: any; + + afterEach(async () => { + await db?.$disconnect(); + }); + + it('works with non-optional fields', async () => { + db = await createTestClient( + ` model User { id Int @id @default(autoincrement()) name String upperName String @computed } `, - { - computedFields: { - User: { - upperName: (eb: any) => eb.fn('upper', ['name']), + { + provider, + dbName: TEST_DB, + computedFields: { + User: { + upperName: (eb: any) => eb.fn('upper', ['name']), + }, }, - }, - } as any, - ); - - await expect( - db.user.create({ - data: { id: 1, name: 'Alex' }, - }), - ).resolves.toMatchObject({ - upperName: 'ALEX', - }); + } as any, + ); - await expect( - db.user.findUnique({ - where: { id: 1 }, - select: { upperName: true }, - }), - ).resolves.toMatchObject({ - upperName: 'ALEX', - }); + await expect( + db.user.create({ + data: { id: 1, name: 'Alex' }, + }), + ).resolves.toMatchObject({ + upperName: 'ALEX', + }); - await expect( - db.user.findFirst({ - where: { upperName: 'ALEX' }, - }), - ).resolves.toMatchObject({ - upperName: 'ALEX', - }); + await expect( + db.user.findUnique({ + where: { id: 1 }, + select: { upperName: true }, + }), + ).resolves.toMatchObject({ + upperName: 'ALEX', + }); - await expect( - db.user.findFirst({ - where: { upperName: 'Alex' }, - }), - ).toResolveNull(); - - await expect( - db.user.findFirst({ - orderBy: { upperName: 'desc' }, - }), - ).resolves.toMatchObject({ - upperName: 'ALEX', - }); + await expect( + db.user.findFirst({ + where: { upperName: 'ALEX' }, + }), + ).resolves.toMatchObject({ + upperName: 'ALEX', + }); - await expect( - db.user.findFirst({ - orderBy: { upperName: 'desc' }, - take: -1, - }), - ).resolves.toMatchObject({ - upperName: 'ALEX', - }); + await expect( + db.user.findFirst({ + where: { upperName: 'Alex' }, + }), + ).toResolveNull(); - await expect( - db.user.aggregate({ - _count: { upperName: true }, - }), - ).resolves.toMatchObject({ - _count: { upperName: 1 }, - }); + await expect( + db.user.findFirst({ + orderBy: { upperName: 'desc' }, + }), + ).resolves.toMatchObject({ + upperName: 'ALEX', + }); + + await expect( + db.user.findFirst({ + orderBy: { upperName: 'desc' }, + take: -1, + }), + ).resolves.toMatchObject({ + upperName: 'ALEX', + }); - await expect( - db.user.groupBy({ - by: ['upperName'], - _count: { upperName: true }, - _max: { upperName: true }, - }), - ).resolves.toEqual([ - expect.objectContaining({ + await expect( + db.user.aggregate({ + _count: { upperName: true }, + }), + ).resolves.toMatchObject({ _count: { upperName: 1 }, - _max: { upperName: 'ALEX' }, - }), - ]); - }); + }); + + await expect( + db.user.groupBy({ + by: ['upperName'], + _count: { upperName: true }, + _max: { upperName: true }, + }), + ).resolves.toEqual([ + expect.objectContaining({ + _count: { upperName: 1 }, + _max: { upperName: 'ALEX' }, + }), + ]); + }); - it('is typed correctly for non-optional fields', async () => { - await createTestClient( - ` + it('is typed correctly for non-optional fields', async () => { + db = await createTestClient( + ` model User { id Int @id @default(autoincrement()) name String upperName String @computed } `, - { - extraSourceFiles: { - main: ` + { + provider, + dbName: TEST_DB, + extraSourceFiles: { + main: ` import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './schema'; @@ -125,50 +139,54 @@ async function main() { main(); `, + }, }, - }, - ); - }); + ); + }); - it('works with optional fields', async () => { - const db = await createTestClient( - ` + it('works with optional fields', async () => { + db = await createTestClient( + ` model User { id Int @id @default(autoincrement()) name String upperName String? @computed } `, - { - computedFields: { - User: { - upperName: (eb: any) => eb.lit(null), + { + provider, + dbName: TEST_DB, + computedFields: { + User: { + upperName: (eb: any) => eb.lit(null), + }, }, - }, - } as any, - ); - - await expect( - db.user.create({ - data: { id: 1, name: 'Alex' }, - }), - ).resolves.toMatchObject({ - upperName: null, + } as any, + ); + + await expect( + db.user.create({ + data: { id: 1, name: 'Alex' }, + }), + ).resolves.toMatchObject({ + upperName: null, + }); }); - }); - it('is typed correctly for optional fields', async () => { - await createTestClient( - ` + it('is typed correctly for optional fields', async () => { + db = await createTestClient( + ` model User { id Int @id @default(autoincrement()) name String upperName String? @computed } `, - { - extraSourceFiles: { - main: ` + { + provider, + dbName: TEST_DB, + extraSourceFiles: { + main: ` import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './schema'; @@ -191,8 +209,50 @@ async function main() { main(); `, + }, }, - }, - ); - }); -}); + ); + }); + + it('works with read from a relation', async () => { + db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + posts Post[] + postCount Int @computed +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int +} +`, + { + provider, + dbName: TEST_DB, + computedFields: { + User: { + postCount: (eb: any) => + eb + .selectFrom('Post') + .whereRef('Post.authorId', '=', 'User.id') + .select(() => eb.fn.countAll().as('count')), + }, + }, + } as any, + ); + + await db.user.create({ + data: { id: 1, name: 'Alex', posts: { create: { title: 'Post1' } } }, + }); + + await expect(db.post.findFirst({ select: { id: true, author: true } })).resolves.toMatchObject({ + author: expect.objectContaining({ postCount: 1 }), + }); + }); + }, +); diff --git a/packages/runtime/tsconfig.build.json b/packages/runtime/tsconfig.build.json new file mode 100644 index 00000000..aacb3723 --- /dev/null +++ b/packages/runtime/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["src/**/*"] +} diff --git a/packages/runtime/tsconfig.json b/packages/runtime/tsconfig.json index 2125902f..6056fb01 100644 --- a/packages/runtime/tsconfig.json +++ b/packages/runtime/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "rootDir": "." }, "include": ["src/**/*", "test/**/*"] } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 73120876..3cd7688b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -4,7 +4,7 @@ "description": "ZenStack SDK", "type": "module", "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 71637e39..07f91d53 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist", + "baseUrl": ".", "noUnusedLocals": false }, "include": ["src/**/*.ts"] diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index 08f82e6f..8e955bd4 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -6,7 +6,7 @@ "type": "module", "private": true, "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "lint": "eslint src --ext ts" }, "keywords": [], diff --git a/packages/tanstack-query/tsconfig.json b/packages/tanstack-query/tsconfig.json index 3df1d231..a64b0eb5 100644 --- a/packages/tanstack-query/tsconfig.json +++ b/packages/tanstack-query/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 9bd7778a..8b5d1cf2 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -4,7 +4,7 @@ "description": "ZenStack Test Tools", "type": "module", "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" diff --git a/packages/testtools/tsconfig.json b/packages/testtools/tsconfig.json index 3df1d231..a64b0eb5 100644 --- a/packages/testtools/tsconfig.json +++ b/packages/testtools/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/zod/package.json b/packages/zod/package.json index c80efbc3..fed86620 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -6,7 +6,7 @@ "main": "index.js", "private": true, "scripts": { - "build": "tsup-node", + "build": "tsc --noEmit && tsup-node", "lint": "eslint src --ext ts" }, "keywords": [], diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json index 3df1d231..a64b0eb5 100644 --- a/packages/zod/tsconfig.json +++ b/packages/zod/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "baseUrl": "." }, "include": ["src/**/*.ts", "test/**/*.ts"] }