diff --git a/.gitignore b/.gitignore index 503a4e114..9801ed968 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist *.db-journal *.tgz .pnpm-store +*.vsix diff --git a/packages/language/src/validators/expression-validator.ts b/packages/language/src/validators/expression-validator.ts index f455753f8..c2848c145 100644 --- a/packages/language/src/validators/expression-validator.ts +++ b/packages/language/src/validators/expression-validator.ts @@ -12,6 +12,7 @@ import { isReferenceExpr, isThisExpr, MemberAccessExpr, + UnaryExpr, type ExpressionType, } from '../generated/ast'; @@ -67,6 +68,9 @@ export default class ExpressionValidator implements AstValidator { case 'BinaryExpr': this.validateBinaryExpr(expr, accept); break; + case 'UnaryExpr': + this.validateUnaryExpr(expr, accept); + break; } } @@ -245,6 +249,12 @@ export default class ExpressionValidator implements AstValidator { } } + private validateUnaryExpr(expr: UnaryExpr, accept: ValidationAcceptor) { + if (expr.operand.$resolvedType && expr.operand.$resolvedType.decl !== 'Boolean') { + accept('error', `operand of "${expr.operator}" must be of Boolean type`, { node: expr.operand }); + } + } + private validateCollectionPredicate(expr: BinaryExpr, accept: ValidationAcceptor) { if (!expr.$resolvedType) { accept('error', 'collection predicate can only be used on an array of model type', { node: expr }); diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index 201c3229f..6c1cbd5e9 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -198,7 +198,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { if (parameters) { compiled = { ...compiled, parameters }; } - return connection.executeQuery(compiled); + return this.internalExecuteQuery(connection, compiled); } if ( @@ -246,7 +246,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { queryId, ); - const result = await connection.executeQuery(compiled); + const result = await this.internalExecuteQuery(connection, compiled); if (!this.driver.isTransactionConnection(connection)) { // not in a transaction, just call all after-mutation hooks @@ -470,7 +470,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { const compiled = this.compileQuery(selectQueryNode, createQueryId()); // execute the query directly with the given connection to avoid triggering // any other side effects - const result = await connection.executeQuery(compiled); + const result = await this.internalExecuteQuery(connection, compiled); return result.rows as Record[]; } @@ -483,4 +483,12 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { return condition2; } } + + private async internalExecuteQuery(connection: DatabaseConnection, compiledQuery: CompiledQuery) { + try { + return await connection.executeQuery(compiledQuery); + } catch (err) { + throw createDBQueryError('Failed to execute query', err, compiledQuery.sql, compiledQuery.parameters); + } + } } diff --git a/tests/regression/test/issue-503/regression.test.ts b/tests/regression/test/issue-503/regression.test.ts index c31864a70..e901ce426 100644 --- a/tests/regression/test/issue-503/regression.test.ts +++ b/tests/regression/test/issue-503/regression.test.ts @@ -2,7 +2,7 @@ import { createTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; import { schema } from './schema'; -describe('Regression tests for issues #503', () => { +describe('Regression tests for issue #503', () => { it('verifies the issue', async () => { const db = await createTestClient(schema); const r = await db.internalChat.create({ diff --git a/tests/regression/test/issue-505.test.ts b/tests/regression/test/issue-505.test.ts index da8482857..bbbca26c9 100644 --- a/tests/regression/test/issue-505.test.ts +++ b/tests/regression/test/issue-505.test.ts @@ -1,7 +1,7 @@ import { createTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -describe('Regression tests for issues 505', () => { +describe('Regression tests for issue 505', () => { it('verifies the issue', async () => { const db = await createTestClient( ` diff --git a/tests/regression/test/issue-510.test.ts b/tests/regression/test/issue-510.test.ts new file mode 100644 index 000000000..1816fe824 --- /dev/null +++ b/tests/regression/test/issue-510.test.ts @@ -0,0 +1,103 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression tests for issue 510', () => { + it('verifies the issue', async () => { + const schema = ` +type ID { + id String @id @default(nanoid()) +} + +type Timestamps { + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +type Base with ID, Timestamps { +} + +type AuthInfo { + id String + username String + role Role + + @@auth +} + +enum Role { + SUPERADMIN + ADMIN + USER +} + +enum FileStatus { + PENDING + UPLOADED + FAILED +} + +model User with Base { + username String @unique + passwordHash String + name String + role Role + + RefreshToken RefreshToken[] + File File[] + Post Post[] + + @@allow('all', auth().id == id) +} + +model File with Timestamps { + key String @id + + userId String + User User @relation(fields: [userId], references: [id]) + + originalFilename String + filename String + contentType String + size Int? + status FileStatus + + Post Post[] + + @@allow('all', auth().id == userId) +} + +model AuditLog { + timestamp DateTime @id @default(now()) + + action String + data Json + + @@deny('all', true) +} + +model RefreshToken with Base { + userId String + User User @relation(fields: [userId], references: [id]) + + revoked Boolean + + @@deny('all', true) +} + +model Post with Base { + userId String + User User @relation(fields: [userId], references: [id]) + + content String + imageKey String? + Image File? @relation(fields: [imageKey], references: [key]) + + @@allow('read', true) + @@allow('create', auth().id == userId && (!Image || auth().id == Image.userId)) + @@allow('update,delete', auth().id == userId) +} +`; + + await expect(createPolicyTestClient(schema)).rejects.toThrow(/operand of "!" must be of Boolean type/); + }); +});