From 411524404ed90e85e348a685093c03d1e2ccfc91 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 3 Oct 2025 21:06:19 -0700 Subject: [PATCH 01/17] refactor: move e2e orm tests to e2e project, fix bundle issues (#285) * refactor: move e2e orm tests to e2e project, fix bundle issues * add missing package * include all tests * more fixes * update lockfile * extract policy plugin to its own package * fix lint * addressing review comments --- packages/cli/package.json | 2 +- packages/plugins/policy/eslint.config.js | 4 + packages/plugins/policy/package.json | 50 +++ .../policy/src}/column-collector.ts | 2 +- .../policy/src}/expression-evaluator.ts | 2 +- .../policy/src}/expression-transformer.ts | 86 ++--- .../policy/src}/functions.ts | 12 +- .../policy => plugins/policy/src}/index.ts | 1 - .../policy => plugins/policy/src}/plugin.ts | 4 +- .../policy/src}/policy-handler.ts | 66 ++-- .../policy => plugins/policy/src}/types.ts | 4 +- .../policy => plugins/policy/src}/utils.ts | 4 +- .../policy/tsconfig.json} | 3 - packages/plugins/policy/tsup.config.ts | 12 + packages/plugins/policy/vitest.config.ts | 4 + packages/runtime/package.json | 12 +- .../src/client/crud/operations/base.ts | 3 +- .../src/client/crud/operations/create.ts | 2 +- .../src/client/crud/operations/delete.ts | 3 +- .../src/client/crud/operations/update.ts | 2 +- packages/runtime/src/client/errors.ts | 33 ++ .../src/client/executor/name-mapper.ts | 3 +- .../executor/zenstack-query-executor.ts | 2 +- packages/runtime/src/client/index.ts | 10 +- packages/runtime/src/client/kysely-utils.ts | 33 -- packages/runtime/src/client/query-utils.ts | 43 ++- packages/runtime/src/plugins/policy/errors.ts | 34 -- packages/runtime/src/schema/index.ts | 1 + packages/runtime/test/policy/utils.ts | 25 -- packages/runtime/tsconfig.json | 2 +- packages/runtime/tsup.config.ts | 1 - packages/runtime/vitest.config.ts | 10 +- .../src}/default-operation-node-visitor.ts | 0 .../src/utils => sdk/src}/expression-utils.ts | 2 +- packages/sdk/src/index.ts | 2 + packages/testtools/package.json | 24 +- .../test/utils.ts => testtools/src/client.ts} | 30 +- packages/testtools/src/index.ts | 2 + .../vitest.d.ts => testtools/src/types.d.ts} | 0 .../test => testtools/src}/vitest-ext.ts | 3 +- pnpm-lock.yaml | 365 ++++++++++++++++-- pnpm-workspace.yaml | 2 + .../cal.com/cal-com.test.ts | 0 .../{ => github-repos}/cal.com/schema.zmodel | 0 .../formbricks/formbricks.test.ts | 0 .../formbricks/schema.zmodel | 0 .../trigger.dev/schema.zmodel | 0 .../trigger.dev/trigger-dev.test.ts | 0 .../e2e/orm}/client-api/aggregate.test.ts | 4 +- .../e2e/orm}/client-api/compound-id.test.ts | 2 +- .../orm}/client-api/computed-fields.test.ts | 4 +- .../e2e/orm}/client-api/count.test.ts | 4 +- .../client-api/create-many-and-return.test.ts | 4 +- .../e2e/orm}/client-api/create-many.test.ts | 4 +- .../e2e/orm}/client-api/create.test.ts | 4 +- .../orm}/client-api/default-values.test.ts | 4 +- .../e2e/orm}/client-api/delegate.test.ts | 4 +- .../e2e/orm}/client-api/delete-many.test.ts | 4 +- .../e2e/orm}/client-api/delete.test.ts | 4 +- .../e2e/orm}/client-api/filter.test.ts | 4 +- .../e2e/orm}/client-api/find.test.ts | 6 +- .../e2e/orm}/client-api/group-by.test.ts | 4 +- .../e2e/orm}/client-api/import.test.ts | 2 +- .../e2e/orm}/client-api/mixin.test.ts | 2 +- .../e2e/orm}/client-api/name-mapping.test.ts | 4 +- .../e2e/orm}/client-api/raw-query.test.ts | 4 +- .../client-api/relation/many-to-many.test.ts | 2 +- .../client-api/relation/one-to-many.test.ts | 2 +- .../client-api/relation/one-to-one.test.ts | 2 +- .../client-api/relation/self-relation.test.ts | 2 +- .../e2e/orm}/client-api/scalar-list.test.ts | 2 +- .../e2e/orm}/client-api/transaction.test.ts | 4 +- .../e2e/orm}/client-api/type-coverage.test.ts | 2 +- .../orm}/client-api/typed-json-fields.test.ts | 2 +- .../orm}/client-api/undefined-values.test.ts | 4 +- .../e2e/orm}/client-api/update-many.test.ts | 4 +- .../e2e/orm}/client-api/update.test.ts | 4 +- .../e2e/orm}/client-api/upsert.test.ts | 4 +- .../e2e/orm}/client-api/utils.ts | 2 +- .../entity-mutation-hooks.test.ts | 4 +- .../orm}/plugin-infra/on-kysely-query.test.ts | 4 +- .../orm}/plugin-infra/on-query-hooks.test.ts | 4 +- .../e2e/orm}/policy/auth-equality.test.ts | 2 +- .../e2e/orm}/policy/basic-schema-read.test.ts | 8 +- .../e2e/orm}/policy/crud/create.test.ts | 2 +- .../e2e/orm}/policy/crud/delete.test.ts | 2 +- .../e2e/orm}/policy/crud/dumb-rules.test.ts | 2 +- .../e2e/orm}/policy/crud/post-update.test.ts | 2 +- .../e2e/orm}/policy/crud/read.test.ts | 2 +- .../e2e/orm}/policy/crud/update.test.ts | 2 +- .../e2e/orm}/policy/migrated/auth.test.ts | 2 +- .../policy/migrated/client-extensions.test.ts | 4 +- .../migrated/connect-disconnect.test.ts | 8 +- .../migrated/create-many-and-return.test.ts | 2 +- .../cross-model-field-comparison.test.ts | 2 +- .../policy/migrated/current-model.test.ts | 11 +- .../policy/migrated/current-operation.test.ts | 2 +- .../orm}/policy/migrated/deep-nested.test.ts | 2 +- .../orm}/policy/migrated/empty-policy.test.ts | 2 +- .../policy/migrated/field-comparison.test.ts | 2 +- .../migrated/multi-field-unique.test.ts | 4 +- .../policy/migrated/multi-id-fields.test.ts | 2 +- .../policy/migrated/nested-to-many.test.ts | 2 +- .../policy/migrated/nested-to-one.test.ts | 2 +- .../e2e/orm}/policy/migrated/omit.test.ts | 3 +- .../policy/migrated/petstore-sample.test.ts | 2 +- .../policy/migrated/query-reduction.test.ts | 2 +- .../policy/migrated/relation-check.test.ts | 2 +- .../relation-many-to-many-filter.test.ts | 2 +- .../relation-one-to-many-filter.test.ts | 2 +- .../relation-one-to-one-filter.test.ts | 2 +- .../policy/migrated/self-relation.test.ts | 2 +- .../orm}/policy/migrated/todo-sample.test.ts | 4 +- .../migrated/toplevel-operations.test.ts | 2 +- .../orm}/policy/migrated/unique-as-id.test.ts | 2 +- .../migrated/update-many-and-return.test.ts | 2 +- .../e2e/orm}/policy/migrated/view.test.ts | 2 +- .../e2e/orm}/policy/mixin.test.ts | 2 +- .../e2e/orm}/policy/policy-functions.test.ts | 2 +- .../e2e/orm}/policy/todo-sample.test.ts | 2 +- .../prisma-consistency/attributes.test.ts | 6 +- .../prisma-consistency/basic-models.test.ts | 6 +- .../prisma-consistency/compound-ids.test.ts | 6 +- .../prisma-consistency/datasource.test.ts | 0 .../prisma-consistency/enums.test.ts | 6 +- .../prisma-consistency/field-types.test.ts | 6 +- .../relation-validation.test.ts | 10 +- .../relations-many-to-many.test.ts | 6 +- .../relations-one-to-many.test.ts | 0 .../relations-one-to-one.test.ts | 6 +- .../prisma-consistency/relations-self.test.ts | 6 +- .../prisma-consistency/test-utils.ts | 2 +- .../unique-constraints.test.ts | 6 +- .../orm}/query-builder/query-builder.test.ts | 2 +- .../e2e/orm}/schemas/basic/helper.ts | 0 .../e2e/orm}/schemas/basic/index.ts | 0 .../e2e/orm}/schemas/basic/input.ts | 0 .../e2e/orm}/schemas/basic/models.ts | 0 .../e2e/orm}/schemas/basic/schema.ts | 2 +- .../e2e/orm}/schemas/basic/schema.zmodel | 0 .../e2e/orm}/schemas/delegate/input.ts | 0 .../e2e/orm}/schemas/delegate/models.ts | 0 .../e2e/orm}/schemas/delegate/schema.ts | 2 +- .../e2e/orm}/schemas/delegate/schema.zmodel | 0 .../e2e/orm}/schemas/delegate/typecheck.ts | 2 +- .../e2e/orm}/schemas/name-mapping/input.ts | 0 .../e2e/orm}/schemas/name-mapping/models.ts | 0 .../e2e/orm}/schemas/name-mapping/schema.ts | 2 +- .../orm}/schemas/name-mapping/schema.zmodel | 0 .../e2e/orm}/schemas/petstore/input.ts | 0 .../e2e/orm}/schemas/petstore/models.ts | 0 .../e2e/orm}/schemas/petstore/schema.ts | 2 +- .../e2e/orm}/schemas/petstore/schema.zmodel | 0 .../e2e/orm}/schemas/todo/input.ts | 0 .../e2e/orm}/schemas/todo/models.ts | 0 .../e2e/orm}/schemas/todo/schema.ts | 2 +- .../e2e/orm}/schemas/todo/todo.zmodel | 0 .../e2e/orm}/schemas/typing/input.ts | 0 .../e2e/orm}/schemas/typing/models.ts | 0 .../e2e/orm}/schemas/typing/schema.ts | 2 +- .../e2e/orm}/schemas/typing/schema.zmodel | 0 .../e2e/orm}/schemas/typing/typecheck.ts | 2 +- .../e2e/orm}/scripts/generate.ts | 5 - tests/e2e/package.json | 22 +- .../e2e/tsconfig.json | 5 +- tests/e2e/vitest.config.ts | 9 +- tests/regression/tsconfig.json | 3 +- tests/regression/vitest.config.ts | 9 +- vitest.config.ts | 2 +- 169 files changed, 820 insertions(+), 444 deletions(-) create mode 100644 packages/plugins/policy/eslint.config.js create mode 100644 packages/plugins/policy/package.json rename packages/{runtime/src/plugins/policy => plugins/policy/src}/column-collector.ts (85%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/expression-evaluator.ts (99%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/expression-transformer.ts (93%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/functions.ts (82%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/index.ts (50%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/plugin.ts (81%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/policy-handler.ts (94%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/types.ts (73%) rename packages/{runtime/src/plugins/policy => plugins/policy/src}/utils.ts (97%) rename packages/{runtime/tsconfig.build.json => plugins/policy/tsconfig.json} (62%) create mode 100644 packages/plugins/policy/tsup.config.ts create mode 100644 packages/plugins/policy/vitest.config.ts delete mode 100644 packages/runtime/src/client/kysely-utils.ts delete mode 100644 packages/runtime/src/plugins/policy/errors.ts delete mode 100644 packages/runtime/test/policy/utils.ts rename packages/{runtime/src/utils => sdk/src}/default-operation-node-visitor.ts (100%) rename packages/{runtime/src/utils => sdk/src}/expression-utils.ts (98%) rename packages/{runtime/test/utils.ts => testtools/src/client.ts} (86%) rename packages/{runtime/test/vitest.d.ts => testtools/src/types.d.ts} (100%) rename packages/{runtime/test => testtools/src}/vitest-ext.ts (96%) rename tests/e2e/{ => github-repos}/cal.com/cal-com.test.ts (100%) rename tests/e2e/{ => github-repos}/cal.com/schema.zmodel (100%) rename tests/e2e/{ => github-repos}/formbricks/formbricks.test.ts (100%) rename tests/e2e/{ => github-repos}/formbricks/schema.zmodel (100%) rename tests/e2e/{ => github-repos}/trigger.dev/schema.zmodel (100%) rename tests/e2e/{ => github-repos}/trigger.dev/trigger-dev.test.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/client-api/aggregate.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/client-api/compound-id.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/computed-fields.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/client-api/count.test.ts (94%) rename {packages/runtime/test => tests/e2e/orm}/client-api/create-many-and-return.test.ts (95%) rename {packages/runtime/test => tests/e2e/orm}/client-api/create-many.test.ts (93%) rename {packages/runtime/test => tests/e2e/orm}/client-api/create.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/client-api/default-values.test.ts (96%) rename {packages/runtime/test => tests/e2e/orm}/client-api/delegate.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/delete-many.test.ts (94%) rename {packages/runtime/test => tests/e2e/orm}/client-api/delete.test.ts (92%) rename {packages/runtime/test => tests/e2e/orm}/client-api/filter.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/find.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/group-by.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/client-api/import.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/client-api/mixin.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/client-api/name-mapping.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/raw-query.test.ts (95%) rename {packages/runtime/test => tests/e2e/orm}/client-api/relation/many-to-many.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/relation/one-to-many.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/client-api/relation/one-to-one.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/client-api/relation/self-relation.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/scalar-list.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/transaction.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/client-api/type-coverage.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/client-api/typed-json-fields.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/client-api/undefined-values.test.ts (90%) rename {packages/runtime/test => tests/e2e/orm}/client-api/update-many.test.ts (96%) rename {packages/runtime/test => tests/e2e/orm}/client-api/update.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/client-api/upsert.test.ts (94%) rename {packages/runtime/test => tests/e2e/orm}/client-api/utils.ts (92%) rename {packages/runtime/test => tests/e2e/orm}/plugin-infra/entity-mutation-hooks.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/plugin-infra/on-kysely-query.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/plugin-infra/on-query-hooks.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/auth-equality.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/policy/basic-schema-read.test.ts (90%) rename {packages/runtime/test => tests/e2e/orm}/policy/crud/create.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/crud/delete.test.ts (96%) rename {packages/runtime/test => tests/e2e/orm}/policy/crud/dumb-rules.test.ts (94%) rename {packages/runtime/test => tests/e2e/orm}/policy/crud/post-update.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/crud/read.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/crud/update.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/auth.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/client-extensions.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/connect-disconnect.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/create-many-and-return.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/cross-model-field-comparison.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/current-model.test.ts (95%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/current-operation.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/deep-nested.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/empty-policy.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/field-comparison.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/multi-field-unique.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/multi-id-fields.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/nested-to-many.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/nested-to-one.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/omit.test.ts (95%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/petstore-sample.test.ts (95%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/query-reduction.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/relation-check.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/relation-many-to-many-filter.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/relation-one-to-many-filter.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/relation-one-to-one-filter.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/self-relation.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/todo-sample.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/toplevel-operations.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/unique-as-id.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/update-many-and-return.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/migrated/view.test.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/policy/mixin.test.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/policy/policy-functions.test.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/policy/todo-sample.test.ts (99%) rename tests/e2e/{ => orm}/prisma-consistency/attributes.test.ts (95%) rename tests/e2e/{ => orm}/prisma-consistency/basic-models.test.ts (96%) rename tests/e2e/{ => orm}/prisma-consistency/compound-ids.test.ts (93%) rename tests/e2e/{ => orm}/prisma-consistency/datasource.test.ts (100%) rename tests/e2e/{ => orm}/prisma-consistency/enums.test.ts (93%) rename tests/e2e/{ => orm}/prisma-consistency/field-types.test.ts (94%) rename tests/e2e/{ => orm}/prisma-consistency/relation-validation.test.ts (93%) rename tests/e2e/{ => orm}/prisma-consistency/relations-many-to-many.test.ts (96%) rename tests/e2e/{ => orm}/prisma-consistency/relations-one-to-many.test.ts (100%) rename tests/e2e/{ => orm}/prisma-consistency/relations-one-to-one.test.ts (96%) rename tests/e2e/{ => orm}/prisma-consistency/relations-self.test.ts (95%) rename tests/e2e/{ => orm}/prisma-consistency/test-utils.ts (96%) rename tests/e2e/{ => orm}/prisma-consistency/unique-constraints.test.ts (95%) rename {packages/runtime/test => tests/e2e/orm}/query-builder/query-builder.test.ts (96%) rename {packages/runtime/test => tests/e2e/orm}/schemas/basic/helper.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/basic/index.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/basic/input.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/basic/models.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/basic/schema.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/schemas/basic/schema.zmodel (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/delegate/input.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/delegate/models.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/delegate/schema.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/schemas/delegate/schema.zmodel (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/delegate/typecheck.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/schemas/name-mapping/input.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/name-mapping/models.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/name-mapping/schema.ts (97%) rename {packages/runtime/test => tests/e2e/orm}/schemas/name-mapping/schema.zmodel (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/petstore/input.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/petstore/models.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/petstore/schema.ts (98%) rename {packages/runtime/test => tests/e2e/orm}/schemas/petstore/schema.zmodel (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/todo/input.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/todo/models.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/todo/schema.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/schemas/todo/todo.zmodel (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/typing/input.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/typing/models.ts (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/typing/schema.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/schemas/typing/schema.zmodel (100%) rename {packages/runtime/test => tests/e2e/orm}/schemas/typing/typecheck.ts (99%) rename {packages/runtime/test => tests/e2e/orm}/scripts/generate.ts (76%) rename packages/runtime/tsconfig.test.json => tests/e2e/tsconfig.json (69%) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1275a901..3338728a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -53,7 +53,7 @@ "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", - "better-sqlite3": "^12.2.0", + "better-sqlite3": "catalog:", "tmp": "catalog:" } } diff --git a/packages/plugins/policy/eslint.config.js b/packages/plugins/policy/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/plugins/policy/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json new file mode 100644 index 00000000..eb869945 --- /dev/null +++ b/packages/plugins/policy/package.json @@ -0,0 +1,50 @@ +{ + "name": "@zenstackhq/plugin-policy", + "version": "3.0.0-beta.8", + "description": "ZenStack Policy Plugin", + "type": "module", + "scripts": { + "build": "tsc --noEmit && tsup-node", + "watch": "tsup-node --watch", + "lint": "eslint src --ext ts", + "pack": "pnpm pack" + }, + "keywords": [], + "author": "ZenStack Team", + "license": "MIT", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": { + "import": "./package.json", + "require": "./package.json" + } + }, + "dependencies": { + "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/runtime": "workspace:*", + "ts-pattern": "catalog:" + }, + "peerDependencies": { + "kysely": "catalog:" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/pg": "^8.0.0", + "@zenstackhq/eslint-config": "workspace:*", + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*" + } +} diff --git a/packages/runtime/src/plugins/policy/column-collector.ts b/packages/plugins/policy/src/column-collector.ts similarity index 85% rename from packages/runtime/src/plugins/policy/column-collector.ts rename to packages/plugins/policy/src/column-collector.ts index 8b2a9f77..37d9df11 100644 --- a/packages/runtime/src/plugins/policy/column-collector.ts +++ b/packages/plugins/policy/src/column-collector.ts @@ -1,5 +1,5 @@ import type { ColumnNode, OperationNode } from 'kysely'; -import { DefaultOperationNodeVisitor } from '../../utils/default-operation-node-visitor'; +import { DefaultOperationNodeVisitor } from '@zenstackhq/sdk'; /** * Collects all column names from a query. diff --git a/packages/runtime/src/plugins/policy/expression-evaluator.ts b/packages/plugins/policy/src/expression-evaluator.ts similarity index 99% rename from packages/runtime/src/plugins/policy/expression-evaluator.ts rename to packages/plugins/policy/src/expression-evaluator.ts index a35530e5..330bf48f 100644 --- a/packages/runtime/src/plugins/policy/expression-evaluator.ts +++ b/packages/plugins/policy/src/expression-evaluator.ts @@ -10,7 +10,7 @@ import { type LiteralExpression, type MemberExpression, type UnaryExpression, -} from '../../schema'; +} from '@zenstackhq/runtime/schema'; type ExpressionEvaluatorContext = { auth?: any; diff --git a/packages/runtime/src/plugins/policy/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts similarity index 93% rename from packages/runtime/src/plugins/policy/expression-transformer.ts rename to packages/plugins/policy/src/expression-transformer.ts index 1eca04fa..58bcea2e 100644 --- a/packages/runtime/src/plugins/policy/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -1,4 +1,31 @@ import { invariant } from '@zenstackhq/common-helpers'; +import { + getCrudDialect, + InternalError, + QueryError, + QueryUtils, + type BaseCrudDialect, + type ClientContract, + type CRUD_EXT, +} from '@zenstackhq/runtime'; +import type { + BinaryExpression, + BinaryOperator, + BuiltinType, + FieldDef, + GetModels, + LiteralExpression, + MemberExpression, + UnaryExpression, +} from '@zenstackhq/runtime/schema'; +import { + ExpressionUtils, + type ArrayExpression, + type CallExpression, + type Expression, + type FieldExpression, + type SchemaDef, +} from '@zenstackhq/runtime/schema'; import { AliasNode, BinaryOperationNode, @@ -20,35 +47,6 @@ import { type OperationNode, } from 'kysely'; import { match } from 'ts-pattern'; -import type { ClientContract, CRUD_EXT } from '../../client/contract'; -import { getCrudDialect } from '../../client/crud/dialects'; -import type { BaseCrudDialect } from '../../client/crud/dialects/base-dialect'; -import { InternalError, QueryError } from '../../client/errors'; -import { - getManyToManyRelation, - getModel, - getRelationForeignKeyFieldPairs, - requireField, - requireIdFields, -} from '../../client/query-utils'; -import type { - BinaryExpression, - BinaryOperator, - BuiltinType, - FieldDef, - GetModels, - LiteralExpression, - MemberExpression, - UnaryExpression, -} from '../../schema'; -import { - ExpressionUtils, - type ArrayExpression, - type CallExpression, - type Expression, - type FieldExpression, - type SchemaDef, -} from '../../schema'; import { ExpressionEvaluator } from './expression-evaluator'; import { conjunction, disjunction, falseNode, isBeforeInvocation, logicalNot, trueNode } from './utils'; @@ -124,7 +122,7 @@ export class ExpressionTransformer { @expr('field') private _field(expr: FieldExpression, context: ExpressionTransformerContext) { - const fieldDef = requireField(this.schema, context.model, expr.field); + const fieldDef = QueryUtils.requireField(this.schema, context.model, expr.field); if (!fieldDef.relation) { return this.createColumnRef(expr.field, context); } else { @@ -226,7 +224,7 @@ export class ExpressionTransformer { invariant(ExpressionUtils.isNull(expr.right), 'only null comparison is supported for relation field'); const leftRelDef = this.getFieldDefFromFieldRef(expr.left, context.model); invariant(leftRelDef, 'failed to get relation field definition'); - const idFields = requireIdFields(this.schema, leftRelDef.type); + const idFields = QueryUtils.requireIdFields(this.schema, leftRelDef.type); normalizedLeft = this.makeOrAppendMember(normalizedLeft, idFields[0]!); } let normalizedRight: Expression = expr.right; @@ -234,7 +232,7 @@ export class ExpressionTransformer { invariant(ExpressionUtils.isNull(expr.left), 'only null comparison is supported for relation field'); const rightRelDef = this.getFieldDefFromFieldRef(expr.right, context.model); invariant(rightRelDef, 'failed to get relation field definition'); - const idFields = requireIdFields(this.schema, rightRelDef.type); + const idFields = QueryUtils.requireIdFields(this.schema, rightRelDef.type); normalizedRight = this.makeOrAppendMember(normalizedRight, idFields[0]!); } return { normalizedLeft, normalizedRight }; @@ -265,10 +263,10 @@ export class ExpressionTransformer { ExpressionUtils.isMember(expr.left) && ExpressionUtils.isField(expr.left.receiver), 'left operand must be member access with field receiver', ); - const fieldDef = requireField(this.schema, context.model, expr.left.receiver.field); + const fieldDef = QueryUtils.requireField(this.schema, context.model, expr.left.receiver.field); newContextModel = fieldDef.type; for (const member of expr.left.members) { - const memberDef = requireField(this.schema, newContextModel, member); + const memberDef = QueryUtils.requireField(this.schema, newContextModel, member); newContextModel = memberDef.type; } } @@ -318,7 +316,7 @@ export class ExpressionTransformer { if (ExpressionUtils.isNull(other)) { return this.transformValue(expr.op === '==' ? !this.auth : !!this.auth, 'Boolean'); } else { - const authModel = getModel(this.schema, this.authType); + const authModel = QueryUtils.getModel(this.schema, this.authType); if (!authModel) { throw new QueryError( `Unsupported use of \`auth()\` in policy of model "${context.model}", comparing with \`auth()\` is only possible when auth type is a model`, @@ -481,7 +479,7 @@ export class ExpressionTransformer { return this._field(ExpressionUtils.field(expr.members[0]!), context); } else { // transform the first segment into a relation access, then continue with the rest of the members - const firstMemberFieldDef = requireField(this.schema, context.model, expr.members[0]!); + const firstMemberFieldDef = QueryUtils.requireField(this.schema, context.model, expr.members[0]!); receiver = this.transformRelationAccess(expr.members[0]!, firstMemberFieldDef.type, restContext); members = expr.members.slice(1); } @@ -493,7 +491,7 @@ export class ExpressionTransformer { let startType: string; if (ExpressionUtils.isField(expr.receiver)) { - const receiverField = requireField(this.schema, context.model, expr.receiver.field); + const receiverField = QueryUtils.requireField(this.schema, context.model, expr.receiver.field); startType = receiverField.type; } else { // "this." case, start type is the model of the context @@ -504,7 +502,7 @@ export class ExpressionTransformer { const memberFields: { fromModel: string; fieldDef: FieldDef }[] = []; let currType = startType; for (const member of members) { - const fieldDef = requireField(this.schema, currType, member); + const fieldDef = QueryUtils.requireField(this.schema, currType, member); memberFields.push({ fieldDef, fromModel: currType }); currType = fieldDef.type; } @@ -561,7 +559,7 @@ export class ExpressionTransformer { } const field = expr.members[0]!; - const fieldDef = requireField(this.schema, receiverType, field); + const fieldDef = QueryUtils.requireField(this.schema, receiverType, field); const fieldValue = receiver[field] ?? null; return this.transformValue(fieldValue, fieldDef.type as BuiltinType); } @@ -571,13 +569,13 @@ export class ExpressionTransformer { relationModel: string, context: ExpressionTransformerContext, ): SelectQueryNode { - const m2m = getManyToManyRelation(this.schema, context.model, field); + const m2m = QueryUtils.getManyToManyRelation(this.schema, context.model, field); if (m2m) { return this.transformManyToManyRelationAccess(m2m, context); } const fromModel = context.model; - const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs(this.schema, fromModel, field); + const { keyPairs, ownedByModel } = QueryUtils.getRelationForeignKeyFieldPairs(this.schema, fromModel, field); let condition: OperationNode; if (ownedByModel) { @@ -614,7 +612,7 @@ export class ExpressionTransformer { } private transformManyToManyRelationAccess( - m2m: NonNullable>, + m2m: NonNullable>, context: ExpressionTransformerContext, ) { const eb = expressionBuilder(); @@ -672,13 +670,13 @@ export class ExpressionTransformer { private getFieldDefFromFieldRef(expr: Expression, model: GetModels): FieldDef | undefined { if (ExpressionUtils.isField(expr)) { - return requireField(this.schema, model, expr.field); + return QueryUtils.requireField(this.schema, model, expr.field); } else if ( ExpressionUtils.isMember(expr) && expr.members.length === 1 && ExpressionUtils.isThis(expr.receiver) ) { - return requireField(this.schema, model, expr.members[0]!); + return QueryUtils.requireField(this.schema, model, expr.members[0]!); } else { return undefined; } diff --git a/packages/runtime/src/plugins/policy/functions.ts b/packages/plugins/policy/src/functions.ts similarity index 82% rename from packages/runtime/src/plugins/policy/functions.ts rename to packages/plugins/policy/src/functions.ts index c7fa09d7..5d6d6621 100644 --- a/packages/runtime/src/plugins/policy/functions.ts +++ b/packages/plugins/policy/src/functions.ts @@ -1,9 +1,7 @@ import { invariant } from '@zenstackhq/common-helpers'; +import type { ZModelFunction, ZModelFunctionContext } from '@zenstackhq/runtime'; +import { CRUD, QueryUtils } from '@zenstackhq/runtime'; import { ExpressionWrapper, ValueNode, type Expression, type ExpressionBuilder } from 'kysely'; -import { CRUD } from '../../client/contract'; -import { extractFieldName } from '../../client/kysely-utils'; -import type { ZModelFunction, ZModelFunctionContext } from '../../client/options'; -import { buildJoinPairs, requireField } from '../../client/query-utils'; import { PolicyHandler } from './policy-handler'; /** @@ -31,9 +29,9 @@ export const check: ZModelFunction = ( } // first argument must be a field reference - const fieldName = extractFieldName(arg1Node); + const fieldName = QueryUtils.extractFieldName(arg1Node); invariant(fieldName, 'Failed to extract field name from the first argument of "check" function'); - const fieldDef = requireField(client.$schema, model, fieldName); + const fieldDef = QueryUtils.requireField(client.$schema, model, fieldName); invariant(fieldDef.relation, `Field "${fieldName}" is not a relation field in model "${model}"`); invariant(!fieldDef.array, `Field "${fieldName}" is a to-many relation, which is not supported by "check"`); const relationModel = fieldDef.type; @@ -43,7 +41,7 @@ export const check: ZModelFunction = ( const policyHandler = new PolicyHandler(client); // join with parent model - const joinPairs = buildJoinPairs(client.$schema, model, modelAlias, fieldName, relationModel); + const joinPairs = QueryUtils.buildJoinPairs(client.$schema, model, modelAlias, fieldName, relationModel); const joinCondition = joinPairs.length === 1 ? eb(eb.ref(joinPairs[0]![0]), '=', eb.ref(joinPairs[0]![1])) diff --git a/packages/runtime/src/plugins/policy/index.ts b/packages/plugins/policy/src/index.ts similarity index 50% rename from packages/runtime/src/plugins/policy/index.ts rename to packages/plugins/policy/src/index.ts index 9958cffb..1110b645 100644 --- a/packages/runtime/src/plugins/policy/index.ts +++ b/packages/plugins/policy/src/index.ts @@ -1,2 +1 @@ -export * from './errors'; export * from './plugin'; diff --git a/packages/runtime/src/plugins/policy/plugin.ts b/packages/plugins/policy/src/plugin.ts similarity index 81% rename from packages/runtime/src/plugins/policy/plugin.ts rename to packages/plugins/policy/src/plugin.ts index 7ebd2882..bbb3d0a7 100644 --- a/packages/runtime/src/plugins/policy/plugin.ts +++ b/packages/plugins/policy/src/plugin.ts @@ -1,5 +1,5 @@ -import { type OnKyselyQueryArgs, type RuntimePlugin } from '../../client/plugin'; -import type { SchemaDef } from '../../schema'; +import { type OnKyselyQueryArgs, type RuntimePlugin } from '@zenstackhq/runtime'; +import type { SchemaDef } from '@zenstackhq/runtime/schema'; import { check } from './functions'; import { PolicyHandler } from './policy-handler'; diff --git a/packages/runtime/src/plugins/policy/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts similarity index 94% rename from packages/runtime/src/plugins/policy/policy-handler.ts rename to packages/plugins/policy/src/policy-handler.ts index 49e5afd1..f6daf04d 100644 --- a/packages/runtime/src/plugins/policy/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -1,4 +1,23 @@ import { invariant } from '@zenstackhq/common-helpers'; +import type { BaseCrudDialect, ClientContract, ProceedKyselyQueryFunction } from '@zenstackhq/runtime'; +import { + getCrudDialect, + InternalError, + QueryError, + QueryUtils, + RejectedByPolicyError, + RejectedByPolicyReason, + type CRUD_EXT, +} from '@zenstackhq/runtime'; +import { + ExpressionUtils, + type BuiltinType, + type Expression, + type GetModels, + type MemberExpression, + type SchemaDef, +} from '@zenstackhq/runtime/schema'; +import { ExpressionVisitor } from '@zenstackhq/sdk'; import { AliasNode, BinaryOperationNode, @@ -33,24 +52,7 @@ import { type RootOperationNode, } from 'kysely'; import { match } from 'ts-pattern'; -import type { ClientContract } from '../../client'; -import { type CRUD_EXT } from '../../client/contract'; -import { getCrudDialect } from '../../client/crud/dialects'; -import type { BaseCrudDialect } from '../../client/crud/dialects/base-dialect'; -import { InternalError, QueryError } from '../../client/errors'; -import type { ProceedKyselyQueryFunction } from '../../client/plugin'; -import { getManyToManyRelation, requireField, requireIdFields, requireModel } from '../../client/query-utils'; -import { - ExpressionUtils, - type BuiltinType, - type Expression, - type GetModels, - type MemberExpression, - type SchemaDef, -} from '../../schema'; -import { ExpressionVisitor } from '../../utils/expression-utils'; import { ColumnCollector } from './column-collector'; -import { RejectedByPolicyError, RejectedByPolicyReason } from './errors'; import { ExpressionTransformer } from './expression-transformer'; import type { Policy, PolicyOperation } from './types'; import { buildIsFalse, conjunction, disjunction, falseNode, getTableName, isBeforeInvocation, trueNode } from './utils'; @@ -149,7 +151,7 @@ export class PolicyHandler extends OperationNodeTransf ), ]), selections: beforeUpdateInfo.fields.map((name, index) => { - const def = requireField(this.client.$schema, mutationModel, name); + const def = QueryUtils.requireField(this.client.$schema, mutationModel, name); const castedColumnRef = sql`CAST(${eb.ref(`column${index + 1}`)} as ${sql.raw(this.dialect.getFieldSqlType(def))})`.as( name, @@ -167,7 +169,7 @@ export class PolicyHandler extends OperationNodeTransf qb.leftJoin( () => new ExpressionWrapper(beforeUpdateTable!).as('$before'), (join) => { - const idFields = requireIdFields(this.client.$schema, mutationModel); + const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); return idFields.reduce( (acc, f) => acc.onRef(`${mutationModel}.${f}`, '=', `$before.${f}`), join, @@ -268,7 +270,7 @@ export class PolicyHandler extends OperationNodeTransf } // make sure id fields are included - requireIdFields(this.client.$schema, model).forEach((f) => fields.add(f)); + QueryUtils.requireIdFields(this.client.$schema, model).forEach((f) => fields.add(f)); return Array.from(fields).sort(); } @@ -350,7 +352,7 @@ export class PolicyHandler extends OperationNodeTransf let returning = result.returning; if (returning) { const { mutationModel } = this.getMutationModel(node); - const idFields = requireIdFields(this.client.$schema, mutationModel); + const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); returning = ReturningNode.create(idFields.map((f) => SelectionNode.create(ColumnNode.create(f)))); } @@ -382,7 +384,7 @@ export class PolicyHandler extends OperationNodeTransf // before-update rows if (returning || this.hasPostUpdatePolicies(mutationModel)) { - const idFields = requireIdFields(this.client.$schema, mutationModel); + const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); returning = ReturningNode.create(idFields.map((f) => SelectionNode.create(ColumnNode.create(f)))); } @@ -421,10 +423,10 @@ export class PolicyHandler extends OperationNodeTransf return true; } const { mutationModel } = this.getMutationModel(node); - const idFields = requireIdFields(this.client.$schema, mutationModel); + const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); if (node.returning.selections.some((s) => SelectAllNode.is(s.selection))) { - const modelDef = requireModel(this.client.$schema, mutationModel); + const modelDef = QueryUtils.requireModel(this.client.$schema, mutationModel); if (Object.keys(modelDef.fields).some((f) => !idFields.includes(f))) { // there are fields other than ID fields return false; @@ -538,7 +540,7 @@ export class PolicyHandler extends OperationNodeTransf values: OperationNode[], proceed: ProceedKyselyQueryFunction, ) { - const allFields = Object.entries(requireModel(this.client.$schema, model).fields).filter( + const allFields = Object.entries(QueryUtils.requireModel(this.client.$schema, model).fields).filter( ([, def]) => !def.relation, ); const allValues: OperationNode[] = []; @@ -625,7 +627,7 @@ export class PolicyHandler extends OperationNodeTransf for (let i = 0; i < data.length; i++) { const item = data[i]!; if (typeof item === 'object' && item && 'kind' in item) { - const fieldDef = requireField(this.client.$schema, model, fields[i]!); + const fieldDef = QueryUtils.requireField(this.client.$schema, model, fields[i]!); invariant(item.kind === 'ValueNode', 'expecting a ValueNode'); result.push({ node: ValueNode.create( @@ -644,7 +646,7 @@ export class PolicyHandler extends OperationNodeTransf // but there's no need to transform values anyway because they're the fields // are all foreign keys if (!isImplicitManyToManyJoinTable) { - const fieldDef = requireField(this.client.$schema, model, fields[i]!); + const fieldDef = QueryUtils.requireField(this.client.$schema, model, fields[i]!); value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType, !!fieldDef.array); } if (Array.isArray(value)) { @@ -710,7 +712,7 @@ export class PolicyHandler extends OperationNodeTransf } private buildIdConditions(table: string, rows: any[]): OperationNode { - const idFields = requireIdFields(this.client.$schema, table); + const idFields = QueryUtils.requireIdFields(this.client.$schema, table); return disjunction( this.dialect, rows.map((row) => @@ -865,7 +867,7 @@ export class PolicyHandler extends OperationNodeTransf } private getModelPolicies(model: string, operation: PolicyOperation) { - const modelDef = requireModel(this.client.$schema, model); + const modelDef = QueryUtils.requireModel(this.client.$schema, model); const result: Policy[] = []; const extractOperations = (expr: Expression) => { @@ -902,7 +904,7 @@ export class PolicyHandler extends OperationNodeTransf private resolveManyToManyJoinTable(tableName: string) { for (const model of Object.values(this.client.$schema.models)) { for (const field of Object.values(model.fields)) { - const m2m = getManyToManyRelation(this.client.$schema, model.name, field.name); + const m2m = QueryUtils.getManyToManyRelation(this.client.$schema, model.name, field.name); if (m2m?.joinTable === tableName) { const sortedRecord = [ { @@ -915,8 +917,8 @@ export class PolicyHandler extends OperationNodeTransf }, ].sort(this.manyToManySorter); - const firstIdFields = requireIdFields(this.client.$schema, sortedRecord[0]!.model); - const secondIdFields = requireIdFields(this.client.$schema, sortedRecord[1]!.model); + const firstIdFields = QueryUtils.requireIdFields(this.client.$schema, sortedRecord[0]!.model); + const secondIdFields = QueryUtils.requireIdFields(this.client.$schema, sortedRecord[1]!.model); invariant( firstIdFields.length === 1 && secondIdFields.length === 1, 'only single-field id is supported for implicit many-to-many join table', diff --git a/packages/runtime/src/plugins/policy/types.ts b/packages/plugins/policy/src/types.ts similarity index 73% rename from packages/runtime/src/plugins/policy/types.ts rename to packages/plugins/policy/src/types.ts index 74c49d85..f4c12e13 100644 --- a/packages/runtime/src/plugins/policy/types.ts +++ b/packages/plugins/policy/src/types.ts @@ -1,5 +1,5 @@ -import type { CRUD_EXT } from '../../client/contract'; -import type { Expression } from '../../schema'; +import type { CRUD_EXT } from '@zenstackhq/runtime'; +import type { Expression } from '@zenstackhq/runtime/schema'; /** * Access policy kind. diff --git a/packages/runtime/src/plugins/policy/utils.ts b/packages/plugins/policy/src/utils.ts similarity index 97% rename from packages/runtime/src/plugins/policy/utils.ts rename to packages/plugins/policy/src/utils.ts index 5fc11410..8a62458f 100644 --- a/packages/runtime/src/plugins/policy/utils.ts +++ b/packages/plugins/policy/src/utils.ts @@ -1,3 +1,5 @@ +import type { BaseCrudDialect } from '@zenstackhq/runtime'; +import { ExpressionUtils, type Expression, type SchemaDef } from '@zenstackhq/runtime/schema'; import type { OperationNode } from 'kysely'; import { AliasNode, @@ -12,8 +14,6 @@ import { UnaryOperationNode, ValueNode, } from 'kysely'; -import type { BaseCrudDialect } from '../../client/crud/dialects/base-dialect'; -import { ExpressionUtils, type Expression, type SchemaDef } from '../../schema'; /** * Creates a `true` value node. diff --git a/packages/runtime/tsconfig.build.json b/packages/plugins/policy/tsconfig.json similarity index 62% rename from packages/runtime/tsconfig.build.json rename to packages/plugins/policy/tsconfig.json index aacb3723..41472d08 100644 --- a/packages/runtime/tsconfig.build.json +++ b/packages/plugins/policy/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "rootDir": "." - }, "include": ["src/**/*"] } diff --git a/packages/plugins/policy/tsup.config.ts b/packages/plugins/policy/tsup.config.ts new file mode 100644 index 00000000..4b7a3428 --- /dev/null +++ b/packages/plugins/policy/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + outDir: 'dist', + splitting: false, + sourcemap: true, + dts: true, + format: ['cjs', 'esm'], +}); diff --git a/packages/plugins/policy/vitest.config.ts b/packages/plugins/policy/vitest.config.ts new file mode 100644 index 00000000..75a9f709 --- /dev/null +++ b/packages/plugins/policy/vitest.config.ts @@ -0,0 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +export default mergeConfig(base, defineConfig({})); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 9e563efa..52af6379 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -4,14 +4,9 @@ "description": "ZenStack Runtime", "type": "module", "scripts": { - "build": "tsc --project tsconfig.build.json --noEmit && tsup-node && pnpm test:generate", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", - "test": "vitest run && pnpm test:typecheck", - "test:sqlite": "TEST_DB_PROVIDER=sqlite vitest run", - "test:postgresql": "TEST_DB_PROVIDER=postgresql vitest run", - "test:generate": "tsx test/scripts/generate.ts", - "test:typecheck": "tsc --project tsconfig.test.json", "pack": "pnpm pack" }, "keywords": [], @@ -79,9 +74,9 @@ "zod-validation-error": "catalog:" }, "peerDependencies": { - "better-sqlite3": "^12.2.0", + "better-sqlite3": "catalog:", "kysely": "catalog:", - "pg": "^8.13.1", + "pg": "catalog:", "zod": "catalog:" }, "peerDependenciesMeta": { @@ -99,7 +94,6 @@ "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", - "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", "tsx": "^4.19.2", diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 7a4c9a66..34924952 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -14,7 +14,6 @@ import { match } from 'ts-pattern'; import { ulid } from 'ulid'; import * as uuid from 'uuid'; import type { ClientContract } from '../..'; -import { PolicyPlugin } from '../../../plugins/policy'; import type { BuiltinType, Expression, FieldDef } from '../../../schema'; import { ExpressionUtils, type GetModels, type ModelDef, type SchemaDef } from '../../../schema'; import { clone } from '../../../utils/clone'; @@ -108,7 +107,7 @@ export abstract class BaseOperationHandler { // TODO: this is not clean, needs a better solution protected get hasPolicyEnabled() { - return this.options.plugins?.some((plugin) => plugin instanceof PolicyPlugin); + return this.options.plugins?.some((plugin) => plugin.constructor.name === 'PolicyPlugin'); } protected requireModel(model: string) { diff --git a/packages/runtime/src/client/crud/operations/create.ts b/packages/runtime/src/client/crud/operations/create.ts index 26206d99..36e76211 100644 --- a/packages/runtime/src/client/crud/operations/create.ts +++ b/packages/runtime/src/client/crud/operations/create.ts @@ -1,7 +1,7 @@ import { match } from 'ts-pattern'; -import { RejectedByPolicyError, RejectedByPolicyReason } from '../../../plugins/policy/errors'; import type { GetModels, SchemaDef } from '../../../schema'; import type { CreateArgs, CreateManyAndReturnArgs, CreateManyArgs, WhereInput } from '../../crud-types'; +import { RejectedByPolicyError, RejectedByPolicyReason } from '../../errors'; import { getIdValues } from '../../query-utils'; import { BaseOperationHandler } from './base'; diff --git a/packages/runtime/src/client/crud/operations/delete.ts b/packages/runtime/src/client/crud/operations/delete.ts index 6eb1eca3..21539aed 100644 --- a/packages/runtime/src/client/crud/operations/delete.ts +++ b/packages/runtime/src/client/crud/operations/delete.ts @@ -1,9 +1,8 @@ import { match } from 'ts-pattern'; import type { SchemaDef } from '../../../schema'; import type { DeleteArgs, DeleteManyArgs } from '../../crud-types'; -import { NotFoundError } from '../../errors'; +import { NotFoundError, RejectedByPolicyError, RejectedByPolicyReason } from '../../errors'; import { BaseOperationHandler } from './base'; -import { RejectedByPolicyError, RejectedByPolicyReason } from '../../../plugins/policy'; export class DeleteOperationHandler extends BaseOperationHandler { async handle(operation: 'delete' | 'deleteMany', args: unknown | undefined) { diff --git a/packages/runtime/src/client/crud/operations/update.ts b/packages/runtime/src/client/crud/operations/update.ts index ad2fc613..567721b0 100644 --- a/packages/runtime/src/client/crud/operations/update.ts +++ b/packages/runtime/src/client/crud/operations/update.ts @@ -1,7 +1,7 @@ import { match } from 'ts-pattern'; -import { RejectedByPolicyError, RejectedByPolicyReason } from '../../../plugins/policy/errors'; import type { GetModels, SchemaDef } from '../../../schema'; import type { UpdateArgs, UpdateManyAndReturnArgs, UpdateManyArgs, UpsertArgs, WhereInput } from '../../crud-types'; +import { RejectedByPolicyError, RejectedByPolicyReason } from '../../errors'; import { getIdValues } from '../../query-utils'; import { BaseOperationHandler } from './base'; diff --git a/packages/runtime/src/client/errors.ts b/packages/runtime/src/client/errors.ts index 15961811..c1be626a 100644 --- a/packages/runtime/src/client/errors.ts +++ b/packages/runtime/src/client/errors.ts @@ -34,3 +34,36 @@ export class NotFoundError extends ZenStackError { super(`Entity not found for model "${model}"${details ? `: ${details}` : ''}`); } } + +/** + * Reason code for policy rejection. + */ +export enum RejectedByPolicyReason { + /** + * Rejected because the operation is not allowed by policy. + */ + NO_ACCESS = 'no-access', + + /** + * Rejected because the result cannot be read back after mutation due to policy. + */ + CANNOT_READ_BACK = 'cannot-read-back', + + /** + * Other reasons. + */ + OTHER = 'other', +} + +/** + * Error thrown when an operation is rejected by access policy. + */ +export class RejectedByPolicyError extends ZenStackError { + constructor( + public readonly model: string | undefined, + public readonly reason: RejectedByPolicyReason = RejectedByPolicyReason.NO_ACCESS, + message?: string, + ) { + super(message ?? `Operation rejected by policy${model ? ': ' + model : ''}`); + } +} diff --git a/packages/runtime/src/client/executor/name-mapper.ts b/packages/runtime/src/client/executor/name-mapper.ts index 83ef8a33..410aa7b7 100644 --- a/packages/runtime/src/client/executor/name-mapper.ts +++ b/packages/runtime/src/client/executor/name-mapper.ts @@ -17,8 +17,7 @@ import { type OperationNode, } from 'kysely'; import type { FieldDef, ModelDef, SchemaDef } from '../../schema'; -import { extractFieldName, extractModelName, stripAlias } from '../kysely-utils'; -import { getModel, requireModel } from '../query-utils'; +import { extractFieldName, extractModelName, getModel, requireModel, stripAlias } from '../query-utils'; type Scope = { model?: string; diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index c307bc4e..f3e855fa 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -26,8 +26,8 @@ import type { GetModels, SchemaDef } from '../../schema'; import { type ClientImpl } from '../client-impl'; import { TransactionIsolationLevel, type ClientContract } from '../contract'; import { InternalError, QueryError, ZenStackError } from '../errors'; -import { stripAlias } from '../kysely-utils'; import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; +import { stripAlias } from '../query-utils'; import { QueryNameMapper } from './name-mapper'; import type { ZenStackDriver } from './zenstack-driver'; diff --git a/packages/runtime/src/client/index.ts b/packages/runtime/src/client/index.ts index aaf50754..225aeba5 100644 --- a/packages/runtime/src/client/index.ts +++ b/packages/runtime/src/client/index.ts @@ -1,9 +1,11 @@ export { ZenStackClient } from './client-impl'; -export type { ClientConstructor, ClientContract } from './contract'; +export * from './contract'; export type * from './crud-types'; +export { getCrudDialect } from './crud/dialects'; +export { BaseCrudDialect } from './crud/dialects/base-dialect'; export * from './errors'; -export type { ClientOptions } from './options'; -export { definePlugin } from './plugin'; +export * from './options'; +export * from './plugin'; export type { ZenStackPromise } from './promise'; export type { ToKysely } from './query-builder'; -export { sql } from 'kysely'; +export * as QueryUtils from './query-utils'; diff --git a/packages/runtime/src/client/kysely-utils.ts b/packages/runtime/src/client/kysely-utils.ts deleted file mode 100644 index a46464c3..00000000 --- a/packages/runtime/src/client/kysely-utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { type OperationNode, AliasNode, ColumnNode, ReferenceNode, TableNode } from 'kysely'; - -/** - * Strips alias from the node if it exists. - */ -export function stripAlias(node: OperationNode) { - if (AliasNode.is(node)) { - return { alias: node.alias, node: node.node }; - } else { - return { alias: undefined, node }; - } -} - -/** - * Extracts model name from an OperationNode. - */ -export function extractModelName(node: OperationNode) { - const { node: innerNode } = stripAlias(node); - return TableNode.is(innerNode!) ? innerNode!.table.identifier.name : undefined; -} - -/** - * Extracts field name from an OperationNode. - */ -export function extractFieldName(node: OperationNode) { - if (ReferenceNode.is(node) && ColumnNode.is(node.column)) { - return node.column.column.name; - } else if (ColumnNode.is(node)) { - return node.column.name; - } else { - return undefined; - } -} diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index 1cfbdd14..869d3535 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -1,5 +1,14 @@ import { invariant } from '@zenstackhq/common-helpers'; -import type { Expression, ExpressionBuilder, ExpressionWrapper } from 'kysely'; +import { + AliasNode, + ColumnNode, + ReferenceNode, + TableNode, + type Expression, + type ExpressionBuilder, + type ExpressionWrapper, + type OperationNode, +} from 'kysely'; import { match } from 'ts-pattern'; import { ExpressionUtils, type FieldDef, type GetModels, type ModelDef, type SchemaDef } from '../schema'; import { extractFields } from '../utils/object-utils'; @@ -367,3 +376,35 @@ export function aggregate(eb: ExpressionBuilder, expr: Expression .with('_max', () => eb.fn.max(expr)) .exhaustive(); } + +/** + * Strips alias from the node if it exists. + */ +export function stripAlias(node: OperationNode) { + if (AliasNode.is(node)) { + return { alias: node.alias, node: node.node }; + } else { + return { alias: undefined, node }; + } +} + +/** + * Extracts model name from an OperationNode. + */ +export function extractModelName(node: OperationNode) { + const { node: innerNode } = stripAlias(node); + return TableNode.is(innerNode!) ? innerNode!.table.identifier.name : undefined; +} + +/** + * Extracts field name from an OperationNode. + */ +export function extractFieldName(node: OperationNode) { + if (ReferenceNode.is(node) && ColumnNode.is(node.column)) { + return node.column.column.name; + } else if (ColumnNode.is(node)) { + return node.column.name; + } else { + return undefined; + } +} diff --git a/packages/runtime/src/plugins/policy/errors.ts b/packages/runtime/src/plugins/policy/errors.ts deleted file mode 100644 index 42d57b18..00000000 --- a/packages/runtime/src/plugins/policy/errors.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ZenStackError } from '../../client'; - -/** - * Reason code for policy rejection. - */ -export enum RejectedByPolicyReason { - /** - * Rejected because the operation is not allowed by policy. - */ - NO_ACCESS = 'no-access', - - /** - * Rejected because the result cannot be read back after mutation due to policy. - */ - CANNOT_READ_BACK = 'cannot-read-back', - - /** - * Other reasons. - */ - OTHER = 'other', -} - -/** - * Error thrown when an operation is rejected by access policy. - */ -export class RejectedByPolicyError extends ZenStackError { - constructor( - public readonly model: string | undefined, - public readonly reason: RejectedByPolicyReason = RejectedByPolicyReason.NO_ACCESS, - message?: string, - ) { - super(message ?? `Operation rejected by policy${model ? ': ' + model : ''}`); - } -} diff --git a/packages/runtime/src/schema/index.ts b/packages/runtime/src/schema/index.ts index 4f98939e..10a69276 100644 --- a/packages/runtime/src/schema/index.ts +++ b/packages/runtime/src/schema/index.ts @@ -1,3 +1,4 @@ export type * from '@zenstackhq/sdk/schema'; export type { OperandExpression } from 'kysely'; +export * from './auth'; export * from './expression'; diff --git a/packages/runtime/test/policy/utils.ts b/packages/runtime/test/policy/utils.ts deleted file mode 100644 index 30b99577..00000000 --- a/packages/runtime/test/policy/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ClientContract } from '../../src'; -import { PolicyPlugin } from '../../src/plugins/policy'; -import type { SchemaDef } from '../../src/schema'; -import { createTestClient, type CreateTestClientOptions } from '../utils'; - -export async function createPolicyTestClient( - schema: Schema, - options?: CreateTestClientOptions, -): Promise>; -export async function createPolicyTestClient( - schema: string, - options?: CreateTestClientOptions, -): Promise; -export async function createPolicyTestClient( - schema: Schema | string, - options?: CreateTestClientOptions, -): Promise { - return createTestClient( - schema as any, - { - ...options, - plugins: [new PolicyPlugin()], - } as any, - ); -} diff --git a/packages/runtime/tsconfig.json b/packages/runtime/tsconfig.json index 6056fb01..aacb3723 100644 --- a/packages/runtime/tsconfig.json +++ b/packages/runtime/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "rootDir": "." }, - "include": ["src/**/*", "test/**/*"] + "include": ["src/**/*"] } diff --git a/packages/runtime/tsup.config.ts b/packages/runtime/tsup.config.ts index 7f7a9348..b5acbfc2 100644 --- a/packages/runtime/tsup.config.ts +++ b/packages/runtime/tsup.config.ts @@ -5,7 +5,6 @@ export default defineConfig({ index: 'src/index.ts', schema: 'src/schema/index.ts', helpers: 'src/helpers.ts', - 'plugins/policy/index': 'src/plugins/policy/index.ts', }, outDir: 'dist', splitting: false, diff --git a/packages/runtime/vitest.config.ts b/packages/runtime/vitest.config.ts index ecf30fd7..75a9f709 100644 --- a/packages/runtime/vitest.config.ts +++ b/packages/runtime/vitest.config.ts @@ -1,12 +1,4 @@ import base from '@zenstackhq/vitest-config/base'; -import path from 'node:path'; import { defineConfig, mergeConfig } from 'vitest/config'; -export default mergeConfig( - base, - defineConfig({ - test: { - setupFiles: [path.resolve(__dirname, './test/vitest-ext.ts')], - }, - }), -); +export default mergeConfig(base, defineConfig({})); diff --git a/packages/runtime/src/utils/default-operation-node-visitor.ts b/packages/sdk/src/default-operation-node-visitor.ts similarity index 100% rename from packages/runtime/src/utils/default-operation-node-visitor.ts rename to packages/sdk/src/default-operation-node-visitor.ts diff --git a/packages/runtime/src/utils/expression-utils.ts b/packages/sdk/src/expression-utils.ts similarity index 98% rename from packages/runtime/src/utils/expression-utils.ts rename to packages/sdk/src/expression-utils.ts index 8c0824d4..ec423767 100644 --- a/packages/runtime/src/utils/expression-utils.ts +++ b/packages/sdk/src/expression-utils.ts @@ -10,7 +10,7 @@ import type { NullExpression, ThisExpression, UnaryExpression, -} from '../schema'; +} from './schema'; export class ExpressionVisitor { visit(expr: Expression): void { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 649a7201..c74b1419 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,5 +1,7 @@ import * as ModelUtils from './model-utils'; export * from './cli-plugin'; +export * from './expression-utils'; +export * from './default-operation-node-visitor'; export { PrismaSchemaGenerator } from './prisma/prisma-schema-generator'; export * from './ts-schema-generator'; export * from './zmodel-code-generator'; diff --git a/packages/testtools/package.json b/packages/testtools/package.json index be36abc2..d34a1e4c 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -4,7 +4,7 @@ "description": "ZenStack Test Tools", "type": "module", "scripts": { - "build": "tsc --noEmit && tsup-node", + "build": "tsc --noEmit && tsup-node && copyfiles -f ./src/types.d.ts ./dist", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" @@ -25,25 +25,35 @@ "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } + }, + "./types": { + "types": "./dist/types.d.ts" } }, "dependencies": { + "@zenstackhq/common-helpers": "workspace:*", "@zenstackhq/language": "workspace:*", + "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/plugin-policy": "workspace:*", "glob": "^11.0.2", - "tmp": "catalog:", - "ts-pattern": "catalog:", + "kysely": "catalog:", "prisma": "catalog:", - "typescript": "catalog:" + "tmp": "catalog:", + "ts-pattern": "catalog:" }, "peerDependencies": { - "better-sqlite3": "^12.2.0", - "pg": "^8.13.1" + "better-sqlite3": "catalog:", + "pg": "catalog:" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "catalog:", "@types/tmp": "catalog:", + "@types/pg": "^8.11.11", "@zenstackhq/eslint-config": "workspace:*", - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "copyfiles": "^2.4.1", + "typescript": "catalog:" } } diff --git a/packages/runtime/test/utils.ts b/packages/testtools/src/client.ts similarity index 86% rename from packages/runtime/test/utils.ts rename to packages/testtools/src/client.ts index d5bea549..fcb1b1ec 100644 --- a/packages/runtime/test/utils.ts +++ b/packages/testtools/src/client.ts @@ -1,8 +1,10 @@ import { invariant } from '@zenstackhq/common-helpers'; import { loadDocument } from '@zenstackhq/language'; import type { Model } from '@zenstackhq/language/ast'; +import { PolicyPlugin } from '@zenstackhq/plugin-policy'; +import { ZenStackClient, type ClientContract, type ClientOptions } from '@zenstackhq/runtime'; +import type { SchemaDef } from '@zenstackhq/runtime/schema'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; -import { createTestProject, generateTsSchema } from '@zenstackhq/testtools'; import SQLite from 'better-sqlite3'; import { PostgresDialect, SqliteDialect, type LogEvent } from 'kysely'; import { execSync } from 'node:child_process'; @@ -11,9 +13,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { Client as PGClient, Pool } from 'pg'; import { expect } from 'vitest'; -import type { ClientContract, ClientOptions } from '../src/client'; -import { ZenStackClient } from '../src/client'; -import type { SchemaDef } from '../src/schema'; +import { createTestProject } from './project'; +import { generateTsSchema } from './schema'; export function getTestDbProvider() { const val = process.env['TEST_DB_PROVIDER'] ?? 'sqlite'; @@ -165,6 +166,27 @@ export async function createTestClient( return client; } +export async function createPolicyTestClient( + schema: Schema, + options?: CreateTestClientOptions, +): Promise>; +export async function createPolicyTestClient( + schema: string, + options?: CreateTestClientOptions, +): Promise; +export async function createPolicyTestClient( + schema: Schema | string, + options?: CreateTestClientOptions, +): Promise { + return createTestClient( + schema as any, + { + ...options, + plugins: [...(options?.plugins ?? []), new PolicyPlugin()], + } as any, + ); +} + export function testLogger(e: LogEvent) { console.log(e.query.sql, e.query.parameters); } diff --git a/packages/testtools/src/index.ts b/packages/testtools/src/index.ts index dd917ab1..96ce1534 100644 --- a/packages/testtools/src/index.ts +++ b/packages/testtools/src/index.ts @@ -1,2 +1,4 @@ +export * from './client'; export * from './project'; export * from './schema'; +export * from './vitest-ext'; diff --git a/packages/runtime/test/vitest.d.ts b/packages/testtools/src/types.d.ts similarity index 100% rename from packages/runtime/test/vitest.d.ts rename to packages/testtools/src/types.d.ts diff --git a/packages/runtime/test/vitest-ext.ts b/packages/testtools/src/vitest-ext.ts similarity index 96% rename from packages/runtime/test/vitest-ext.ts rename to packages/testtools/src/vitest-ext.ts index 096b2429..70b5a61b 100644 --- a/packages/runtime/test/vitest-ext.ts +++ b/packages/testtools/src/vitest-ext.ts @@ -1,6 +1,5 @@ +import { NotFoundError, RejectedByPolicyError } from '@zenstackhq/runtime'; import { expect } from 'vitest'; -import { NotFoundError } from '../src/client/errors'; -import { RejectedByPolicyError } from '../src/plugins/policy/errors'; function isPromise(value: any) { return typeof value.then === 'function' && typeof value.catch === 'function'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6638dc1..3c85aa5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ catalogs: '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + better-sqlite3: + specifier: ^12.2.0 + version: 12.2.0 kysely: specifier: ^0.27.6 version: 0.27.6 @@ -21,6 +24,9 @@ catalogs: langium-cli: specifier: 3.5.0 version: 3.5.0 + pg: + specifier: ^8.13.1 + version: 8.16.3 prisma: specifier: ^6.10.0 version: 6.14.0 @@ -142,7 +148,7 @@ importers: specifier: workspace:* version: link:../vitest-config better-sqlite3: - specifier: ^12.2.0 + specifier: 'catalog:' version: 12.2.0 tmp: specifier: 'catalog:' @@ -264,6 +270,40 @@ importers: specifier: 'catalog:' version: 0.2.3 + packages/plugins/policy: + dependencies: + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../../common-helpers + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../../runtime + '@zenstackhq/sdk': + specifier: workspace:* + version: link:../../sdk + kysely: + specifier: 'catalog:' + version: 0.27.6 + ts-pattern: + specifier: 'catalog:' + version: 5.7.1 + devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/pg': + specifier: ^8.0.0 + version: 8.11.11 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../../eslint-config + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../../typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../../vitest-config + packages/runtime: dependencies: '@paralleldrive/cuid2': @@ -273,7 +313,7 @@ importers: specifier: workspace:* version: link:../common-helpers better-sqlite3: - specifier: ^12.2.0 + specifier: 'catalog:' version: 12.2.0 decimal.js: specifier: ^10.4.3 @@ -288,8 +328,8 @@ importers: specifier: ^5.0.9 version: 5.0.9 pg: - specifier: ^8.13.1 - version: 8.13.1 + specifier: 'catalog:' + version: 8.16.3 toposort: specifier: ^2.0.2 version: 2.0.2 @@ -324,9 +364,6 @@ importers: '@zenstackhq/sdk': specifier: workspace:* version: link:../sdk - '@zenstackhq/testtools': - specifier: workspace:* - version: link:../testtools '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -389,21 +426,33 @@ importers: packages/testtools: dependencies: + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../common-helpers '@zenstackhq/language': specifier: workspace:* version: link:../language + '@zenstackhq/plugin-policy': + specifier: workspace:* + version: link:../plugins/policy + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../runtime '@zenstackhq/sdk': specifier: workspace:* version: link:../sdk better-sqlite3: - specifier: ^12.2.0 + specifier: 'catalog:' version: 12.2.0 glob: specifier: ^11.0.2 version: 11.0.2 + kysely: + specifier: 'catalog:' + version: 0.27.6 pg: - specifier: ^8.13.1 - version: 8.13.1 + specifier: 'catalog:' + version: 8.16.3 prisma: specifier: 'catalog:' version: 6.14.0(typescript@5.8.3) @@ -413,13 +462,16 @@ importers: ts-pattern: specifier: 'catalog:' version: 5.7.1 - typescript: - specifier: 'catalog:' - version: 5.8.3 devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 '@types/node': specifier: 'catalog:' version: 20.17.24 + '@types/pg': + specifier: ^8.11.11 + version: 8.11.11 '@types/tmp': specifier: 'catalog:' version: 0.2.6 @@ -429,6 +481,12 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + typescript: + specifier: 'catalog:' + version: 5.8.3 packages/typescript-config: {} @@ -480,13 +538,49 @@ importers: tests/e2e: dependencies: + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 + '@zenstackhq/cli': + specifier: workspace:* + version: link:../../packages/cli + '@zenstackhq/language': + specifier: workspace:* + version: link:../../packages/language + '@zenstackhq/plugin-policy': + specifier: workspace:* + version: link:../../packages/plugins/policy + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../../packages/runtime + '@zenstackhq/sdk': + specifier: workspace:* + version: link:../../packages/sdk '@zenstackhq/testtools': specifier: workspace:* version: link:../../packages/testtools + better-sqlite3: + specifier: 'catalog:' + version: 12.2.0 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 + kysely: + specifier: 'catalog:' + version: 0.27.6 + ulid: + specifier: ^3.0.0 + version: 3.0.0 + uuid: + specifier: ^11.0.5 + version: 11.0.5 devDependencies: - '@zenstackhq/cli': + '@types/uuid': + specifier: ^11.0.0 + version: 11.0.0 + '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../packages/cli + version: link:../../packages/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* version: link:../../packages/vitest-config @@ -1177,6 +1271,10 @@ packages: '@types/toposort@2.0.7': resolution: {integrity: sha512-sQNk65vbC36+UixCkcky+dCr7MlflHcVILg1FVGqlUntsLFv9xd9ToWIVko/gTuin+cVe16t+2YubEFkhnSuPQ==} + '@types/uuid@11.0.0': + resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} + deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed. + '@types/vscode@1.101.0': resolution: {integrity: sha512-ZWf0IWa+NGegdW3iU42AcDTFHWW7fApLdkdnBqwYEtHVIBGbTu0ZNQKP/kX3Ds/uMJXIMQNAojHR4vexCEEz5Q==} @@ -1419,6 +1517,9 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1459,6 +1560,13 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + copyfiles@2.4.1: + resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} + hasBin: true + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1563,6 +1671,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1687,6 +1799,9 @@ packages: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1695,6 +1810,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1733,6 +1852,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1785,6 +1908,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1815,6 +1942,12 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1981,6 +2114,11 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -2013,6 +2151,9 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + noms@0.0.0: + resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} + nypm@0.6.1: resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==} engines: {node: ^14.16.0 || >=16.10.0} @@ -2069,6 +2210,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2091,11 +2236,11 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - pg-cloudflare@1.1.1: - resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} - pg-connection-string@2.7.0: - resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + pg-connection-string@2.9.1: + resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -2105,13 +2250,13 @@ packages: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} - pg-pool@3.7.0: - resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + pg-pool@3.10.1: + resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} peerDependencies: pg: '>=8.0' - pg-protocol@1.7.0: - resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -2121,9 +2266,9 @@ packages: resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} engines: {node: '>=10'} - pg@8.13.1: - resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} - engines: {node: '>= 8.0.0'} + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' peerDependenciesMeta: @@ -2239,6 +2384,9 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -2266,6 +2414,12 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2274,6 +2428,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2301,6 +2459,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2367,6 +2528,12 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2412,6 +2579,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2563,6 +2733,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2708,11 +2882,23 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3177,7 +3363,7 @@ snapshots: '@types/pg@8.11.11': dependencies: '@types/node': 20.17.24 - pg-protocol: 1.7.0 + pg-protocol: 1.10.3 pg-types: 4.0.2 '@types/pluralize@0.0.33': {} @@ -3193,6 +3379,10 @@ snapshots: '@types/toposort@2.0.7': {} + '@types/uuid@11.0.0': + dependencies: + uuid: 11.0.5 + '@types/vscode@1.101.0': {} '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': @@ -3493,6 +3683,12 @@ snapshots: cli-spinners@2.9.2: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} color-convert@2.0.1: @@ -3517,6 +3713,18 @@ snapshots: consola@3.4.2: {} + copyfiles@2.4.1: + dependencies: + glob: 7.2.3 + minimatch: 3.1.2 + mkdirp: 1.0.4 + noms: 0.0.0 + through2: 2.0.5 + untildify: 4.0.0 + yargs: 16.2.0 + + core-util-is@1.0.3: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3647,6 +3855,8 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} eslint-scope@8.4.0: @@ -3795,11 +4005,15 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3863,6 +4077,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@14.0.0: {} gopd@1.2.0: {} @@ -3903,6 +4126,11 @@ snapshots: imurmurhash@0.1.4: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ini@1.3.8: {} @@ -3921,6 +4149,10 @@ snapshots: is-unicode-supported@0.1.0: {} + isarray@0.0.1: {} + + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -4077,6 +4309,8 @@ snapshots: mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} + mlly@1.7.4: dependencies: acorn: 8.15.0 @@ -4106,6 +4340,11 @@ snapshots: node-fetch-native@1.6.7: {} + noms@0.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 1.0.34 + nypm@0.6.1: dependencies: citty: 0.1.6 @@ -4169,6 +4408,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-scurry@1.11.1: @@ -4187,20 +4428,20 @@ snapshots: perfect-debounce@1.0.0: {} - pg-cloudflare@1.1.1: + pg-cloudflare@1.2.7: optional: true - pg-connection-string@2.7.0: {} + pg-connection-string@2.9.1: {} pg-int8@1.0.1: {} pg-numeric@1.0.2: {} - pg-pool@3.7.0(pg@8.13.1): + pg-pool@3.10.1(pg@8.16.3): dependencies: - pg: 8.13.1 + pg: 8.16.3 - pg-protocol@1.7.0: {} + pg-protocol@1.10.3: {} pg-types@2.2.0: dependencies: @@ -4220,15 +4461,15 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.4 - pg@8.13.1: + pg@8.16.3: dependencies: - pg-connection-string: 2.7.0 - pg-pool: 3.7.0(pg@8.13.1) - pg-protocol: 1.7.0 + pg-connection-string: 2.9.1 + pg-pool: 3.10.1(pg@8.16.3) + pg-protocol: 1.10.3 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.1.1 + pg-cloudflare: 1.2.7 pgpass@1.0.5: dependencies: @@ -4321,6 +4562,8 @@ snapshots: transitivePeerDependencies: - magicast + process-nextick-args@2.0.1: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -4348,6 +4591,23 @@ snapshots: react@19.1.0: {} + readable-stream@1.0.34: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -4356,6 +4616,8 @@ snapshots: readdirp@4.1.2: {} + require-directory@2.1.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -4399,6 +4661,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} semver@7.7.2: {} @@ -4458,6 +4722,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string_decoder@0.10.31: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -4515,6 +4785,11 @@ snapshots: dependencies: any-promise: 1.3.0 + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4652,6 +4927,8 @@ snapshots: universalify@2.0.1: {} + untildify@4.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -4799,8 +5076,22 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + yaml@2.8.0: {} + yargs-parser@20.2.9: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yocto-queue@0.1.0: {} zod-validation-error@4.0.1(zod@3.25.76): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d54970c6..9dc25b4c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,3 +14,5 @@ catalog: tmp: ^0.2.3 '@types/tmp': ^0.2.6 'zod-validation-error': ^4.0.1 + 'better-sqlite3': ^12.2.0 + 'pg': ^8.13.1 diff --git a/tests/e2e/cal.com/cal-com.test.ts b/tests/e2e/github-repos/cal.com/cal-com.test.ts similarity index 100% rename from tests/e2e/cal.com/cal-com.test.ts rename to tests/e2e/github-repos/cal.com/cal-com.test.ts diff --git a/tests/e2e/cal.com/schema.zmodel b/tests/e2e/github-repos/cal.com/schema.zmodel similarity index 100% rename from tests/e2e/cal.com/schema.zmodel rename to tests/e2e/github-repos/cal.com/schema.zmodel diff --git a/tests/e2e/formbricks/formbricks.test.ts b/tests/e2e/github-repos/formbricks/formbricks.test.ts similarity index 100% rename from tests/e2e/formbricks/formbricks.test.ts rename to tests/e2e/github-repos/formbricks/formbricks.test.ts diff --git a/tests/e2e/formbricks/schema.zmodel b/tests/e2e/github-repos/formbricks/schema.zmodel similarity index 100% rename from tests/e2e/formbricks/schema.zmodel rename to tests/e2e/github-repos/formbricks/schema.zmodel diff --git a/tests/e2e/trigger.dev/schema.zmodel b/tests/e2e/github-repos/trigger.dev/schema.zmodel similarity index 100% rename from tests/e2e/trigger.dev/schema.zmodel rename to tests/e2e/github-repos/trigger.dev/schema.zmodel diff --git a/tests/e2e/trigger.dev/trigger-dev.test.ts b/tests/e2e/github-repos/trigger.dev/trigger-dev.test.ts similarity index 100% rename from tests/e2e/trigger.dev/trigger-dev.test.ts rename to tests/e2e/github-repos/trigger.dev/trigger-dev.test.ts diff --git a/packages/runtime/test/client-api/aggregate.test.ts b/tests/e2e/orm/client-api/aggregate.test.ts similarity index 97% rename from packages/runtime/test/client-api/aggregate.test.ts rename to tests/e2e/orm/client-api/aggregate.test.ts index 6b7edd64..9c1d2a1f 100644 --- a/packages/runtime/test/client-api/aggregate.test.ts +++ b/tests/e2e/orm/client-api/aggregate.test.ts @@ -1,8 +1,8 @@ +import type { ClientContract } from '@zenstackhq/runtime'; +import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; import { schema } from '../schemas/basic'; import { createUser } from './utils'; -import { createTestClient } from '../utils'; describe('Client aggregate tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/compound-id.test.ts b/tests/e2e/orm/client-api/compound-id.test.ts similarity index 99% rename from packages/runtime/test/client-api/compound-id.test.ts rename to tests/e2e/orm/client-api/compound-id.test.ts index 88457467..b983b045 100644 --- a/packages/runtime/test/client-api/compound-id.test.ts +++ b/tests/e2e/orm/client-api/compound-id.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Compound ID tests', () => { describe('to-one relation', () => { diff --git a/packages/runtime/test/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts similarity index 98% rename from packages/runtime/test/client-api/computed-fields.test.ts rename to tests/e2e/orm/client-api/computed-fields.test.ts index 0ece9ddf..4969d21d 100644 --- a/packages/runtime/test/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1,6 +1,6 @@ -import { sql } from 'kysely'; +import { sql } from '@zenstackhq/runtime/helpers'; +import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../utils'; describe('Computed fields tests', () => { let db: any; diff --git a/packages/runtime/test/client-api/count.test.ts b/tests/e2e/orm/client-api/count.test.ts similarity index 94% rename from packages/runtime/test/client-api/count.test.ts rename to tests/e2e/orm/client-api/count.test.ts index 22a89ddc..78050b95 100644 --- a/packages/runtime/test/client-api/count.test.ts +++ b/tests/e2e/orm/client-api/count.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client count tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/create-many-and-return.test.ts b/tests/e2e/orm/client-api/create-many-and-return.test.ts similarity index 95% rename from packages/runtime/test/client-api/create-many-and-return.test.ts rename to tests/e2e/orm/client-api/create-many-and-return.test.ts index be2a46e8..3b93ce22 100644 --- a/packages/runtime/test/client-api/create-many-and-return.test.ts +++ b/tests/e2e/orm/client-api/create-many-and-return.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client createManyAndReturn tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/create-many.test.ts b/tests/e2e/orm/client-api/create-many.test.ts similarity index 93% rename from packages/runtime/test/client-api/create-many.test.ts rename to tests/e2e/orm/client-api/create-many.test.ts index 3ccbbe73..961f3c36 100644 --- a/packages/runtime/test/client-api/create-many.test.ts +++ b/tests/e2e/orm/client-api/create-many.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client createMany tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/create.test.ts b/tests/e2e/orm/client-api/create.test.ts similarity index 98% rename from packages/runtime/test/client-api/create.test.ts rename to tests/e2e/orm/client-api/create.test.ts index 41ab341c..72e2ad7a 100644 --- a/packages/runtime/test/client-api/create.test.ts +++ b/tests/e2e/orm/client-api/create.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client create tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/default-values.test.ts b/tests/e2e/orm/client-api/default-values.test.ts similarity index 96% rename from packages/runtime/test/client-api/default-values.test.ts rename to tests/e2e/orm/client-api/default-values.test.ts index 1e257a10..73349e59 100644 --- a/packages/runtime/test/client-api/default-values.test.ts +++ b/tests/e2e/orm/client-api/default-values.test.ts @@ -1,11 +1,11 @@ import { isCuid } from '@paralleldrive/cuid2'; +import { ZenStackClient } from '@zenstackhq/runtime'; +import { ExpressionUtils, type SchemaDef } from '@zenstackhq/runtime/schema'; import SQLite from 'better-sqlite3'; import { SqliteDialect } from 'kysely'; import { isValid as isValidUlid } from 'ulid'; import { validate as isValidUuid } from 'uuid'; import { describe, expect, it } from 'vitest'; -import { ZenStackClient } from '../../src'; -import { ExpressionUtils, type SchemaDef } from '../../src/schema'; const schema = { provider: { diff --git a/packages/runtime/test/client-api/delegate.test.ts b/tests/e2e/orm/client-api/delegate.test.ts similarity index 99% rename from packages/runtime/test/client-api/delegate.test.ts rename to tests/e2e/orm/client-api/delegate.test.ts index d9efff6f..8d5cfbb9 100644 --- a/packages/runtime/test/client-api/delegate.test.ts +++ b/tests/e2e/orm/client-api/delegate.test.ts @@ -1,8 +1,8 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema, type SchemaType } from '../schemas/delegate/schema'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Delegate model tests ', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/delete-many.test.ts b/tests/e2e/orm/client-api/delete-many.test.ts similarity index 94% rename from packages/runtime/test/client-api/delete-many.test.ts rename to tests/e2e/orm/client-api/delete-many.test.ts index b31896f0..d80f2f93 100644 --- a/packages/runtime/test/client-api/delete-many.test.ts +++ b/tests/e2e/orm/client-api/delete-many.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client deleteMany tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/delete.test.ts b/tests/e2e/orm/client-api/delete.test.ts similarity index 92% rename from packages/runtime/test/client-api/delete.test.ts rename to tests/e2e/orm/client-api/delete.test.ts index 4e518c07..668f3b9f 100644 --- a/packages/runtime/test/client-api/delete.test.ts +++ b/tests/e2e/orm/client-api/delete.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client delete tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/filter.test.ts b/tests/e2e/orm/client-api/filter.test.ts similarity index 99% rename from packages/runtime/test/client-api/filter.test.ts rename to tests/e2e/orm/client-api/filter.test.ts index 26af9dd7..fd1335fe 100644 --- a/packages/runtime/test/client-api/filter.test.ts +++ b/tests/e2e/orm/client-api/filter.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client filter tests ', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts similarity index 99% rename from packages/runtime/test/client-api/find.test.ts rename to tests/e2e/orm/client-api/find.test.ts index 1f8219ec..6d188843 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; -import { InputValidationError, NotFoundError } from '../../src/client/errors'; +import type { ClientContract } from '@zenstackhq/runtime'; +import { InputValidationError, NotFoundError } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; import { createPosts, createUser } from './utils'; describe('Client find tests ', () => { diff --git a/packages/runtime/test/client-api/group-by.test.ts b/tests/e2e/orm/client-api/group-by.test.ts similarity index 98% rename from packages/runtime/test/client-api/group-by.test.ts rename to tests/e2e/orm/client-api/group-by.test.ts index b0909e34..08da59fe 100644 --- a/packages/runtime/test/client-api/group-by.test.ts +++ b/tests/e2e/orm/client-api/group-by.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; import { createPosts, createUser } from './utils'; describe('Client groupBy tests', () => { diff --git a/packages/runtime/test/client-api/import.test.ts b/tests/e2e/orm/client-api/import.test.ts similarity index 97% rename from packages/runtime/test/client-api/import.test.ts rename to tests/e2e/orm/client-api/import.test.ts index 98e43f77..9e7657f8 100644 --- a/packages/runtime/test/client-api/import.test.ts +++ b/tests/e2e/orm/client-api/import.test.ts @@ -2,7 +2,7 @@ import { createTestProject, generateTsSchemaInPlace } from '@zenstackhq/testtool import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Import tests', () => { it('works with imported models', async () => { diff --git a/packages/runtime/test/client-api/mixin.test.ts b/tests/e2e/orm/client-api/mixin.test.ts similarity index 98% rename from packages/runtime/test/client-api/mixin.test.ts rename to tests/e2e/orm/client-api/mixin.test.ts index e7b9dcac..1e6d0f41 100644 --- a/packages/runtime/test/client-api/mixin.test.ts +++ b/tests/e2e/orm/client-api/mixin.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Mixin tests', () => { it('includes fields and attributes from mixins', async () => { diff --git a/packages/runtime/test/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts similarity index 99% rename from packages/runtime/test/client-api/name-mapping.test.ts rename to tests/e2e/orm/client-api/name-mapping.test.ts index cfb9bec2..3fafc038 100644 --- a/packages/runtime/test/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -1,8 +1,8 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema, type SchemaType } from '../schemas/name-mapping/schema'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Name mapping tests', () => { let db: ClientContract; diff --git a/packages/runtime/test/client-api/raw-query.test.ts b/tests/e2e/orm/client-api/raw-query.test.ts similarity index 95% rename from packages/runtime/test/client-api/raw-query.test.ts rename to tests/e2e/orm/client-api/raw-query.test.ts index f7838641..517dfb49 100644 --- a/packages/runtime/test/client-api/raw-query.test.ts +++ b/tests/e2e/orm/client-api/raw-query.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client raw query tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/relation/many-to-many.test.ts b/tests/e2e/orm/client-api/relation/many-to-many.test.ts similarity index 99% rename from packages/runtime/test/client-api/relation/many-to-many.test.ts rename to tests/e2e/orm/client-api/relation/many-to-many.test.ts index dd1eacf0..c6fdb2c5 100644 --- a/packages/runtime/test/client-api/relation/many-to-many.test.ts +++ b/tests/e2e/orm/client-api/relation/many-to-many.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Many-to-many relation tests', () => { let client: any; diff --git a/packages/runtime/test/client-api/relation/one-to-many.test.ts b/tests/e2e/orm/client-api/relation/one-to-many.test.ts similarity index 97% rename from packages/runtime/test/client-api/relation/one-to-many.test.ts rename to tests/e2e/orm/client-api/relation/one-to-many.test.ts index be847d5e..377b8e21 100644 --- a/packages/runtime/test/client-api/relation/one-to-many.test.ts +++ b/tests/e2e/orm/client-api/relation/one-to-many.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('One-to-many relation tests ', () => { let client: any; diff --git a/packages/runtime/test/client-api/relation/one-to-one.test.ts b/tests/e2e/orm/client-api/relation/one-to-one.test.ts similarity index 97% rename from packages/runtime/test/client-api/relation/one-to-one.test.ts rename to tests/e2e/orm/client-api/relation/one-to-one.test.ts index e41a0cf9..961961aa 100644 --- a/packages/runtime/test/client-api/relation/one-to-one.test.ts +++ b/tests/e2e/orm/client-api/relation/one-to-one.test.ts @@ -1,5 +1,5 @@ +import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../../utils'; const TEST_DB = 'client-api-relation-test-one-to-one'; diff --git a/packages/runtime/test/client-api/relation/self-relation.test.ts b/tests/e2e/orm/client-api/relation/self-relation.test.ts similarity index 99% rename from packages/runtime/test/client-api/relation/self-relation.test.ts rename to tests/e2e/orm/client-api/relation/self-relation.test.ts index 65380b30..9e70ca00 100644 --- a/packages/runtime/test/client-api/relation/self-relation.test.ts +++ b/tests/e2e/orm/client-api/relation/self-relation.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Self relation tests', () => { let client: any; diff --git a/packages/runtime/test/client-api/scalar-list.test.ts b/tests/e2e/orm/client-api/scalar-list.test.ts similarity index 99% rename from packages/runtime/test/client-api/scalar-list.test.ts rename to tests/e2e/orm/client-api/scalar-list.test.ts index c9bfb0fc..22ea5d2f 100644 --- a/packages/runtime/test/client-api/scalar-list.test.ts +++ b/tests/e2e/orm/client-api/scalar-list.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Scalar list tests', () => { const schema = ` diff --git a/packages/runtime/test/client-api/transaction.test.ts b/tests/e2e/orm/client-api/transaction.test.ts similarity index 97% rename from packages/runtime/test/client-api/transaction.test.ts rename to tests/e2e/orm/client-api/transaction.test.ts index c235420a..98f7a49c 100644 --- a/packages/runtime/test/client-api/transaction.test.ts +++ b/tests/e2e/orm/client-api/transaction.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client raw query tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/type-coverage.test.ts b/tests/e2e/orm/client-api/type-coverage.test.ts similarity index 98% rename from packages/runtime/test/client-api/type-coverage.test.ts rename to tests/e2e/orm/client-api/type-coverage.test.ts index 1055c712..9ce29fce 100644 --- a/packages/runtime/test/client-api/type-coverage.test.ts +++ b/tests/e2e/orm/client-api/type-coverage.test.ts @@ -1,6 +1,6 @@ import Decimal from 'decimal.js'; import { describe, expect, it } from 'vitest'; -import { createTestClient, getTestDbProvider } from '../utils'; +import { createTestClient, getTestDbProvider } from '@zenstackhq/testtools'; describe('Zmodel type coverage tests', () => { it('supports all types - plain', async () => { diff --git a/packages/runtime/test/client-api/typed-json-fields.test.ts b/tests/e2e/orm/client-api/typed-json-fields.test.ts similarity index 98% rename from packages/runtime/test/client-api/typed-json-fields.test.ts rename to tests/e2e/orm/client-api/typed-json-fields.test.ts index 65757169..a213c1d8 100644 --- a/packages/runtime/test/client-api/typed-json-fields.test.ts +++ b/tests/e2e/orm/client-api/typed-json-fields.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Typed JSON fields', () => { const schema = ` diff --git a/packages/runtime/test/client-api/undefined-values.test.ts b/tests/e2e/orm/client-api/undefined-values.test.ts similarity index 90% rename from packages/runtime/test/client-api/undefined-values.test.ts rename to tests/e2e/orm/client-api/undefined-values.test.ts index 74d2851b..240ad1f4 100644 --- a/packages/runtime/test/client-api/undefined-values.test.ts +++ b/tests/e2e/orm/client-api/undefined-values.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; import { createUser } from './utils'; describe('Client undefined values tests ', () => { diff --git a/packages/runtime/test/client-api/update-many.test.ts b/tests/e2e/orm/client-api/update-many.test.ts similarity index 96% rename from packages/runtime/test/client-api/update-many.test.ts rename to tests/e2e/orm/client-api/update-many.test.ts index 934154fe..b37abf35 100644 --- a/packages/runtime/test/client-api/update-many.test.ts +++ b/tests/e2e/orm/client-api/update-many.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client updateMany tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts similarity index 99% rename from packages/runtime/test/client-api/update.test.ts rename to tests/e2e/orm/client-api/update.test.ts index 82ec4a5a..ab39d706 100644 --- a/packages/runtime/test/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; import { createUser } from './utils'; describe('Client update tests', () => { diff --git a/packages/runtime/test/client-api/upsert.test.ts b/tests/e2e/orm/client-api/upsert.test.ts similarity index 94% rename from packages/runtime/test/client-api/upsert.test.ts rename to tests/e2e/orm/client-api/upsert.test.ts index cbb16d65..14f418b3 100644 --- a/packages/runtime/test/client-api/upsert.test.ts +++ b/tests/e2e/orm/client-api/upsert.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client upsert tests', () => { let client: ClientContract; diff --git a/packages/runtime/test/client-api/utils.ts b/tests/e2e/orm/client-api/utils.ts similarity index 92% rename from packages/runtime/test/client-api/utils.ts rename to tests/e2e/orm/client-api/utils.ts index 2f1b85d0..11fd2090 100644 --- a/packages/runtime/test/client-api/utils.ts +++ b/tests/e2e/orm/client-api/utils.ts @@ -1,4 +1,4 @@ -import type { ClientContract } from '../../src/client'; +import type { ClientContract } from '@zenstackhq/runtime'; import type { schema } from '../schemas/basic'; type ClientType = ClientContract; diff --git a/packages/runtime/test/plugin-infra/entity-mutation-hooks.test.ts b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts similarity index 99% rename from packages/runtime/test/plugin-infra/entity-mutation-hooks.test.ts rename to tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts index 9172d0f5..63133e65 100644 --- a/packages/runtime/test/plugin-infra/entity-mutation-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts @@ -1,8 +1,8 @@ +import { type ClientContract } from '@zenstackhq/runtime'; +import { createTestClient } from '@zenstackhq/testtools'; import { DeleteQueryNode, InsertQueryNode, UpdateQueryNode } from 'kysely'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { type ClientContract } from '../../src'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; describe('Entity mutation hooks tests', () => { let _client: ClientContract; diff --git a/packages/runtime/test/plugin-infra/on-kysely-query.test.ts b/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts similarity index 98% rename from packages/runtime/test/plugin-infra/on-kysely-query.test.ts rename to tests/e2e/orm/plugin-infra/on-kysely-query.test.ts index 4fe5855d..8d7d0507 100644 --- a/packages/runtime/test/plugin-infra/on-kysely-query.test.ts +++ b/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts @@ -1,8 +1,8 @@ +import { type ClientContract } from '@zenstackhq/runtime'; +import { createTestClient } from '@zenstackhq/testtools'; import { InsertQueryNode, Kysely, PrimitiveValueListNode, ValuesNode, type QueryResult } from 'kysely'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { type ClientContract } from '../../src/client'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; describe('On kysely query tests', () => { let _client: ClientContract; diff --git a/packages/runtime/test/plugin-infra/on-query-hooks.test.ts b/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts similarity index 98% rename from packages/runtime/test/plugin-infra/on-query-hooks.test.ts rename to tests/e2e/orm/plugin-infra/on-query-hooks.test.ts index 3a6df8ca..90073ab5 100644 --- a/packages/runtime/test/plugin-infra/on-query-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts @@ -1,7 +1,7 @@ +import { definePlugin, type ClientContract } from '@zenstackhq/runtime'; +import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { definePlugin, type ClientContract } from '../../src/client'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; describe('On query hooks tests', () => { let _client: ClientContract; diff --git a/packages/runtime/test/policy/auth-equality.test.ts b/tests/e2e/orm/policy/auth-equality.test.ts similarity index 97% rename from packages/runtime/test/policy/auth-equality.test.ts rename to tests/e2e/orm/policy/auth-equality.test.ts index 2baf46ed..ebf9fd1a 100644 --- a/packages/runtime/test/policy/auth-equality.test.ts +++ b/tests/e2e/orm/policy/auth-equality.test.ts @@ -1,5 +1,5 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; describe('Reference Equality Tests', () => { it('works with create and auth equality', async () => { diff --git a/packages/runtime/test/policy/basic-schema-read.test.ts b/tests/e2e/orm/policy/basic-schema-read.test.ts similarity index 90% rename from packages/runtime/test/policy/basic-schema-read.test.ts rename to tests/e2e/orm/policy/basic-schema-read.test.ts index c8b8c87e..40eb2bef 100644 --- a/packages/runtime/test/policy/basic-schema-read.test.ts +++ b/tests/e2e/orm/policy/basic-schema-read.test.ts @@ -1,8 +1,8 @@ +import { PolicyPlugin } from '@zenstackhq/plugin-policy'; +import { type ClientContract } from '@zenstackhq/runtime'; +import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { type ClientContract } from '../../src/client'; -import { PolicyPlugin } from '../../src/plugins/policy/plugin'; import { schema } from '../schemas/basic'; -import { createTestClient } from '../utils'; describe('Read policy tests', () => { let client: ClientContract; @@ -23,7 +23,7 @@ describe('Read policy tests', () => { }); // anonymous auth context by default - const anonClient = client.$use(new PolicyPlugin()); + const anonClient = client.$use(new PolicyPlugin()); await expect(anonClient.user.findFirst()).toResolveNull(); const authClient = anonClient.$setAuth({ diff --git a/packages/runtime/test/policy/crud/create.test.ts b/tests/e2e/orm/policy/crud/create.test.ts similarity index 99% rename from packages/runtime/test/policy/crud/create.test.ts rename to tests/e2e/orm/policy/crud/create.test.ts index d5eb0657..67b01500 100644 --- a/packages/runtime/test/policy/crud/create.test.ts +++ b/tests/e2e/orm/policy/crud/create.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy create tests', () => { it('works with scalar field check', async () => { diff --git a/packages/runtime/test/policy/crud/delete.test.ts b/tests/e2e/orm/policy/crud/delete.test.ts similarity index 96% rename from packages/runtime/test/policy/crud/delete.test.ts rename to tests/e2e/orm/policy/crud/delete.test.ts index f515f0dc..1572d521 100644 --- a/packages/runtime/test/policy/crud/delete.test.ts +++ b/tests/e2e/orm/policy/crud/delete.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Delete policy tests', () => { it('works with top-level delete/deleteMany', async () => { diff --git a/packages/runtime/test/policy/crud/dumb-rules.test.ts b/tests/e2e/orm/policy/crud/dumb-rules.test.ts similarity index 94% rename from packages/runtime/test/policy/crud/dumb-rules.test.ts rename to tests/e2e/orm/policy/crud/dumb-rules.test.ts index b169e3a0..6b9e1f81 100644 --- a/packages/runtime/test/policy/crud/dumb-rules.test.ts +++ b/tests/e2e/orm/policy/crud/dumb-rules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy dumb rules tests', () => { it('works with create dumb rules', async () => { diff --git a/packages/runtime/test/policy/crud/post-update.test.ts b/tests/e2e/orm/policy/crud/post-update.test.ts similarity index 99% rename from packages/runtime/test/policy/crud/post-update.test.ts rename to tests/e2e/orm/policy/crud/post-update.test.ts index 98c2b5b5..dfaa8599 100644 --- a/packages/runtime/test/policy/crud/post-update.test.ts +++ b/tests/e2e/orm/policy/crud/post-update.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy post-update tests', () => { it('allows post-update by default', async () => { diff --git a/packages/runtime/test/policy/crud/read.test.ts b/tests/e2e/orm/policy/crud/read.test.ts similarity index 99% rename from packages/runtime/test/policy/crud/read.test.ts rename to tests/e2e/orm/policy/crud/read.test.ts index 46f4e38b..d57d6385 100644 --- a/packages/runtime/test/policy/crud/read.test.ts +++ b/tests/e2e/orm/policy/crud/read.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Read policy tests', () => { describe('Find tests', () => { diff --git a/packages/runtime/test/policy/crud/update.test.ts b/tests/e2e/orm/policy/crud/update.test.ts similarity index 99% rename from packages/runtime/test/policy/crud/update.test.ts rename to tests/e2e/orm/policy/crud/update.test.ts index c092682b..9a83577f 100644 --- a/packages/runtime/test/policy/crud/update.test.ts +++ b/tests/e2e/orm/policy/crud/update.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Update policy tests', () => { describe('Scalar condition tests', () => { diff --git a/packages/runtime/test/policy/migrated/auth.test.ts b/tests/e2e/orm/policy/migrated/auth.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/auth.test.ts rename to tests/e2e/orm/policy/migrated/auth.test.ts index d075e3d7..fb8f30de 100644 --- a/packages/runtime/test/policy/migrated/auth.test.ts +++ b/tests/e2e/orm/policy/migrated/auth.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('auth() tests', () => { it('works with string id non-null test', async () => { diff --git a/packages/runtime/test/policy/migrated/client-extensions.test.ts b/tests/e2e/orm/policy/migrated/client-extensions.test.ts similarity index 97% rename from packages/runtime/test/policy/migrated/client-extensions.test.ts rename to tests/e2e/orm/policy/migrated/client-extensions.test.ts index 16692543..c62f49f9 100644 --- a/packages/runtime/test/policy/migrated/client-extensions.test.ts +++ b/tests/e2e/orm/policy/migrated/client-extensions.test.ts @@ -1,6 +1,6 @@ +import { definePlugin } from '@zenstackhq/runtime'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -import { definePlugin } from '../../../src/client'; -import { createPolicyTestClient } from '../utils'; describe('client extensions tests for policies', () => { it('query override one model', async () => { diff --git a/packages/runtime/test/policy/migrated/connect-disconnect.test.ts b/tests/e2e/orm/policy/migrated/connect-disconnect.test.ts similarity index 97% rename from packages/runtime/test/policy/migrated/connect-disconnect.test.ts rename to tests/e2e/orm/policy/migrated/connect-disconnect.test.ts index 02d8e04e..ec338727 100644 --- a/packages/runtime/test/policy/migrated/connect-disconnect.test.ts +++ b/tests/e2e/orm/policy/migrated/connect-disconnect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('connect and disconnect tests', () => { const modelToMany = ` @@ -302,8 +302,7 @@ describe('connect and disconnect tests', () => { } `; - // TODO: many-to-many support - it.skip('works with implicit many-to-many', async () => { + it('works with implicit many-to-many', async () => { const db = await createPolicyTestClient(modelImplicitManyToMany); const rawDb = db.$unuseAll(); @@ -350,8 +349,7 @@ describe('connect and disconnect tests', () => { } `; - // TODO: many-to-many support - it.skip('works with explicit many-to-many', async () => { + it('works with explicit many-to-many', async () => { const db = await createPolicyTestClient(modelExplicitManyToMany); const rawDb = db.$unuseAll(); diff --git a/packages/runtime/test/policy/migrated/create-many-and-return.test.ts b/tests/e2e/orm/policy/migrated/create-many-and-return.test.ts similarity index 97% rename from packages/runtime/test/policy/migrated/create-many-and-return.test.ts rename to tests/e2e/orm/policy/migrated/create-many-and-return.test.ts index 1df0e5b6..5191853c 100644 --- a/packages/runtime/test/policy/migrated/create-many-and-return.test.ts +++ b/tests/e2e/orm/policy/migrated/create-many-and-return.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('createManyAndReturn tests', () => { it('works with model-level policies', async () => { diff --git a/packages/runtime/test/policy/migrated/cross-model-field-comparison.test.ts b/tests/e2e/orm/policy/migrated/cross-model-field-comparison.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/cross-model-field-comparison.test.ts rename to tests/e2e/orm/policy/migrated/cross-model-field-comparison.test.ts index f0a35f79..d7138794 100644 --- a/packages/runtime/test/policy/migrated/cross-model-field-comparison.test.ts +++ b/tests/e2e/orm/policy/migrated/cross-model-field-comparison.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('cross-model field comparison tests', () => { it('works with to-one relation', async () => { diff --git a/packages/runtime/test/policy/migrated/current-model.test.ts b/tests/e2e/orm/policy/migrated/current-model.test.ts similarity index 95% rename from packages/runtime/test/policy/migrated/current-model.test.ts rename to tests/e2e/orm/policy/migrated/current-model.test.ts index 61ea1d24..04c9a30a 100644 --- a/packages/runtime/test/policy/migrated/current-model.test.ts +++ b/tests/e2e/orm/policy/migrated/current-model.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('currentModel tests', () => { it('works in models', async () => { @@ -107,20 +107,19 @@ describe('currentModel tests', () => { await expect(db.POST.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); - // TODO: abstract base support - it.skip('works when inherited from abstract base', async () => { + it('works when inherited from abstract base', async () => { const db = await createPolicyTestClient( ` - abstract model Base { + type Base { id Int @id @@allow('read', true) @@allow('create', currentModel() == 'User') } - model User extends Base { + model User with Base { } - model Post extends Base { + model Post with Base { } `, ); diff --git a/packages/runtime/test/policy/migrated/current-operation.test.ts b/tests/e2e/orm/policy/migrated/current-operation.test.ts similarity index 98% rename from packages/runtime/test/policy/migrated/current-operation.test.ts rename to tests/e2e/orm/policy/migrated/current-operation.test.ts index 42d67939..2b1610c9 100644 --- a/packages/runtime/test/policy/migrated/current-operation.test.ts +++ b/tests/e2e/orm/policy/migrated/current-operation.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('currentOperation tests', () => { it('works with specific rules', async () => { diff --git a/packages/runtime/test/policy/migrated/deep-nested.test.ts b/tests/e2e/orm/policy/migrated/deep-nested.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/deep-nested.test.ts rename to tests/e2e/orm/policy/migrated/deep-nested.test.ts index a88134ce..6bd38e1f 100644 --- a/packages/runtime/test/policy/migrated/deep-nested.test.ts +++ b/tests/e2e/orm/policy/migrated/deep-nested.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('deep nested operations tests', () => { const model = ` diff --git a/packages/runtime/test/policy/migrated/empty-policy.test.ts b/tests/e2e/orm/policy/migrated/empty-policy.test.ts similarity index 98% rename from packages/runtime/test/policy/migrated/empty-policy.test.ts rename to tests/e2e/orm/policy/migrated/empty-policy.test.ts index 452845b3..970afb9e 100644 --- a/packages/runtime/test/policy/migrated/empty-policy.test.ts +++ b/tests/e2e/orm/policy/migrated/empty-policy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('empty policy tests', () => { it('works with simple operations', async () => { diff --git a/packages/runtime/test/policy/migrated/field-comparison.test.ts b/tests/e2e/orm/policy/migrated/field-comparison.test.ts similarity index 97% rename from packages/runtime/test/policy/migrated/field-comparison.test.ts rename to tests/e2e/orm/policy/migrated/field-comparison.test.ts index 1bf33c37..33f6c124 100644 --- a/packages/runtime/test/policy/migrated/field-comparison.test.ts +++ b/tests/e2e/orm/policy/migrated/field-comparison.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('field comparison tests', () => { it('works with policies involving field comparison', async () => { diff --git a/packages/runtime/test/policy/migrated/multi-field-unique.test.ts b/tests/e2e/orm/policy/migrated/multi-field-unique.test.ts similarity index 98% rename from packages/runtime/test/policy/migrated/multi-field-unique.test.ts rename to tests/e2e/orm/policy/migrated/multi-field-unique.test.ts index fba22b09..bd2d25d0 100644 --- a/packages/runtime/test/policy/migrated/multi-field-unique.test.ts +++ b/tests/e2e/orm/policy/migrated/multi-field-unique.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { QueryError } from '../../../src'; -import { createPolicyTestClient } from '../utils'; +import { QueryError } from '@zenstackhq/runtime'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy tests multi-field unique', () => { it('toplevel crud test unnamed constraint', async () => { diff --git a/packages/runtime/test/policy/migrated/multi-id-fields.test.ts b/tests/e2e/orm/policy/migrated/multi-id-fields.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/multi-id-fields.test.ts rename to tests/e2e/orm/policy/migrated/multi-id-fields.test.ts index 9444fe20..0c3bdf2a 100644 --- a/packages/runtime/test/policy/migrated/multi-id-fields.test.ts +++ b/tests/e2e/orm/policy/migrated/multi-id-fields.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy tests multiple id fields', () => { it('multi-id fields crud', async () => { diff --git a/packages/runtime/test/policy/migrated/nested-to-many.test.ts b/tests/e2e/orm/policy/migrated/nested-to-many.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/nested-to-many.test.ts rename to tests/e2e/orm/policy/migrated/nested-to-many.test.ts index 03415119..e6402eb3 100644 --- a/packages/runtime/test/policy/migrated/nested-to-many.test.ts +++ b/tests/e2e/orm/policy/migrated/nested-to-many.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy tests to-many', () => { it('read filtering', async () => { diff --git a/packages/runtime/test/policy/migrated/nested-to-one.test.ts b/tests/e2e/orm/policy/migrated/nested-to-one.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/nested-to-one.test.ts rename to tests/e2e/orm/policy/migrated/nested-to-one.test.ts index 432c8065..1db22569 100644 --- a/packages/runtime/test/policy/migrated/nested-to-one.test.ts +++ b/tests/e2e/orm/policy/migrated/nested-to-one.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('With Policy:nested to-one', () => { it('read filtering for optional relation', async () => { diff --git a/packages/runtime/test/policy/migrated/omit.test.ts b/tests/e2e/orm/policy/migrated/omit.test.ts similarity index 95% rename from packages/runtime/test/policy/migrated/omit.test.ts rename to tests/e2e/orm/policy/migrated/omit.test.ts index 57fdd014..84761f5b 100644 --- a/packages/runtime/test/policy/migrated/omit.test.ts +++ b/tests/e2e/orm/policy/migrated/omit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('prisma omit', () => { it('per query', async () => { @@ -48,7 +48,6 @@ describe('prisma omit', () => { found = await db.user.findFirst({ select: { value: true, profile: { omit: { level: true } } }, }); - console.log(found); expect(found.age).toBeUndefined(); expect(found.value).toEqual(10); expect(found.profile.level).toBeUndefined(); diff --git a/packages/runtime/test/policy/migrated/petstore-sample.test.ts b/tests/e2e/orm/policy/migrated/petstore-sample.test.ts similarity index 95% rename from packages/runtime/test/policy/migrated/petstore-sample.test.ts rename to tests/e2e/orm/policy/migrated/petstore-sample.test.ts index 2b210827..9b7bebcf 100644 --- a/packages/runtime/test/policy/migrated/petstore-sample.test.ts +++ b/tests/e2e/orm/policy/migrated/petstore-sample.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { schema } from '../../schemas/petstore/schema'; describe('Pet Store Policy Tests', () => { diff --git a/packages/runtime/test/policy/migrated/query-reduction.test.ts b/tests/e2e/orm/policy/migrated/query-reduction.test.ts similarity index 98% rename from packages/runtime/test/policy/migrated/query-reduction.test.ts rename to tests/e2e/orm/policy/migrated/query-reduction.test.ts index 61b11d06..253156b0 100644 --- a/packages/runtime/test/policy/migrated/query-reduction.test.ts +++ b/tests/e2e/orm/policy/migrated/query-reduction.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('With Policy: query reduction', () => { it('test query reduction', async () => { diff --git a/packages/runtime/test/policy/migrated/relation-check.test.ts b/tests/e2e/orm/policy/migrated/relation-check.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/relation-check.test.ts rename to tests/e2e/orm/policy/migrated/relation-check.test.ts index be0947aa..dfff4dee 100644 --- a/packages/runtime/test/policy/migrated/relation-check.test.ts +++ b/tests/e2e/orm/policy/migrated/relation-check.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Relation checker', () => { it('should work for read', async () => { diff --git a/packages/runtime/test/policy/migrated/relation-many-to-many-filter.test.ts b/tests/e2e/orm/policy/migrated/relation-many-to-many-filter.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/relation-many-to-many-filter.test.ts rename to tests/e2e/orm/policy/migrated/relation-many-to-many-filter.test.ts index 916f8c50..eba86029 100644 --- a/packages/runtime/test/policy/migrated/relation-many-to-many-filter.test.ts +++ b/tests/e2e/orm/policy/migrated/relation-many-to-many-filter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy many-to-many relation tests', () => { const model = ` diff --git a/packages/runtime/test/policy/migrated/relation-one-to-many-filter.test.ts b/tests/e2e/orm/policy/migrated/relation-one-to-many-filter.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/relation-one-to-many-filter.test.ts rename to tests/e2e/orm/policy/migrated/relation-one-to-many-filter.test.ts index 4330c008..4805af86 100644 --- a/packages/runtime/test/policy/migrated/relation-one-to-many-filter.test.ts +++ b/tests/e2e/orm/policy/migrated/relation-one-to-many-filter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Relation one-to-many filter', () => { const model = ` diff --git a/packages/runtime/test/policy/migrated/relation-one-to-one-filter.test.ts b/tests/e2e/orm/policy/migrated/relation-one-to-one-filter.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/relation-one-to-one-filter.test.ts rename to tests/e2e/orm/policy/migrated/relation-one-to-one-filter.test.ts index 060eea77..d556103f 100644 --- a/packages/runtime/test/policy/migrated/relation-one-to-one-filter.test.ts +++ b/tests/e2e/orm/policy/migrated/relation-one-to-one-filter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Relation one-to-one filter', () => { const model = ` diff --git a/packages/runtime/test/policy/migrated/self-relation.test.ts b/tests/e2e/orm/policy/migrated/self-relation.test.ts similarity index 98% rename from packages/runtime/test/policy/migrated/self-relation.test.ts rename to tests/e2e/orm/policy/migrated/self-relation.test.ts index f06c34d2..6abef62a 100644 --- a/packages/runtime/test/policy/migrated/self-relation.test.ts +++ b/tests/e2e/orm/policy/migrated/self-relation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy self relations tests', () => { it('one-to-one', async () => { diff --git a/packages/runtime/test/policy/migrated/todo-sample.test.ts b/tests/e2e/orm/policy/migrated/todo-sample.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/todo-sample.test.ts rename to tests/e2e/orm/policy/migrated/todo-sample.test.ts index 541ca69b..a5d766e3 100644 --- a/packages/runtime/test/policy/migrated/todo-sample.test.ts +++ b/tests/e2e/orm/policy/migrated/todo-sample.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import type { ClientContract } from '../../../src'; +import type { ClientContract } from '@zenstackhq/runtime'; import { schema, type SchemaType } from '../../schemas/todo/schema'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Todo Policy Tests', () => { let db: ClientContract; diff --git a/packages/runtime/test/policy/migrated/toplevel-operations.test.ts b/tests/e2e/orm/policy/migrated/toplevel-operations.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/toplevel-operations.test.ts rename to tests/e2e/orm/policy/migrated/toplevel-operations.test.ts index f427c4ad..086efaf0 100644 --- a/packages/runtime/test/policy/migrated/toplevel-operations.test.ts +++ b/tests/e2e/orm/policy/migrated/toplevel-operations.test.ts @@ -1,5 +1,5 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; describe('Policy toplevel operations tests', () => { it('read tests', async () => { diff --git a/packages/runtime/test/policy/migrated/unique-as-id.test.ts b/tests/e2e/orm/policy/migrated/unique-as-id.test.ts similarity index 99% rename from packages/runtime/test/policy/migrated/unique-as-id.test.ts rename to tests/e2e/orm/policy/migrated/unique-as-id.test.ts index 6b3e8588..b245baaa 100644 --- a/packages/runtime/test/policy/migrated/unique-as-id.test.ts +++ b/tests/e2e/orm/policy/migrated/unique-as-id.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy unique as id tests', () => { it('unique fields', async () => { diff --git a/packages/runtime/test/policy/migrated/update-many-and-return.test.ts b/tests/e2e/orm/policy/migrated/update-many-and-return.test.ts similarity index 98% rename from packages/runtime/test/policy/migrated/update-many-and-return.test.ts rename to tests/e2e/orm/policy/migrated/update-many-and-return.test.ts index ba83335b..32367c35 100644 --- a/packages/runtime/test/policy/migrated/update-many-and-return.test.ts +++ b/tests/e2e/orm/policy/migrated/update-many-and-return.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('Policy updateManyAndReturn tests', () => { it('model-level policies', async () => { diff --git a/packages/runtime/test/policy/migrated/view.test.ts b/tests/e2e/orm/policy/migrated/view.test.ts similarity index 97% rename from packages/runtime/test/policy/migrated/view.test.ts rename to tests/e2e/orm/policy/migrated/view.test.ts index 010a7f1f..7a8afe28 100644 --- a/packages/runtime/test/policy/migrated/view.test.ts +++ b/tests/e2e/orm/policy/migrated/view.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from '../utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('View Policy Test', () => { it('view policy', async () => { diff --git a/packages/runtime/test/policy/mixin.test.ts b/tests/e2e/orm/policy/mixin.test.ts similarity index 98% rename from packages/runtime/test/policy/mixin.test.ts rename to tests/e2e/orm/policy/mixin.test.ts index 247e9864..94e3028e 100644 --- a/packages/runtime/test/policy/mixin.test.ts +++ b/tests/e2e/orm/policy/mixin.test.ts @@ -1,5 +1,5 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; describe('Abstract models', () => { it('connect test1', async () => { diff --git a/packages/runtime/test/policy/policy-functions.test.ts b/tests/e2e/orm/policy/policy-functions.test.ts similarity index 99% rename from packages/runtime/test/policy/policy-functions.test.ts rename to tests/e2e/orm/policy/policy-functions.test.ts index 2ac094b0..6d70426e 100644 --- a/packages/runtime/test/policy/policy-functions.test.ts +++ b/tests/e2e/orm/policy/policy-functions.test.ts @@ -1,5 +1,5 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; describe('policy functions tests', () => { it('supports contains case-sensitive', async () => { diff --git a/packages/runtime/test/policy/todo-sample.test.ts b/tests/e2e/orm/policy/todo-sample.test.ts similarity index 99% rename from packages/runtime/test/policy/todo-sample.test.ts rename to tests/e2e/orm/policy/todo-sample.test.ts index a53c7466..8164052f 100644 --- a/packages/runtime/test/policy/todo-sample.test.ts +++ b/tests/e2e/orm/policy/todo-sample.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { schema } from '../schemas/todo/schema'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; describe('todo sample tests', () => { it('works with user CRUD', async () => { diff --git a/tests/e2e/prisma-consistency/attributes.test.ts b/tests/e2e/orm/prisma-consistency/attributes.test.ts similarity index 95% rename from tests/e2e/prisma-consistency/attributes.test.ts rename to tests/e2e/orm/prisma-consistency/attributes.test.ts index 0e1b8044..0ff1f7be 100644 --- a/tests/e2e/prisma-consistency/attributes.test.ts +++ b/tests/e2e/orm/prisma-consistency/attributes.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Attributes Validation', () => { diff --git a/tests/e2e/prisma-consistency/basic-models.test.ts b/tests/e2e/orm/prisma-consistency/basic-models.test.ts similarity index 96% rename from tests/e2e/prisma-consistency/basic-models.test.ts rename to tests/e2e/orm/prisma-consistency/basic-models.test.ts index bb075dbe..b44111e8 100644 --- a/tests/e2e/prisma-consistency/basic-models.test.ts +++ b/tests/e2e/orm/prisma-consistency/basic-models.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Basic Models Validation', () => { diff --git a/tests/e2e/prisma-consistency/compound-ids.test.ts b/tests/e2e/orm/prisma-consistency/compound-ids.test.ts similarity index 93% rename from tests/e2e/prisma-consistency/compound-ids.test.ts rename to tests/e2e/orm/prisma-consistency/compound-ids.test.ts index 017d1f53..9797b08d 100644 --- a/tests/e2e/prisma-consistency/compound-ids.test.ts +++ b/tests/e2e/orm/prisma-consistency/compound-ids.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Compound IDs Validation', () => { diff --git a/tests/e2e/prisma-consistency/datasource.test.ts b/tests/e2e/orm/prisma-consistency/datasource.test.ts similarity index 100% rename from tests/e2e/prisma-consistency/datasource.test.ts rename to tests/e2e/orm/prisma-consistency/datasource.test.ts diff --git a/tests/e2e/prisma-consistency/enums.test.ts b/tests/e2e/orm/prisma-consistency/enums.test.ts similarity index 93% rename from tests/e2e/prisma-consistency/enums.test.ts rename to tests/e2e/orm/prisma-consistency/enums.test.ts index c1e6cbc2..f77ae8e6 100644 --- a/tests/e2e/prisma-consistency/enums.test.ts +++ b/tests/e2e/orm/prisma-consistency/enums.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Enums Validation', () => { diff --git a/tests/e2e/prisma-consistency/field-types.test.ts b/tests/e2e/orm/prisma-consistency/field-types.test.ts similarity index 94% rename from tests/e2e/prisma-consistency/field-types.test.ts rename to tests/e2e/orm/prisma-consistency/field-types.test.ts index d6a51931..090cba6c 100644 --- a/tests/e2e/prisma-consistency/field-types.test.ts +++ b/tests/e2e/orm/prisma-consistency/field-types.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, sqliteSchema, } from './test-utils'; diff --git a/tests/e2e/prisma-consistency/relation-validation.test.ts b/tests/e2e/orm/prisma-consistency/relation-validation.test.ts similarity index 93% rename from tests/e2e/prisma-consistency/relation-validation.test.ts rename to tests/e2e/orm/prisma-consistency/relation-validation.test.ts index 7ad878be..d5ef65ca 100644 --- a/tests/e2e/prisma-consistency/relation-validation.test.ts +++ b/tests/e2e/orm/prisma-consistency/relation-validation.test.ts @@ -1,11 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { - ZenStackValidationTester, - createTestDir, - expectValidationSuccess, - expectValidationFailure, - baseSchema, -} from './test-utils'; +import { afterEach, beforeEach, describe, it } from 'vitest'; +import { ZenStackValidationTester, baseSchema, createTestDir, expectValidationFailure } from './test-utils'; describe('Relation Validation Rules', () => { let tester: ZenStackValidationTester; diff --git a/tests/e2e/prisma-consistency/relations-many-to-many.test.ts b/tests/e2e/orm/prisma-consistency/relations-many-to-many.test.ts similarity index 96% rename from tests/e2e/prisma-consistency/relations-many-to-many.test.ts rename to tests/e2e/orm/prisma-consistency/relations-many-to-many.test.ts index 9322948b..eb0b8f91 100644 --- a/tests/e2e/prisma-consistency/relations-many-to-many.test.ts +++ b/tests/e2e/orm/prisma-consistency/relations-many-to-many.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Many-to-Many Relations Validation', () => { diff --git a/tests/e2e/prisma-consistency/relations-one-to-many.test.ts b/tests/e2e/orm/prisma-consistency/relations-one-to-many.test.ts similarity index 100% rename from tests/e2e/prisma-consistency/relations-one-to-many.test.ts rename to tests/e2e/orm/prisma-consistency/relations-one-to-many.test.ts diff --git a/tests/e2e/prisma-consistency/relations-one-to-one.test.ts b/tests/e2e/orm/prisma-consistency/relations-one-to-one.test.ts similarity index 96% rename from tests/e2e/prisma-consistency/relations-one-to-one.test.ts rename to tests/e2e/orm/prisma-consistency/relations-one-to-one.test.ts index 10459241..5107283f 100644 --- a/tests/e2e/prisma-consistency/relations-one-to-one.test.ts +++ b/tests/e2e/orm/prisma-consistency/relations-one-to-one.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('One-to-One Relations Validation', () => { diff --git a/tests/e2e/prisma-consistency/relations-self.test.ts b/tests/e2e/orm/prisma-consistency/relations-self.test.ts similarity index 95% rename from tests/e2e/prisma-consistency/relations-self.test.ts rename to tests/e2e/orm/prisma-consistency/relations-self.test.ts index 6a283685..5ec97ea7 100644 --- a/tests/e2e/prisma-consistency/relations-self.test.ts +++ b/tests/e2e/orm/prisma-consistency/relations-self.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Self Relations Validation', () => { diff --git a/tests/e2e/prisma-consistency/test-utils.ts b/tests/e2e/orm/prisma-consistency/test-utils.ts similarity index 96% rename from tests/e2e/prisma-consistency/test-utils.ts rename to tests/e2e/orm/prisma-consistency/test-utils.ts index 5a53fead..2d125900 100644 --- a/tests/e2e/prisma-consistency/test-utils.ts +++ b/tests/e2e/orm/prisma-consistency/test-utils.ts @@ -21,7 +21,7 @@ export class ZenStackValidationTester { // Get path relative to current test file const currentDir = dirname(fileURLToPath(import.meta.url)); - this.cliPath = join(currentDir, '../node_modules/@zenstackhq/cli/bin/cli'); + this.cliPath = join(currentDir, '../../node_modules/@zenstackhq/cli/bin/cli'); } private setupTestDirectory() { diff --git a/tests/e2e/prisma-consistency/unique-constraints.test.ts b/tests/e2e/orm/prisma-consistency/unique-constraints.test.ts similarity index 95% rename from tests/e2e/prisma-consistency/unique-constraints.test.ts rename to tests/e2e/orm/prisma-consistency/unique-constraints.test.ts index dbb0cbf4..58a4784a 100644 --- a/tests/e2e/prisma-consistency/unique-constraints.test.ts +++ b/tests/e2e/orm/prisma-consistency/unique-constraints.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('Unique Constraints Validation', () => { diff --git a/packages/runtime/test/query-builder/query-builder.test.ts b/tests/e2e/orm/query-builder/query-builder.test.ts similarity index 96% rename from packages/runtime/test/query-builder/query-builder.test.ts rename to tests/e2e/orm/query-builder/query-builder.test.ts index 32890468..23f31e12 100644 --- a/packages/runtime/test/query-builder/query-builder.test.ts +++ b/tests/e2e/orm/query-builder/query-builder.test.ts @@ -1,7 +1,7 @@ import { createId } from '@paralleldrive/cuid2'; import { describe, expect, it } from 'vitest'; import { getSchema } from '../schemas/basic'; -import { createTestClient } from '../utils'; +import { createTestClient } from '@zenstackhq/testtools'; describe('Client API tests', () => { const schema = getSchema('sqlite'); diff --git a/packages/runtime/test/schemas/basic/helper.ts b/tests/e2e/orm/schemas/basic/helper.ts similarity index 100% rename from packages/runtime/test/schemas/basic/helper.ts rename to tests/e2e/orm/schemas/basic/helper.ts diff --git a/packages/runtime/test/schemas/basic/index.ts b/tests/e2e/orm/schemas/basic/index.ts similarity index 100% rename from packages/runtime/test/schemas/basic/index.ts rename to tests/e2e/orm/schemas/basic/index.ts diff --git a/packages/runtime/test/schemas/basic/input.ts b/tests/e2e/orm/schemas/basic/input.ts similarity index 100% rename from packages/runtime/test/schemas/basic/input.ts rename to tests/e2e/orm/schemas/basic/input.ts diff --git a/packages/runtime/test/schemas/basic/models.ts b/tests/e2e/orm/schemas/basic/models.ts similarity index 100% rename from packages/runtime/test/schemas/basic/models.ts rename to tests/e2e/orm/schemas/basic/models.ts diff --git a/packages/runtime/test/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts similarity index 99% rename from packages/runtime/test/schemas/basic/schema.ts rename to tests/e2e/orm/schemas/basic/schema.ts index 8fd0b900..93cfb4df 100644 --- a/packages/runtime/test/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: { type: "sqlite" diff --git a/packages/runtime/test/schemas/basic/schema.zmodel b/tests/e2e/orm/schemas/basic/schema.zmodel similarity index 100% rename from packages/runtime/test/schemas/basic/schema.zmodel rename to tests/e2e/orm/schemas/basic/schema.zmodel diff --git a/packages/runtime/test/schemas/delegate/input.ts b/tests/e2e/orm/schemas/delegate/input.ts similarity index 100% rename from packages/runtime/test/schemas/delegate/input.ts rename to tests/e2e/orm/schemas/delegate/input.ts diff --git a/packages/runtime/test/schemas/delegate/models.ts b/tests/e2e/orm/schemas/delegate/models.ts similarity index 100% rename from packages/runtime/test/schemas/delegate/models.ts rename to tests/e2e/orm/schemas/delegate/models.ts diff --git a/packages/runtime/test/schemas/delegate/schema.ts b/tests/e2e/orm/schemas/delegate/schema.ts similarity index 99% rename from packages/runtime/test/schemas/delegate/schema.ts rename to tests/e2e/orm/schemas/delegate/schema.ts index a15d4f46..ca076fa3 100644 --- a/packages/runtime/test/schemas/delegate/schema.ts +++ b/tests/e2e/orm/schemas/delegate/schema.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: { type: "sqlite" diff --git a/packages/runtime/test/schemas/delegate/schema.zmodel b/tests/e2e/orm/schemas/delegate/schema.zmodel similarity index 100% rename from packages/runtime/test/schemas/delegate/schema.zmodel rename to tests/e2e/orm/schemas/delegate/schema.zmodel diff --git a/packages/runtime/test/schemas/delegate/typecheck.ts b/tests/e2e/orm/schemas/delegate/typecheck.ts similarity index 99% rename from packages/runtime/test/schemas/delegate/typecheck.ts rename to tests/e2e/orm/schemas/delegate/typecheck.ts index 82b3e194..e9c15ed8 100644 --- a/packages/runtime/test/schemas/delegate/typecheck.ts +++ b/tests/e2e/orm/schemas/delegate/typecheck.ts @@ -1,6 +1,6 @@ +import { ZenStackClient } from '@zenstackhq/runtime'; import SQLite from 'better-sqlite3'; import { SqliteDialect } from 'kysely'; -import { ZenStackClient } from '../../../dist'; import { schema } from './schema'; const client = new ZenStackClient(schema, { diff --git a/packages/runtime/test/schemas/name-mapping/input.ts b/tests/e2e/orm/schemas/name-mapping/input.ts similarity index 100% rename from packages/runtime/test/schemas/name-mapping/input.ts rename to tests/e2e/orm/schemas/name-mapping/input.ts diff --git a/packages/runtime/test/schemas/name-mapping/models.ts b/tests/e2e/orm/schemas/name-mapping/models.ts similarity index 100% rename from packages/runtime/test/schemas/name-mapping/models.ts rename to tests/e2e/orm/schemas/name-mapping/models.ts diff --git a/packages/runtime/test/schemas/name-mapping/schema.ts b/tests/e2e/orm/schemas/name-mapping/schema.ts similarity index 97% rename from packages/runtime/test/schemas/name-mapping/schema.ts rename to tests/e2e/orm/schemas/name-mapping/schema.ts index a766ff99..984f36d1 100644 --- a/packages/runtime/test/schemas/name-mapping/schema.ts +++ b/tests/e2e/orm/schemas/name-mapping/schema.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: { type: "sqlite" diff --git a/packages/runtime/test/schemas/name-mapping/schema.zmodel b/tests/e2e/orm/schemas/name-mapping/schema.zmodel similarity index 100% rename from packages/runtime/test/schemas/name-mapping/schema.zmodel rename to tests/e2e/orm/schemas/name-mapping/schema.zmodel diff --git a/packages/runtime/test/schemas/petstore/input.ts b/tests/e2e/orm/schemas/petstore/input.ts similarity index 100% rename from packages/runtime/test/schemas/petstore/input.ts rename to tests/e2e/orm/schemas/petstore/input.ts diff --git a/packages/runtime/test/schemas/petstore/models.ts b/tests/e2e/orm/schemas/petstore/models.ts similarity index 100% rename from packages/runtime/test/schemas/petstore/models.ts rename to tests/e2e/orm/schemas/petstore/models.ts diff --git a/packages/runtime/test/schemas/petstore/schema.ts b/tests/e2e/orm/schemas/petstore/schema.ts similarity index 98% rename from packages/runtime/test/schemas/petstore/schema.ts rename to tests/e2e/orm/schemas/petstore/schema.ts index a2eb7d67..0d208b8e 100644 --- a/packages/runtime/test/schemas/petstore/schema.ts +++ b/tests/e2e/orm/schemas/petstore/schema.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: { type: "sqlite" diff --git a/packages/runtime/test/schemas/petstore/schema.zmodel b/tests/e2e/orm/schemas/petstore/schema.zmodel similarity index 100% rename from packages/runtime/test/schemas/petstore/schema.zmodel rename to tests/e2e/orm/schemas/petstore/schema.zmodel diff --git a/packages/runtime/test/schemas/todo/input.ts b/tests/e2e/orm/schemas/todo/input.ts similarity index 100% rename from packages/runtime/test/schemas/todo/input.ts rename to tests/e2e/orm/schemas/todo/input.ts diff --git a/packages/runtime/test/schemas/todo/models.ts b/tests/e2e/orm/schemas/todo/models.ts similarity index 100% rename from packages/runtime/test/schemas/todo/models.ts rename to tests/e2e/orm/schemas/todo/models.ts diff --git a/packages/runtime/test/schemas/todo/schema.ts b/tests/e2e/orm/schemas/todo/schema.ts similarity index 99% rename from packages/runtime/test/schemas/todo/schema.ts rename to tests/e2e/orm/schemas/todo/schema.ts index f0ae9c26..243e59d6 100644 --- a/packages/runtime/test/schemas/todo/schema.ts +++ b/tests/e2e/orm/schemas/todo/schema.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: { type: "sqlite" diff --git a/packages/runtime/test/schemas/todo/todo.zmodel b/tests/e2e/orm/schemas/todo/todo.zmodel similarity index 100% rename from packages/runtime/test/schemas/todo/todo.zmodel rename to tests/e2e/orm/schemas/todo/todo.zmodel diff --git a/packages/runtime/test/schemas/typing/input.ts b/tests/e2e/orm/schemas/typing/input.ts similarity index 100% rename from packages/runtime/test/schemas/typing/input.ts rename to tests/e2e/orm/schemas/typing/input.ts diff --git a/packages/runtime/test/schemas/typing/models.ts b/tests/e2e/orm/schemas/typing/models.ts similarity index 100% rename from packages/runtime/test/schemas/typing/models.ts rename to tests/e2e/orm/schemas/typing/models.ts diff --git a/packages/runtime/test/schemas/typing/schema.ts b/tests/e2e/orm/schemas/typing/schema.ts similarity index 99% rename from packages/runtime/test/schemas/typing/schema.ts rename to tests/e2e/orm/schemas/typing/schema.ts index 18270ceb..d8287c0e 100644 --- a/packages/runtime/test/schemas/typing/schema.ts +++ b/tests/e2e/orm/schemas/typing/schema.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { type SchemaDef, type OperandExpression, ExpressionUtils } from "../../../dist/schema"; +import { type SchemaDef, type OperandExpression, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: { type: "postgresql" diff --git a/packages/runtime/test/schemas/typing/schema.zmodel b/tests/e2e/orm/schemas/typing/schema.zmodel similarity index 100% rename from packages/runtime/test/schemas/typing/schema.zmodel rename to tests/e2e/orm/schemas/typing/schema.zmodel diff --git a/packages/runtime/test/schemas/typing/typecheck.ts b/tests/e2e/orm/schemas/typing/typecheck.ts similarity index 99% rename from packages/runtime/test/schemas/typing/typecheck.ts rename to tests/e2e/orm/schemas/typing/typecheck.ts index 94961400..f183766f 100644 --- a/packages/runtime/test/schemas/typing/typecheck.ts +++ b/tests/e2e/orm/schemas/typing/typecheck.ts @@ -1,6 +1,6 @@ +import { ZenStackClient } from '@zenstackhq/runtime'; import SQLite from 'better-sqlite3'; import { SqliteDialect } from 'kysely'; -import { ZenStackClient } from '../../../dist'; import { Role, Status, type Identity, type IdentityProvider } from './models'; import { schema } from './schema'; diff --git a/packages/runtime/test/scripts/generate.ts b/tests/e2e/orm/scripts/generate.ts similarity index 76% rename from packages/runtime/test/scripts/generate.ts rename to tests/e2e/orm/scripts/generate.ts index df405295..b4f78958 100644 --- a/packages/runtime/test/scripts/generate.ts +++ b/tests/e2e/orm/scripts/generate.ts @@ -1,7 +1,6 @@ import { loadDocument } from '@zenstackhq/language'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import { glob } from 'glob'; -import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -18,15 +17,11 @@ async function main() { async function generate(schemaPath: string) { const generator = new TsSchemaGenerator(); const outputDir = path.dirname(schemaPath); - const tsPath = path.join(outputDir, 'schema.ts'); const result = await loadDocument(schemaPath); if (!result.success) { throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); } await generator.generate(result.model, outputDir); - const content = fs.readFileSync(tsPath, 'utf-8'); - fs.writeFileSync(tsPath, content.replace(/@zenstackhq\/runtime/g, '../../../dist')); - console.log('TS schema generated at:', outputDir); } main(); diff --git a/tests/e2e/package.json b/tests/e2e/package.json index f1b847a1..c17d7750 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -4,13 +4,31 @@ "private": true, "type": "module", "scripts": { - "test": "vitest run" + "build": "pnpm test:generate && pnpm test:typecheck", + "test:generate": "tsx orm/scripts/generate.ts", + "test:typecheck": "tsc --noEmit", + "test": "vitest run", + "test:sqlite": "TEST_DB_PROVIDER=sqlite vitest run", + "test:postgresql": "TEST_DB_PROVIDER=postgresql vitest run" }, "dependencies": { - "@zenstackhq/testtools": "workspace:*" + "@paralleldrive/cuid2": "^2.2.2", + "@zenstackhq/cli": "workspace:*", + "@zenstackhq/language": "workspace:*", + "@zenstackhq/runtime": "workspace:*", + "@zenstackhq/plugin-policy": "workspace:*", + "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/testtools": "workspace:*", + "better-sqlite3": "catalog:", + "decimal.js": "^10.4.3", + "kysely": "catalog:", + "ulid": "^3.0.0", + "uuid": "^11.0.5" }, "devDependencies": { + "@types/uuid": "^11.0.0", "@zenstackhq/cli": "workspace:*", + "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*" } } diff --git a/packages/runtime/tsconfig.test.json b/tests/e2e/tsconfig.json similarity index 69% rename from packages/runtime/tsconfig.test.json rename to tests/e2e/tsconfig.json index de56945b..6d8efbee 100644 --- a/packages/runtime/tsconfig.test.json +++ b/tests/e2e/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "noEmit": true, "noImplicitAny": false, - "rootDir": "." - }, - "include": ["test/**/*.ts"] + "types": ["@zenstackhq/testtools/types"] + } } diff --git a/tests/e2e/vitest.config.ts b/tests/e2e/vitest.config.ts index 75a9f709..10606ce6 100644 --- a/tests/e2e/vitest.config.ts +++ b/tests/e2e/vitest.config.ts @@ -1,4 +1,11 @@ import base from '@zenstackhq/vitest-config/base'; import { defineConfig, mergeConfig } from 'vitest/config'; -export default mergeConfig(base, defineConfig({})); +export default mergeConfig( + base, + defineConfig({ + test: { + setupFiles: ['@zenstackhq/testtools'], + }, + }), +); diff --git a/tests/regression/tsconfig.json b/tests/regression/tsconfig.json index f3a2dbcb..e0b83aa8 100644 --- a/tests/regression/tsconfig.json +++ b/tests/regression/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "noEmit": true + "noEmit": true, + "types": ["@zenstackhq/testtools/types"] }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/tests/regression/vitest.config.ts b/tests/regression/vitest.config.ts index 75a9f709..10606ce6 100644 --- a/tests/regression/vitest.config.ts +++ b/tests/regression/vitest.config.ts @@ -1,4 +1,11 @@ import base from '@zenstackhq/vitest-config/base'; import { defineConfig, mergeConfig } from 'vitest/config'; -export default mergeConfig(base, defineConfig({})); +export default mergeConfig( + base, + defineConfig({ + test: { + setupFiles: ['@zenstackhq/testtools'], + }, + }), +); diff --git a/vitest.config.ts b/vitest.config.ts index f0b415da..f7268cb1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - projects: ['packages/*'], + projects: ['packages/*', 'tests/*'], }, }); From cc848f26ce3ddc80acd7dd0486c216c46f42152c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:08:30 -0700 Subject: [PATCH 02/17] chore: bump version 3.0.0-beta.9 (#286) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- package.json | 2 +- packages/cli/package.json | 2 +- packages/common-helpers/package.json | 2 +- packages/create-zenstack/package.json | 2 +- packages/dialects/sql.js/package.json | 2 +- packages/eslint-config/package.json | 2 +- packages/language/package.json | 2 +- packages/plugins/policy/package.json | 2 +- packages/runtime/package.json | 2 +- packages/sdk/package.json | 2 +- packages/tanstack-query/package.json | 2 +- packages/testtools/package.json | 2 +- packages/typescript-config/package.json | 2 +- packages/vitest-config/package.json | 2 +- packages/zod/package.json | 2 +- samples/blog/package.json | 2 +- tests/e2e/package.json | 2 +- tests/regression/package.json | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 7fb1f4c9..fc9d2920 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 3338728a..b21a97e4 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-beta.8", + "version": "3.0.0-beta.9", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 48dfd85c..958daf1b 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-beta.8", + "version": "3.0.0-beta.9", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 2f252d55..4288b173 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "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 b9f0a77d..bb1ab3c1 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-beta.8", + "version": "3.0.0-beta.9", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 1401d50b..c1421f8d 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-beta.8", + "version": "3.0.0-beta.9", "type": "module", "private": true, "license": "MIT" diff --git a/packages/language/package.json b/packages/language/package.json index b2b454b7..522cfa20 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-beta.8", + "version": "3.0.0-beta.9", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index eb869945..4c60ab7d 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/plugin-policy", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "ZenStack Policy Plugin", "type": "module", "scripts": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 52af6379..8d2cdc86 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "ZenStack Runtime", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 278b610a..895e42bc 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index f6b036ef..f96af3b9 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-beta.8", + "version": "3.0.0-beta.9", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index d34a1e4c..f01ffe20 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index aeea0618..53638e67 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-beta.8", + "version": "3.0.0-beta.9", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index 1783b010..35f3a279 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-beta.8", + "version": "3.0.0-beta.9", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index f5f6a862..0de8db0e 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "", "type": "module", "main": "index.js", diff --git a/samples/blog/package.json b/samples/blog/package.json index 3c6353b6..9298036c 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "", "main": "index.js", "scripts": { diff --git a/tests/e2e/package.json b/tests/e2e/package.json index c17d7750..bcf2a10f 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index bb98998f..576b870e 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "private": true, "type": "module", "scripts": { From f1a8cefc7e04b768e4bd5f9a07602b0a0bbc9a93 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sat, 4 Oct 2025 22:30:37 -0700 Subject: [PATCH 03/17] chore: migrate some v2 regression cases (#287) * chore: migrate some v2 regression cases * update * update --- packages/testtools/src/schema.ts | 45 +++++++++++++ .../test/v2-migrated/issue-177.test.ts | 25 ++++++++ .../test/v2-migrated/issue-389.test.ts | 15 +++++ .../test/v2-migrated/issue-392.test.ts | 63 +++++++++++++++++++ .../test/v2-migrated/issue-416.test.ts | 20 ++++++ .../test/v2-migrated/issue-509.test.ts | 29 +++++++++ .../test/v2-migrated/issue-609.test.ts | 59 +++++++++++++++++ .../test/v2-migrated/issue-632.test.ts | 25 ++++++++ .../test/v2-migrated/issue-646.test.ts | 11 ++++ 9 files changed, 292 insertions(+) create mode 100644 tests/regression/test/v2-migrated/issue-177.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-389.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-392.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-416.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-509.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-609.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-632.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-646.test.ts diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index ee57a0c5..fd234378 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,10 +1,14 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { loadDocument } from '@zenstackhq/language'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import type { SchemaDef } from '@zenstackhq/sdk/schema'; import { execSync } from 'node:child_process'; +import crypto from 'node:crypto'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { match } from 'ts-pattern'; +import { expect } from 'vitest'; import { createTestProject } from './project'; function makePrelude(provider: 'sqlite' | 'postgresql', dbUrl?: string) { @@ -86,3 +90,44 @@ export async function generateTsSchemaInPlace(schemaPath: string) { await generator.generate(result.model, workDir); return compileAndLoad(workDir); } + +export async function loadSchema(schema: string) { + if (!schema.includes('datasource ')) { + schema = `${makePrelude('sqlite')}\n\n${schema}`; + } + + // create a temp file + const tempFile = path.join(os.tmpdir(), `zenstack-schema-${crypto.randomUUID()}.zmodel`); + fs.writeFileSync(tempFile, schema); + const r = await loadDocument(tempFile); + expect(r).toSatisfy( + (r) => r.success, + `Failed to load schema: ${(r as any).errors?.map((e: any) => e.toString()).join(', ')}`, + ); + invariant(r.success); + return r.model; +} + +export async function loadSchemaWithError(schema: string, error: string | RegExp) { + if (!schema.includes('datasource ')) { + schema = `${makePrelude('sqlite')}\n\n${schema}`; + } + + // create a temp file + const tempFile = path.join(os.tmpdir(), `zenstack-schema-${crypto.randomUUID()}.zmodel`); + fs.writeFileSync(tempFile, schema); + const r = await loadDocument(tempFile); + expect(r.success).toBe(false); + invariant(!r.success); + if (typeof error === 'string') { + expect(r).toSatisfy( + (r) => r.errors.some((e: any) => e.toString().toLowerCase().includes(error.toLowerCase())), + `Expected error message to include "${error}" but got: ${r.errors.map((e: any) => e.toString()).join(', ')}`, + ); + } else { + expect(r).toSatisfy( + (r) => r.errors.some((e: any) => error.test(e)), + `Expected error message to match "${error}" but got: ${r.errors.map((e: any) => e.toString()).join(', ')}`, + ); + } +} diff --git a/tests/regression/test/v2-migrated/issue-177.test.ts b/tests/regression/test/v2-migrated/issue-177.test.ts new file mode 100644 index 00000000..4c8558fb --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-177.test.ts @@ -0,0 +1,25 @@ +import { loadSchemaWithError } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 177', async () => { + await loadSchemaWithError( + ` + model Foo { + id String @id @default(cuid()) + + bar Bar @relation(fields: [barId1, barId2], references: [id1, id2]) + barId1 String? + barId2 String + } + + model Bar { + id1 String @default(cuid()) + id2 String @default(cuid()) + foos Foo[] + + @@id([id1, id2]) + } + `, + 'relation "bar" is not optional, but field "barId1" is optional', + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-389.test.ts b/tests/regression/test/v2-migrated/issue-389.test.ts new file mode 100644 index 00000000..20777b2e --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-389.test.ts @@ -0,0 +1,15 @@ +import { expect, it } from 'vitest'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; + +it('verifies issue 389', async () => { + const db = await createPolicyTestClient(` + model model { + id String @id @default(uuid()) + value Int + @@allow('read', true) + @@allow('create', value > 0) + } + `); + await expect(db.model.create({ data: { value: 0 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { value: 1 } })).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-392.test.ts b/tests/regression/test/v2-migrated/issue-392.test.ts new file mode 100644 index 00000000..bf8acfb4 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-392.test.ts @@ -0,0 +1,63 @@ +import { loadDocument } from '@zenstackhq/language'; +import { it } from 'vitest'; + +it('verifies issue 392', async () => { + await loadDocument( + ` + model M1 { + m2_id String @id + m2 M2 @relation(fields: [m2_id], references: [id]) + } + + model M2 { + id String @id + m1 M1? + } + `, + ); + + await loadDocument( + ` + model M1 { + id String @id + m2_id String @unique + m2 M2 @relation(fields: [m2_id], references: [id]) + } + + model M2 { + id String @id + m1 M1? + } + `, + ); + + await loadDocument( + ` + model M1 { + m2_id String + m2 M2 @relation(fields: [m2_id], references: [id]) + @@id([m2_id]) + } + + model M2 { + id String @id + m1 M1? + } + `, + ); + + await loadDocument( + ` + model M1 { + m2_id String + m2 M2 @relation(fields: [m2_id], references: [id]) + @@unique([m2_id]) + } + + model M2 { + id String @id + m1 M1? + } + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-416.test.ts b/tests/regression/test/v2-migrated/issue-416.test.ts new file mode 100644 index 00000000..75776394 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-416.test.ts @@ -0,0 +1,20 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 416', async () => { + await loadSchema( + ` +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Example { + id Int @id + doubleQuote String @default("s\\"1") + singleQuote String @default('s\\'1') + json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-509.test.ts b/tests/regression/test/v2-migrated/issue-509.test.ts new file mode 100644 index 00000000..ceefb2f5 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-509.test.ts @@ -0,0 +1,29 @@ +import { loadDocument } from '@zenstackhq/language'; +import { it } from 'vitest'; + +it('verifies issue 509', async () => { + await loadDocument( + ` + model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + content String? + published Boolean @default(false) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? + + deleted Boolean @default(false) @omit + + @@allow('all', true) + @@deny('read', deleted) + } + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-609.test.ts b/tests/regression/test/v2-migrated/issue-609.test.ts new file mode 100644 index 00000000..b25314c6 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-609.test.ts @@ -0,0 +1,59 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 609', async () => { + const db = await createPolicyTestClient( + ` +model User { + id String @id @default(cuid()) + comments Comment[] +} + +model Comment { + id String @id @default(cuid()) + parentCommentId String? + replies Comment[] @relation("CommentToComment") + parent Comment? @relation("CommentToComment", fields: [parentCommentId], references: [id]) + comment String + author User @relation(fields: [authorId], references: [id]) + authorId String + + @@allow('read,create', true) + @@allow('update,delete', auth() == author) +} + `, + { usePrismaPush: true }, + ); + + const rawDb = db.$unuseAll(); + + await rawDb.user.create({ + data: { + id: '1', + comments: { + create: { + id: '1', + comment: 'Comment 1', + }, + }, + }, + }); + + await rawDb.user.create({ + data: { + id: '2', + }, + }); + + // connecting a child comment from a different user to a parent comment should succeed + const dbAuth = db.$setAuth({ id: '2' }); + await expect( + dbAuth.comment.create({ + data: { + comment: 'Comment 2', + author: { connect: { id: '2' } }, + parent: { connect: { id: '1' } }, + }, + }), + ).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-632.test.ts b/tests/regression/test/v2-migrated/issue-632.test.ts new file mode 100644 index 00000000..99d251d2 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-632.test.ts @@ -0,0 +1,25 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 632', async () => { + await createTestClient( + ` +enum InventoryUnit { + DIGITAL + FL_OZ + GRAMS + MILLILITERS + OUNCES + UNIT + UNLIMITED +} + +model TwoEnumsOneModelTest { + id String @id @default(cuid()) + inventoryUnit InventoryUnit @default(UNIT) + inputUnit InventoryUnit @default(UNIT) +} +`, + { provider: 'postgresql', usePrismaPush: true }, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-646.test.ts b/tests/regression/test/v2-migrated/issue-646.test.ts new file mode 100644 index 00000000..12daf25b --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-646.test.ts @@ -0,0 +1,11 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 646', async () => { + await loadSchema(` +model Example { + id Int @id + epsilon Decimal @default(0.00000001) +} + `); +}); From 8fbe27d202bceaeff2f2e4723d3edf2b713c3d33 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 8 Oct 2025 13:06:41 -0700 Subject: [PATCH 04/17] feat: implement field validation (#290) * feat: implement field validation * update * update * update --- packages/{ => config}/eslint-config/base.js | 0 .../{ => config}/eslint-config/package.json | 0 .../{ => config}/typescript-config/base.json | 0 .../typescript-config/package.json | 0 .../{ => config}/vitest-config/base.config.js | 0 .../{ => config}/vitest-config/package.json | 0 packages/language/res/stdlib.zmodel | 8 +- packages/runtime/src/client/contract.ts | 2 +- .../src/client/crud/operations/base.ts | 13 +- .../crud/{validator.ts => validator/index.ts} | 79 ++-- .../src/client/crud/validator/utils.ts | 412 ++++++++++++++++++ packages/runtime/src/client/query-utils.ts | 29 +- packages/testtools/src/types.d.ts | 1 + packages/testtools/src/vitest-ext.ts | 46 +- pnpm-lock.yaml | 80 ++-- tests/e2e/orm/client-api/compound-id.test.ts | 2 +- .../e2e/orm/client-api/type-coverage.test.ts | 2 +- .../orm/validation/custom-validation.test.ts | 111 +++++ tests/e2e/orm/validation/nested.test.ts | 41 ++ tests/e2e/orm/validation/toplevel.test.ts | 209 +++++++++ 20 files changed, 940 insertions(+), 95 deletions(-) rename packages/{ => config}/eslint-config/base.js (100%) rename packages/{ => config}/eslint-config/package.json (100%) rename packages/{ => config}/typescript-config/base.json (100%) rename packages/{ => config}/typescript-config/package.json (100%) rename packages/{ => config}/vitest-config/base.config.js (100%) rename packages/{ => config}/vitest-config/package.json (100%) rename packages/runtime/src/client/crud/{validator.ts => validator/index.ts} (95%) create mode 100644 packages/runtime/src/client/crud/validator/utils.ts create mode 100644 tests/e2e/orm/validation/custom-validation.test.ts create mode 100644 tests/e2e/orm/validation/nested.test.ts create mode 100644 tests/e2e/orm/validation/toplevel.test.ts diff --git a/packages/eslint-config/base.js b/packages/config/eslint-config/base.js similarity index 100% rename from packages/eslint-config/base.js rename to packages/config/eslint-config/base.js diff --git a/packages/eslint-config/package.json b/packages/config/eslint-config/package.json similarity index 100% rename from packages/eslint-config/package.json rename to packages/config/eslint-config/package.json diff --git a/packages/typescript-config/base.json b/packages/config/typescript-config/base.json similarity index 100% rename from packages/typescript-config/base.json rename to packages/config/typescript-config/base.json diff --git a/packages/typescript-config/package.json b/packages/config/typescript-config/package.json similarity index 100% rename from packages/typescript-config/package.json rename to packages/config/typescript-config/package.json diff --git a/packages/vitest-config/base.config.js b/packages/config/vitest-config/base.config.js similarity index 100% rename from packages/vitest-config/base.config.js rename to packages/config/vitest-config/base.config.js diff --git a/packages/vitest-config/package.json b/packages/config/vitest-config/package.json similarity index 100% rename from packages/vitest-config/package.json rename to packages/config/vitest-config/package.json diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 52d34ae4..85dc8e91 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -543,22 +543,22 @@ attribute @upper() @@@targetField([StringField]) @@@validation /** * Validates a number field is greater than the given value. */ -attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation +attribute @gt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation /** * Validates a number field is greater than or equal to the given value. */ -attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation +attribute @gte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation /** * Validates a number field is less than the given value. */ -attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation +attribute @lt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation /** * Validates a number field is less than or equal to the given value. */ -attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation +attribute @lte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation /** * Validates the entity with a complex condition. diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index 002f478c..2374bc6e 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -1,4 +1,4 @@ -import type { Decimal } from 'decimal.js'; +import type Decimal from 'decimal.js'; import { type GetModels, type IsDelegateModel, type ProcedureDef, type SchemaDef } from '../schema'; import type { AuthType } from '../schema/auth'; import type { OrUndefinedIf, Simplify, UnwrapTuplePromises } from '../utils/type-utils'; diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 34924952..65bdbbc2 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -131,15 +131,10 @@ export abstract class BaseOperationHandler { model: GetModels, filter: any, ): Promise { - const idFields = requireIdFields(this.schema, model); - const _filter = flattenCompoundUniqueFilters(this.schema, model, filter); - const query = kysely - .selectFrom(model) - .where((eb) => eb.and(_filter)) - .select(idFields.map((f) => kysely.dynamic.ref(f))) - .limit(1) - .modifyEnd(this.makeContextComment({ model, operation: 'read' })); - return this.executeQueryTakeFirst(kysely, query, 'exists'); + return this.readUnique(kysely, model, { + where: filter, + select: this.makeIdSelect(model), + }); } protected async read( diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator/index.ts similarity index 95% rename from packages/runtime/src/client/crud/validator.ts rename to packages/runtime/src/client/crud/validator/index.ts index beb31faf..90cc67e0 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator/index.ts @@ -4,17 +4,18 @@ import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; import { z, ZodSchema, ZodType } from 'zod'; import { + type AttributeApplication, type BuiltinType, type EnumDef, type FieldDef, type GetModels, type ModelDef, type SchemaDef, -} from '../../schema'; -import { enumerate } from '../../utils/enumerate'; -import { extractFields } from '../../utils/object-utils'; -import { formatError } from '../../utils/zod-utils'; -import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../constants'; +} from '../../../schema'; +import { enumerate } from '../../../utils/enumerate'; +import { extractFields } from '../../../utils/object-utils'; +import { formatError } from '../../../utils/zod-utils'; +import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../../constants'; import { type AggregateArgs, type CountArgs, @@ -29,8 +30,8 @@ import { type UpdateManyAndReturnArgs, type UpdateManyArgs, type UpsertArgs, -} from '../crud-types'; -import { InputValidationError, InternalError } from '../errors'; +} from '../../crud-types'; +import { InputValidationError, InternalError } from '../../errors'; import { fieldHasDefaultValue, getDiscriminatorField, @@ -38,7 +39,14 @@ import { getUniqueFields, requireField, requireModel, -} from '../query-utils'; +} from '../../query-utils'; +import { + addBigIntValidation, + addCustomValidation, + addDecimalValidation, + addNumberValidation, + addStringValidation, +} from './utils'; type GetSchemaFunc = (model: GetModels, options: Options) => ZodType; @@ -191,11 +199,14 @@ export class InputValidator { schema = getSchema(model, options); this.schemaCache.set(cacheKey!, schema); } - const { error } = schema.safeParse(args); + const { error, data } = schema.safeParse(args); if (error) { - throw new InputValidationError(`Invalid ${operation} args: ${formatError(error)}`, error); + throw new InputValidationError( + `Invalid ${operation} args for model "${model}": ${formatError(error)}`, + error, + ); } - return args as T; + return data as T; } // #region Find @@ -235,17 +246,28 @@ export class InputValidator { return result; } - private makePrimitiveSchema(type: string) { + private makePrimitiveSchema(type: string, attributes?: AttributeApplication[]) { if (this.schema.typeDefs && type in this.schema.typeDefs) { return this.makeTypeDefSchema(type); } else { return match(type) - .with('String', () => z.string()) - .with('Int', () => z.number().int()) - .with('Float', () => z.number()) + .with('String', () => addStringValidation(z.string(), attributes)) + .with('Int', () => addNumberValidation(z.number().int(), attributes)) + .with('Float', () => addNumberValidation(z.number(), attributes)) .with('Boolean', () => z.boolean()) - .with('BigInt', () => z.union([z.number().int(), z.bigint()])) - .with('Decimal', () => z.union([z.number(), z.instanceof(Decimal), z.string()])) + .with('BigInt', () => + z.union([ + addNumberValidation(z.number().int(), attributes), + addBigIntValidation(z.bigint(), attributes), + ]), + ) + .with('Decimal', () => + z.union([ + addNumberValidation(z.number(), attributes), + addDecimalValidation(z.instanceof(Decimal), attributes), + addDecimalValidation(z.string(), attributes), + ]), + ) .with('DateTime', () => z.union([z.date(), z.string().datetime()])) .with('Bytes', () => z.instanceof(Uint8Array)) .otherwise(() => z.unknown()); @@ -860,7 +882,7 @@ export class InputValidator { uncheckedVariantFields[field] = fieldSchema; } } else { - let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type); + let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); if (fieldDef.array) { fieldSchema = z @@ -889,14 +911,17 @@ export class InputValidator { } }); + const uncheckedCreateSchema = addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes); + const checkedCreateSchema = addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes); + if (!hasRelation) { - return this.orArray(z.strictObject(uncheckedVariantFields), canBeArray); + return this.orArray(uncheckedCreateSchema, canBeArray); } else { return z.union([ - z.strictObject(uncheckedVariantFields), - z.strictObject(checkedVariantFields), - ...(canBeArray ? [z.array(z.strictObject(uncheckedVariantFields))] : []), - ...(canBeArray ? [z.array(z.strictObject(checkedVariantFields))] : []), + uncheckedCreateSchema, + checkedCreateSchema, + ...(canBeArray ? [z.array(uncheckedCreateSchema)] : []), + ...(canBeArray ? [z.array(checkedCreateSchema)] : []), ]); } } @@ -1112,7 +1137,7 @@ export class InputValidator { uncheckedVariantFields[field] = fieldSchema; } } else { - let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type).optional(); + let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes).optional(); if (this.isNumericField(fieldDef)) { fieldSchema = z.union([ @@ -1161,10 +1186,12 @@ export class InputValidator { } }); + const uncheckedUpdateSchema = addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes); + const checkedUpdateSchema = addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes); if (!hasRelation) { - return z.strictObject(uncheckedVariantFields); + return uncheckedUpdateSchema; } else { - return z.union([z.strictObject(uncheckedVariantFields), z.strictObject(checkedVariantFields)]); + return z.union([uncheckedUpdateSchema, checkedUpdateSchema]); } } diff --git a/packages/runtime/src/client/crud/validator/utils.ts b/packages/runtime/src/client/crud/validator/utils.ts new file mode 100644 index 00000000..6b0a17d5 --- /dev/null +++ b/packages/runtime/src/client/crud/validator/utils.ts @@ -0,0 +1,412 @@ +import { invariant } from '@zenstackhq/common-helpers'; +import type { + AttributeApplication, + BinaryExpression, + CallExpression, + Expression, + FieldExpression, + MemberExpression, + UnaryExpression, +} from '@zenstackhq/sdk/schema'; +import Decimal from 'decimal.js'; +import { match, P } from 'ts-pattern'; +import { z } from 'zod'; +import { ExpressionUtils } from '../../../schema'; +import { QueryError } from '../../errors'; + +function getArgValue(expr: Expression | undefined): T | undefined { + if (!expr || !ExpressionUtils.isLiteral(expr)) { + return undefined; + } + return expr.value as T; +} + +export function addStringValidation(schema: z.ZodString, attributes: AttributeApplication[] | undefined): z.ZodSchema { + if (!attributes || attributes.length === 0) { + return schema; + } + + let result = schema; + for (const attr of attributes) { + match(attr.name) + .with('@length', () => { + const min = getArgValue(attr.args?.[0]?.value); + if (min !== undefined) { + result = result.min(min); + } + const max = getArgValue(attr.args?.[1]?.value); + if (max !== undefined) { + result = result.max(max); + } + }) + .with('@startsWith', () => { + const value = getArgValue(attr.args?.[0]?.value); + if (value !== undefined) { + result = result.startsWith(value); + } + }) + .with('@endsWith', () => { + const value = getArgValue(attr.args?.[0]?.value); + if (value !== undefined) { + result = result.endsWith(value); + } + }) + .with('@contains', () => { + const value = getArgValue(attr.args?.[0]?.value); + if (value !== undefined) { + result = result.includes(value); + } + }) + .with('@regex', () => { + const pattern = getArgValue(attr.args?.[0]?.value); + if (pattern !== undefined) { + result = result.regex(new RegExp(pattern)); + } + }) + .with('@email', () => { + result = result.email(); + }) + .with('@datetime', () => { + result = result.datetime(); + }) + .with('@url', () => { + result = result.url(); + }) + .with('@trim', () => { + result = result.trim(); + }) + .with('@lower', () => { + result = result.toLowerCase(); + }) + .with('@upper', () => { + result = result.toUpperCase(); + }); + } + return result; +} + +export function addNumberValidation(schema: z.ZodNumber, attributes: AttributeApplication[] | undefined): z.ZodSchema { + if (!attributes || attributes.length === 0) { + return schema; + } + + let result = schema; + for (const attr of attributes) { + const val = getArgValue(attr.args?.[0]?.value); + if (val === undefined) { + continue; + } + match(attr.name) + .with('@gt', () => { + result = result.gt(val); + }) + .with('@gte', () => { + result = result.gte(val); + }) + .with('@lt', () => { + result = result.lt(val); + }) + .with('@lte', () => { + result = result.lte(val); + }); + } + return result; +} + +export function addBigIntValidation(schema: z.ZodBigInt, attributes: AttributeApplication[] | undefined): z.ZodSchema { + if (!attributes || attributes.length === 0) { + return schema; + } + + let result = schema; + for (const attr of attributes) { + const val = getArgValue(attr.args?.[0]?.value); + if (val === undefined) { + continue; + } + const bigIntVal = BigInt(val); + match(attr.name) + .with('@gt', () => { + result = result.gt(bigIntVal); + }) + .with('@gte', () => { + result = result.gte(bigIntVal); + }) + .with('@lt', () => { + result = result.lt(bigIntVal); + }) + .with('@lte', () => { + result = result.lte(bigIntVal); + }); + } + return result; +} + +export function addDecimalValidation( + schema: z.ZodType | z.ZodString, + attributes: AttributeApplication[] | undefined, +): z.ZodSchema { + let result: z.ZodSchema = schema; + + // parse string to Decimal + if (schema instanceof z.ZodString) { + result = schema + .superRefine((v, ctx) => { + try { + new Decimal(v); + } catch (err) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid decimal: ${err}`, + }); + } + }) + .transform((val) => new Decimal(val)); + } + + // add validations + + function refine(schema: z.ZodSchema, op: 'gt' | 'gte' | 'lt' | 'lte', value: number) { + return schema.superRefine((v, ctx) => { + const base = z.number(); + const { error } = base[op](value).safeParse((v as Decimal).toNumber()); + error?.errors.forEach((e) => { + ctx.addIssue(e); + }); + }); + } + + if (attributes) { + for (const attr of attributes) { + const val = getArgValue(attr.args?.[0]?.value); + if (val === undefined) { + continue; + } + + match(attr.name) + .with('@gt', () => { + result = refine(result, 'gt', val); + }) + .with('@gte', () => { + result = refine(result, 'gte', val); + }) + .with('@lt', () => { + result = refine(result, 'lt', val); + }) + .with('@lte', () => { + result = refine(result, 'lte', val); + }); + } + } + + return result; +} + +export function addCustomValidation(schema: z.ZodSchema, attributes: AttributeApplication[] | undefined): z.ZodSchema { + const attrs = attributes?.filter((a) => a.name === '@@validate'); + if (!attrs || attrs.length === 0) { + return schema; + } + + let result = schema; + for (const attr of attrs) { + const expr = attr.args?.[0]?.value; + if (!expr) { + continue; + } + const message = getArgValue(attr.args?.[1]?.value); + const pathExpr = attr.args?.[2]?.value; + let path: string[] | undefined = undefined; + if (pathExpr && ExpressionUtils.isArray(pathExpr)) { + path = pathExpr.items.map((e) => ExpressionUtils.getLiteralValue(e) as string); + } + result = applyValidation(result, expr, message, path); + } + return result; +} + +function applyValidation( + schema: z.ZodSchema, + expr: Expression, + message: string | undefined, + path: string[] | undefined, +) { + const options: z.CustomErrorParams = {}; + if (message) { + options.message = message; + } + if (path) { + options.path = path; + } + return schema.refine((data) => Boolean(evalExpression(data, expr)), options); +} + +function evalExpression(data: any, expr: Expression): unknown { + return match(expr) + .with({ kind: 'literal' }, (e) => e.value) + .with({ kind: 'array' }, (e) => e.items.map((item) => evalExpression(data, item))) + .with({ kind: 'field' }, (e) => evalField(data, e)) + .with({ kind: 'member' }, (e) => evalMember(data, e)) + .with({ kind: 'unary' }, (e) => evalUnary(data, e)) + .with({ kind: 'binary' }, (e) => evalBinary(data, e)) + .with({ kind: 'call' }, (e) => evalCall(data, e)) + .with({ kind: 'this' }, () => data ?? null) + .with({ kind: 'null' }, () => null) + .exhaustive(); +} + +function evalField(data: any, e: FieldExpression) { + return data?.[e.field] ?? null; +} + +function evalUnary(data: any, expr: UnaryExpression) { + const operand = evalExpression(data, expr.operand); + switch (expr.op) { + case '!': + return !operand; + default: + throw new Error(`Unsupported unary operator: ${expr.op}`); + } +} + +function evalBinary(data: any, expr: BinaryExpression) { + const left = evalExpression(data, expr.left); + const right = evalExpression(data, expr.right); + return match(expr.op) + .with('&&', () => Boolean(left) && Boolean(right)) + .with('||', () => Boolean(left) || Boolean(right)) + .with('==', () => left == right) + .with('!=', () => left != right) + .with('<', () => (left as any) < (right as any)) + .with('<=', () => (left as any) <= (right as any)) + .with('>', () => (left as any) > (right as any)) + .with('>=', () => (left as any) >= (right as any)) + .with('?', () => { + if (!Array.isArray(left)) { + return false; + } + return left.some((item) => item === right); + }) + .with('!', () => { + if (!Array.isArray(left)) { + return false; + } + return left.every((item) => item === right); + }) + .with('^', () => { + if (!Array.isArray(left)) { + return false; + } + return !left.some((item) => item === right); + }) + .with('in', () => { + if (!Array.isArray(right)) { + return false; + } + return right.includes(left); + }) + .exhaustive(); +} + +function evalMember(data: any, expr: MemberExpression) { + let result: any = evalExpression(data, expr.receiver); + for (const member of expr.members) { + if (!result || typeof result !== 'object') { + return undefined; + } + result = result[member]; + } + return result ?? null; +} + +function evalCall(data: any, expr: CallExpression) { + const fieldArg = expr.args?.[0] ? evalExpression(data, expr.args[0]) : undefined; + return ( + match(expr.function) + // string functions + .with('length', (f) => { + if (fieldArg === undefined || fieldArg === null) { + return false; + } + invariant(typeof fieldArg === 'string', `"${f}" first argument must be a string`); + + const min = getArgValue(expr.args?.[1]); + const max = getArgValue(expr.args?.[2]); + if (min !== undefined && fieldArg.length < min) { + return false; + } + if (max !== undefined && fieldArg.length > max) { + return false; + } + return true; + }) + .with(P.union('startsWith', 'endsWith', 'contains'), (f) => { + if (fieldArg === undefined || fieldArg === null) { + return false; + } + invariant(typeof fieldArg === 'string', `"${f}" first argument must be a string`); + invariant(expr.args?.[1], `"${f}" requires a search argument`); + + const search = getArgValue(expr.args?.[1])!; + const caseInsensitive = getArgValue(expr.args?.[2]) ?? false; + + const matcher = (x: string, y: string) => + match(f) + .with('startsWith', () => x.startsWith(y)) + .with('endsWith', () => x.endsWith(y)) + .with('contains', () => x.includes(y)) + .exhaustive(); + return caseInsensitive + ? matcher(fieldArg.toLowerCase(), search.toLowerCase()) + : matcher(fieldArg, search); + }) + .with('regex', (f) => { + if (fieldArg === undefined || fieldArg === null) { + return false; + } + invariant(typeof fieldArg === 'string', `"${f}" first argument must be a string`); + const pattern = getArgValue(expr.args?.[1])!; + invariant(pattern !== undefined, `"${f}" requires a pattern argument`); + return new RegExp(pattern).test(fieldArg); + }) + .with(P.union('email', 'url', 'datetime'), (f) => { + if (fieldArg === undefined || fieldArg === null) { + return false; + } + return z.string()[f]().safeParse(fieldArg).success; + }) + // list functions + .with(P.union('has', 'hasEvery', 'hasSome'), (f) => { + invariant(expr.args?.[1], `${f} requires a search argument`); + if (fieldArg === undefined || fieldArg === null) { + return false; + } + invariant(Array.isArray(fieldArg), `"${f}" first argument must be an array field`); + + const search = evalExpression(data, expr.args?.[1])!; + const matcher = (x: any[], y: any) => + match(f) + .with('has', () => x.some((item) => item === y)) + .with('hasEvery', () => { + invariant(Array.isArray(y), 'hasEvery second argument must be an array'); + return y.every((v) => x.some((item) => item === v)); + }) + .with('hasSome', () => { + invariant(Array.isArray(y), 'hasSome second argument must be an array'); + return y.some((v) => x.some((item) => item === v)); + }) + .exhaustive(); + return matcher(fieldArg, search); + }) + .with('isEmpty', (f) => { + if (fieldArg === undefined || fieldArg === null) { + return false; + } + invariant(Array.isArray(fieldArg), `"${f}" first argument must be an array field`); + return fieldArg.length === 0; + }) + .otherwise(() => { + throw new QueryError(`Unknown function "${expr.function}"`); + }) + ); +} diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index 869d3535..b5107cdf 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -304,16 +304,37 @@ export function flattenCompoundUniqueFilters(schema: SchemaDef, model: string, f return filter; } - const result: any = {}; + const flattenedResult: any = {}; + const restFilter: any = {}; + for (const [key, value] of Object.entries(filter)) { if (compoundUniques.some(({ name }) => name === key)) { // flatten the compound field - Object.assign(result, value); + Object.assign(flattenedResult, value); } else { - result[key] = value; + restFilter[key] = value; + } + } + + if (Object.keys(flattenedResult).length === 0) { + // nothing flattened + return filter; + } else if (Object.keys(restFilter).length === 0) { + // all flattened + return flattenedResult; + } else { + const flattenedKeys = Object.keys(flattenedResult); + const restKeys = Object.keys(restFilter); + if (flattenedKeys.some((k) => restKeys.includes(k))) { + // keys overlap, cannot merge directly, build an AND clause + return { + AND: [flattenedResult, restFilter], + }; + } else { + // safe to merge directly + return { ...flattenedResult, ...restFilter }; } } - return result; } export function ensureArray(value: T | T[]): T[] { diff --git a/packages/testtools/src/types.d.ts b/packages/testtools/src/types.d.ts index b547127c..9f58106f 100644 --- a/packages/testtools/src/types.d.ts +++ b/packages/testtools/src/types.d.ts @@ -7,6 +7,7 @@ interface CustomMatchers { toResolveWithLength: (length: number) => Promise; toBeRejectedNotFound: () => Promise; toBeRejectedByPolicy: (expectedMessages?: string[]) => Promise; + toBeRejectedByValidation: (expectedMessages?: string[]) => Promise; } declare module 'vitest' { diff --git a/packages/testtools/src/vitest-ext.ts b/packages/testtools/src/vitest-ext.ts index 70b5a61b..06b1709b 100644 --- a/packages/testtools/src/vitest-ext.ts +++ b/packages/testtools/src/vitest-ext.ts @@ -1,4 +1,4 @@ -import { NotFoundError, RejectedByPolicyError } from '@zenstackhq/runtime'; +import { InputValidationError, NotFoundError, RejectedByPolicyError } from '@zenstackhq/runtime'; import { expect } from 'vitest'; function isPromise(value: any) { @@ -19,6 +19,18 @@ function expectError(err: any, errorType: any) { } } +function expectErrorMessages(expectedMessages: string[], message: string) { + for (const m of expectedMessages) { + if (!message.includes(m)) { + return { + message: () => `expected message not found in error: ${m}, got message: ${message}`, + pass: false, + }; + } + } + return undefined; +} + expect.extend({ async toResolveTruthy(received: Promise) { if (!isPromise(received)) { @@ -84,14 +96,9 @@ expect.extend({ await received; } catch (err) { if (expectedMessages && err instanceof RejectedByPolicyError) { - const message = err.message || ''; - for (const m of expectedMessages) { - if (!message.includes(m)) { - return { - message: () => `expected message not found in error: ${m}, got message: ${message}`, - pass: false, - }; - } + const r = expectErrorMessages(expectedMessages, err.message || ''); + if (r) { + return r; } } return expectError(err, RejectedByPolicyError); @@ -101,4 +108,25 @@ expect.extend({ pass: false, }; }, + + async toBeRejectedByValidation(received: Promise, expectedMessages?: string[]) { + if (!isPromise(received)) { + return { message: () => 'a promise is expected', pass: false }; + } + try { + await received; + } catch (err) { + if (expectedMessages && err instanceof InputValidationError) { + const r = expectErrorMessages(expectedMessages, err.message || ''); + if (r) { + return r; + } + } + return expectError(err, InputValidationError); + } + return { + message: () => `expected InputValidationError, got no error`, + pass: false, + }; + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c85aa5e..740f983e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: version: 0.2.6 '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime @@ -143,10 +143,10 @@ importers: version: link:../testtools '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../vitest-config + version: link:../config/vitest-config better-sqlite3: specifier: 'catalog:' version: 12.2.0 @@ -158,10 +158,16 @@ importers: devDependencies: '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config + + packages/config/eslint-config: {} + + packages/config/typescript-config: {} + + packages/config/vitest-config: {} packages/create-zenstack: dependencies: @@ -177,10 +183,10 @@ importers: devDependencies: '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config packages/dialects/sql.js: devDependencies: @@ -189,13 +195,13 @@ importers: version: 1.4.9 '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../../eslint-config + version: link:../../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../typescript-config + version: link:../../config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../../vitest-config + version: link:../../config/vitest-config kysely: specifier: 'catalog:' version: 0.27.6 @@ -203,8 +209,6 @@ importers: specifier: ^1.13.0 version: 1.13.0 - packages/eslint-config: {} - packages/ide/vscode: dependencies: '@zenstackhq/language': @@ -225,10 +229,10 @@ importers: version: 1.101.0 '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../../eslint-config + version: link:../../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../typescript-config + version: link:../../config/typescript-config packages/language: dependencies: @@ -253,13 +257,13 @@ importers: version: link:../common-helpers '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../vitest-config + version: link:../config/vitest-config glob: specifier: ^11.0.2 version: 11.0.2 @@ -296,13 +300,13 @@ importers: version: 8.11.11 '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../../eslint-config + version: link:../../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../typescript-config + version: link:../../config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../../vitest-config + version: link:../../config/vitest-config packages/runtime: dependencies: @@ -357,7 +361,7 @@ importers: version: 2.0.7 '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/language': specifier: workspace:* version: link:../language @@ -366,10 +370,10 @@ importers: version: link:../sdk '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../vitest-config + version: link:../config/vitest-config tsx: specifier: ^4.19.2 version: 4.19.2 @@ -397,10 +401,10 @@ importers: devDependencies: '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config decimal.js: specifier: ^10.4.3 version: 10.4.3 @@ -419,10 +423,10 @@ importers: devDependencies: '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config packages/testtools: dependencies: @@ -477,10 +481,10 @@ importers: version: 0.2.6 '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config copyfiles: specifier: ^2.4.1 version: 2.4.1 @@ -488,10 +492,6 @@ importers: specifier: 'catalog:' version: 5.8.3 - packages/typescript-config: {} - - packages/vitest-config: {} - packages/zod: dependencies: '@zenstackhq/runtime': @@ -503,10 +503,10 @@ importers: devDependencies: '@zenstackhq/eslint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../config/eslint-config '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../typescript-config + version: link:../config/typescript-config zod: specifier: ~3.25.0 version: 3.25.76 @@ -531,7 +531,7 @@ importers: version: link:../../packages/cli '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../packages/typescript-config + version: link:../../packages/config/typescript-config prisma: specifier: 'catalog:' version: 6.14.0(typescript@5.8.3) @@ -580,10 +580,10 @@ importers: version: 11.0.0 '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../packages/typescript-config + version: link:../../packages/config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../../packages/vitest-config + version: link:../../packages/config/vitest-config tests/regression: dependencies: @@ -605,10 +605,10 @@ importers: version: link:../../packages/sdk '@zenstackhq/typescript-config': specifier: workspace:* - version: link:../../packages/typescript-config + version: link:../../packages/config/typescript-config '@zenstackhq/vitest-config': specifier: workspace:* - version: link:../../packages/vitest-config + version: link:../../packages/config/vitest-config packages: diff --git a/tests/e2e/orm/client-api/compound-id.test.ts b/tests/e2e/orm/client-api/compound-id.test.ts index b983b045..dc11c253 100644 --- a/tests/e2e/orm/client-api/compound-id.test.ts +++ b/tests/e2e/orm/client-api/compound-id.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; describe('Compound ID tests', () => { describe('to-one relation', () => { diff --git a/tests/e2e/orm/client-api/type-coverage.test.ts b/tests/e2e/orm/client-api/type-coverage.test.ts index 9ce29fce..a0c24880 100644 --- a/tests/e2e/orm/client-api/type-coverage.test.ts +++ b/tests/e2e/orm/client-api/type-coverage.test.ts @@ -1,6 +1,6 @@ +import { createTestClient, getTestDbProvider } from '@zenstackhq/testtools'; import Decimal from 'decimal.js'; import { describe, expect, it } from 'vitest'; -import { createTestClient, getTestDbProvider } from '@zenstackhq/testtools'; describe('Zmodel type coverage tests', () => { it('supports all types - plain', async () => { diff --git a/tests/e2e/orm/validation/custom-validation.test.ts b/tests/e2e/orm/validation/custom-validation.test.ts new file mode 100644 index 00000000..35667e4c --- /dev/null +++ b/tests/e2e/orm/validation/custom-validation.test.ts @@ -0,0 +1,111 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Custom validation tests', () => { + it('works with custom validation', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + str1 String? + str2 String? + str3 String? + str4 String? + str5 String? + int1 Int? + list1 Int[] + list2 Int[] + + @@validate( + (str1 == null || length(str1, 8, 10)) + && (int1 == null || (int1 > 1 && int1 < 4)), + 'invalid fields') + + @@validate(str1 == null || (startsWith(str1, 'a') && endsWith(str1, 'm') && contains(str1, 'b')), 'invalid fields') + + @@validate(str2 == null || regex(str2, '^x.*z$'), 'invalid str2') + + @@validate(str3 == null || email(str3), 'invalid str3') + + @@validate(str4 == null || url(str4), 'invalid str4') + + @@validate(str5 == null || datetime(str5), 'invalid str5') + + @@validate(list1 == null || (has(list1, 1) && hasSome(list1, [2, 3]) && hasEvery(list1, [4, 5])), 'invalid list1') + + @@validate(list2 == null || isEmpty(list2), 'invalid list2', ['x', 'y']) + } + `, + { provider: 'postgresql' }, + ); + + await db.foo.create({ data: { id: 100 } }); + + for (const action of ['create', 'update']) { + const _t = + action === 'create' + ? (data: any) => db.foo.create({ data }) + : (data: any) => db.foo.update({ where: { id: 100 }, data }); + // violates length + await expect(_t({ str1: 'abd@efg.com' })).toBeRejectedByValidation(['invalid fields']); + await expect(_t({ str1: 'a@b.c' })).toBeRejectedByValidation(['invalid fields']); + + // violates int1 > 1 + await expect(_t({ int1: 1 })).toBeRejectedByValidation(['invalid fields']); + + // violates startsWith + await expect(_t({ str1: 'b@cd.com' })).toBeRejectedByValidation(['invalid fields']); + + // violates endsWith + await expect(_t({ str1: 'a@b.gov' })).toBeRejectedByValidation(['invalid fields']); + + // violates contains + await expect(_t({ str1: 'a@cd.com' })).toBeRejectedByValidation(['invalid fields']); + + // violates regex + await expect(_t({ str2: 'xab' })).toBeRejectedByValidation(['invalid str2']); + + // violates email + await expect(_t({ str3: 'not-an-email' })).toBeRejectedByValidation(['invalid str3']); + + // violates url + await expect(_t({ str4: 'not-an-url' })).toBeRejectedByValidation(['invalid str4']); + + // violates datetime + await expect(_t({ str5: 'not-an-datetime' })).toBeRejectedByValidation(['invalid str5']); + + // violates has + await expect(_t({ list1: [2, 3, 4, 5] })).toBeRejectedByValidation(['invalid list1']); + + // violates hasSome + await expect(_t({ list1: [1, 4, 5] })).toBeRejectedByValidation(['invalid list1']); + + // violates hasEvery + await expect(_t({ list1: [1, 2, 3, 4] })).toBeRejectedByValidation(['invalid list1']); + + // violates isEmpty + let thrown = false; + try { + await _t({ list2: [1] }); + } catch (err) { + thrown = true; + expect((err as any).cause.issues[0].path).toEqual(['data', 'x', 'y']); + } + expect(thrown).toBe(true); + + // satisfies all + await expect( + _t({ + str1: 'ab12345m', + str2: 'x...z', + str3: 'ab@c.com', + str4: 'http://a.b.c', + str5: new Date().toISOString(), + int1: 2, + list1: [1, 2, 4, 5], + list2: [], + }), + ).toResolveTruthy(); + } + }); +}); diff --git a/tests/e2e/orm/validation/nested.test.ts b/tests/e2e/orm/validation/nested.test.ts new file mode 100644 index 00000000..0949a503 --- /dev/null +++ b/tests/e2e/orm/validation/nested.test.ts @@ -0,0 +1,41 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Nested field validation tests', () => { + it('works with nested create/update', async () => { + const db = await createTestClient( + ` + model User { + id Int @id @default(autoincrement()) + profile Profile? + } + + model Profile { + id Int @id @default(autoincrement()) + email String @email + user User @relation(fields: [userId], references: [id]) + userId Int @unique + @@validate(contains(email, 'zenstack'), 'email must be a zenstack email') + } + `, + ); + + await db.user.create({ data: { id: 1 } }); + + for (const action of ['create', 'update']) { + const _t = + action === 'create' + ? (data: any) => db.user.update({ where: { id: 1 }, data: { profile: { create: data } } }) + : (data: any) => db.user.update({ where: { id: 1 }, data: { profile: { update: data } } }); + + // violates email + await expect(_t({ email: 'zenstack' })).toBeRejectedByValidation(['Invalid email']); + + // violates custom validation + await expect(_t({ email: 'a@b.com' })).toBeRejectedByValidation(['email must be a zenstack email']); + + // satisfies all + await expect(_t({ email: 'me@zenstack.dev' })).toResolveTruthy(); + } + }); +}); diff --git a/tests/e2e/orm/validation/toplevel.test.ts b/tests/e2e/orm/validation/toplevel.test.ts new file mode 100644 index 00000000..a7d76475 --- /dev/null +++ b/tests/e2e/orm/validation/toplevel.test.ts @@ -0,0 +1,209 @@ +import { createTestClient, loadSchemaWithError } from '@zenstackhq/testtools'; +import Decimal from 'decimal.js'; +import { describe, expect, it } from 'vitest'; + +describe('Toplevel field validation tests', () => { + it('works with string fields', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + str1 String? @length(2, 4) @startsWith('a') @endsWith('b') @contains('m') @regex('b{2}') + str2 String? @email + str3 String? @datetime + str4 String? @url + str5 String? @trim @lower + str6 String? @upper + } + `, + ); + + await db.foo.create({ data: { id: 100 } }); + + for (const action of ['create', 'update', 'upsert', 'updateMany']) { + console.log(`Testing action: ${action}`); + const _t = + action === 'create' + ? (data: any) => db.foo.create({ data }) + : action === 'update' + ? (data: any) => db.foo.update({ where: { id: 100 }, data }) + : action === 'upsert' + ? (data: any) => + db.foo.upsert({ where: { id: 100 }, create: { id: 101, ...data }, update: data }) + : (data: any) => db.foo.updateMany({ where: { id: 100 }, data }); + + // violates @length min + await expect(_t({ str1: 'a' })).toBeRejectedByValidation(); + + // violates @length max + await expect(_t({ str1: 'abcde' })).toBeRejectedByValidation(); + + // violates @startsWith + await expect(_t({ str1: 'bcd' })).toBeRejectedByValidation(); + + // violates @endsWith + await expect(_t({ str1: 'abc' })).toBeRejectedByValidation(); + + // violates @contains + await expect(_t({ str1: 'abz' })).toBeRejectedByValidation(); + + // violates @regex + await expect(_t({ str1: 'amcb' })).toBeRejectedByValidation(); + + // satisfies all + await expect(_t({ str1: 'ambb' })).toResolveTruthy(); + + // violates @email + await expect(_t({ str2: 'not-an-email' })).toBeRejectedByValidation(['Invalid email']); + + // satisfies @email + await expect(_t({ str2: 'test@example.com' })).toResolveTruthy(); + + // violates @datetime + await expect(_t({ str3: 'not-datetime' })).toBeRejectedByValidation(); + + // satisfies @datetime + await expect(_t({ str3: new Date().toISOString() })).toResolveTruthy(); + + // violates @url + await expect(_t({ str4: 'not-a-url' })).toBeRejectedByValidation(); + + // satisfies @url + await expect(_t({ str4: 'https://example.com' })).toResolveTruthy(); + + // test @trim and @lower + if (action !== 'updateMany') { + await expect(_t({ str5: ' AbC ' })).resolves.toMatchObject({ str5: 'abc' }); + } else { + await expect(_t({ str5: ' AbC ' })).resolves.toMatchObject({ count: 1 }); + } + + // test @upper + if (action !== 'updateMany') { + await expect(_t({ str6: 'aBc' })).resolves.toMatchObject({ str6: 'ABC' }); + } else { + await expect(_t({ str6: 'aBc' })).resolves.toMatchObject({ count: 1 }); + } + } + }); + + it('works with number fields', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + int1 Int? @gt(2) @lt(4) + int2 Int? @gte(2) @lte(4) + } + `, + ); + + // violates @gt + await expect(db.foo.create({ data: { int1: 1 } })).toBeRejectedByValidation(); + + // violates @lt + await expect(db.foo.create({ data: { int1: 4 } })).toBeRejectedByValidation(); + + // violates @gte + await expect(db.foo.create({ data: { int2: 1 } })).toBeRejectedByValidation(); + + // violates @lte + await expect(db.foo.create({ data: { int2: 5 } })).toBeRejectedByValidation(); + + // satisfies all + await expect(db.foo.create({ data: { int1: 3, int2: 4 } })).toResolveTruthy(); + }); + + it('works with bigint fields', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + int1 BigInt? @gt(2) @lt(4) + int2 BigInt? @gte(2) @lte(4) + } + `, + ); + + // violates @gt + await expect(db.foo.create({ data: { int1: 1 } })).toBeRejectedByValidation(); + + // violates @lt + await expect(db.foo.create({ data: { int1: 4 } })).toBeRejectedByValidation(); + + // violates @gte + await expect(db.foo.create({ data: { int2: 1n } })).toBeRejectedByValidation(); + + // violates @lte + await expect(db.foo.create({ data: { int2: 5n } })).toBeRejectedByValidation(); + + // satisfies all + await expect(db.foo.create({ data: { int1: 3, int2: 4 } })).toResolveTruthy(); + }); + + it('works with decimal fields', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + int1 Decimal? @gt(2) @lt(4) + int2 Decimal? @gte(2) @lte(4) + } + `, + ); + + // violates @gt + await expect(db.foo.create({ data: { int1: 1 } })).toBeRejectedByValidation(); + + // violates @lt + await expect(db.foo.create({ data: { int1: new Decimal(4) } })).toBeRejectedByValidation(); + + // invalid decimal string + await expect(db.foo.create({ data: { int2: 'f1.2' } })).toBeRejectedByValidation(); + + // violates @gte + await expect(db.foo.create({ data: { int2: '1.1' } })).toBeRejectedByValidation(); + + // violates @lte + await expect(db.foo.create({ data: { int2: '5.12345678' } })).toBeRejectedByValidation(); + + // satisfies all + await expect(db.foo.create({ data: { int1: '3.3', int2: new Decimal(3.9) } })).toResolveTruthy(); + }); + + it('rejects accessing relation fields', async () => { + await loadSchemaWithError( + ` + model Foo { + id Int @id @default(autoincrement()) + bars Bar[] + @@validate(bars != null) + } + + model Bar { + id Int @id @default(autoincrement()) + foo Foo @relation(fields: [fooId], references: [id]) + fooId Int + } + `, + 'cannot use relation fields', + ); + + await loadSchemaWithError( + ` + model Foo { + id Int @id @default(autoincrement()) + bars Bar[] + @@validate(bars.fooId > 0) + } + + model Bar { + id Int @id @default(autoincrement()) + foo Foo @relation(fields: [fooId], references: [id]) + fooId Int + } + `, + 'cannot use relation fields', + ); + }); +}); From 348abee2613c2144907d2031bae8cfdd1572080a Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:54:54 -0700 Subject: [PATCH 05/17] fix: literal function arguments with `@default()` (#288) * fix: literal function arguments with `@default()` * Add test. --------- Co-authored-by: = <=> --- packages/cli/test/ts-schema-gen.test.ts | 62 +++++++++++++++++++++++++ packages/sdk/src/ts-schema-generator.ts | 4 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 18c1e7d9..d29a0c5c 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -360,4 +360,66 @@ model User extends Base { expect(schema.enums).toMatchObject({ Role: expect.any(Object) }); expect(schema.models).toMatchObject({ User: expect.any(Object) }); }); + + it('generates correct default literal function arguments', async () => { + const { schema } = await generateTsSchema(` +model User { + id String @id @default(uuid(7)) +} + `); + + expect(schema.models).toMatchObject({ + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [ + { + name: "@id" + }, + { + name: "@default", + args: [ + { + name: "value", + value: { + kind: "call", + function: "uuid", + args: [ + { + kind: "literal", + value: 7 + } + ] + } + } + ] + } + ], + default: { + kind: "call", + function: "uuid", + args: [ + { + kind: "literal", + value: 7 + } + ] + } + } + }, + idFields: [ + "id" + ], + uniqueFields: { + id: { + type: "String" + } + } + } + }); + }); }); diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 75c0f44a..78ba40e0 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -507,7 +507,9 @@ export class TsSchemaGenerator { ...(defaultValue.args.length > 0 ? [ ts.factory.createArrayLiteralExpression( - defaultValue.args.map((arg) => this.createLiteralNode(arg)), + defaultValue.args.map((arg) => this.createExpressionUtilsCall('literal', [ + this.createLiteralNode(arg) + ])), ), ] : []), From 831cb66896b985fe21c1a833914dcb24657e3b6d Mon Sep 17 00:00:00 2001 From: Daniel Hritcu <134442484+dhritcu@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:19:55 -0400 Subject: [PATCH 06/17] fix: don't inherit inherited map attribute (#292) Co-authored-by: Yiming Cao --- packages/sdk/src/prisma/prisma-schema-generator.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/prisma/prisma-schema-generator.ts b/packages/sdk/src/prisma/prisma-schema-generator.ts index a02aba03..179ee480 100644 --- a/packages/sdk/src/prisma/prisma-schema-generator.ts +++ b/packages/sdk/src/prisma/prisma-schema-generator.ts @@ -171,7 +171,9 @@ export class PrismaSchemaGenerator { } const allAttributes = getAllAttributes(decl); - for (const attr of allAttributes.filter((attr) => this.isPrismaAttribute(attr))) { + for (const attr of allAttributes.filter( + (attr) => this.isPrismaAttribute(attr) && !this.isInheritedMapAttribute(attr, decl), + )) { this.generateContainerAttribute(model, attr); } @@ -185,6 +187,15 @@ export class PrismaSchemaGenerator { this.generateDelegateRelationForConcrete(model, decl); } + private isInheritedMapAttribute(attr: DataModelAttribute, contextModel: DataModel) { + if (attr.$container === contextModel) { + return false; + } + + const attrName = attr.decl.ref?.name ?? attr.decl.$refText; + return attrName === '@@map'; + } + private isPrismaAttribute(attr: DataModelAttribute | DataFieldAttribute) { if (!attr.decl.ref) { return false; From bf805f0cb2a1c5d88591be4de27ab3cf01b85d70 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 9 Oct 2025 19:38:07 -0700 Subject: [PATCH 07/17] test: migrate more migration cases, a few minor fixes (#293) * test: migrate more migration cases, a few minor fixes * update * update * update * update * update * update --- packages/language/package.json | 1 + packages/language/res/stdlib.zmodel | 4 +- packages/language/src/zmodel-scope.ts | 4 +- .../src/client/crud/operations/base.ts | 28 ++- .../src/client/crud/validator/index.ts | 7 +- packages/testtools/src/client.ts | 1 - pnpm-lock.yaml | 3 + tests/e2e/orm/client-api/update.test.ts | 18 +- tests/e2e/orm/policy/crud/read.test.ts | 4 +- tests/e2e/orm/policy/migrated/omit.test.ts | 2 +- tests/regression/package.json | 5 +- .../test/v2-migrated/issue-657.test.ts | 30 +++ .../test/v2-migrated/issue-665.test.ts | 38 ++++ .../test/v2-migrated/issue-674.test.ts | 14 ++ .../test/v2-migrated/issue-689.test.ts | 71 ++++++ .../test/v2-migrated/issue-703.test.ts | 26 +++ .../test/v2-migrated/issue-714.test.ts | 145 ++++++++++++ .../test/v2-migrated/issue-735.test.ts | 19 ++ .../test/v2-migrated/issue-756.test.ts | 31 +++ .../test/v2-migrated/issue-764.test.ts | 46 ++++ .../test/v2-migrated/issue-765.test.ts | 35 +++ .../test/v2-migrated/issue-804.test.ts | 33 +++ .../test/v2-migrated/issue-811.test.ts | 70 ++++++ .../test/v2-migrated/issue-814.test.ts | 40 ++++ .../test/v2-migrated/issue-825.test.ts | 39 ++++ .../test/v2-migrated/issue-864.test.ts | 183 +++++++++++++++ .../test/v2-migrated/issue-866.test.ts | 22 ++ .../test/v2-migrated/issue-925.test.ts | 69 ++++++ .../test/v2-migrated/issue-947.test.ts | 23 ++ .../test/v2-migrated/issue-961.test.ts | 211 ++++++++++++++++++ .../test/v2-migrated/issue-965.test.ts | 53 +++++ .../test/v2-migrated/issue-971.test.ts | 22 ++ .../test/v2-migrated/issue-992.test.ts | 44 ++++ 33 files changed, 1314 insertions(+), 27 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-657.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-665.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-674.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-689.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-703.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-714.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-735.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-756.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-764.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-765.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-804.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-811.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-814.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-825.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-864.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-866.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-925.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-947.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-961.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-965.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-971.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-992.test.ts diff --git a/packages/language/package.json b/packages/language/package.json index 522cfa20..c6b487f2 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -11,6 +11,7 @@ "type": "module", "scripts": { "build": "pnpm langium:generate && tsc --noEmit && tsup-node", + "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "langium:generate": "langium generate", "langium:generate:production": "langium generate --mode=production", diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 85dc8e91..c49f2606 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -676,7 +676,7 @@ attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", " * @param condition: a boolean expression that controls if the operation should be allowed. * @param override: a boolean value that controls if the field-level policy should override the model-level policy. */ -attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?) +// attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?) /** * Defines an access policy that denies a set of operations when the given condition is true. @@ -692,7 +692,7 @@ attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "' * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. * @param condition: a boolean expression that controls if the operation should be denied. */ -attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) +// attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) /** * Checks if the current user can perform the given operation on the given field. diff --git a/packages/language/src/zmodel-scope.ts b/packages/language/src/zmodel-scope.ts index 2fd8b37a..f4c06ef1 100644 --- a/packages/language/src/zmodel-scope.ts +++ b/packages/language/src/zmodel-scope.ts @@ -36,8 +36,8 @@ import { getAuthDecl, getRecursiveBases, isAuthInvocation, - isCollectionPredicate, isBeforeInvocation, + isCollectionPredicate, resolveImportUri, } from './utils'; @@ -75,7 +75,7 @@ export class ZModelScopeComputation extends DefaultScopeComputation { override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes) { super.processNode(node, document, scopes); - if (isDataModel(node)) { + if (isDataModel(node) || isTypeDef(node)) { // add base fields to the scope recursively const bases = getRecursiveBases(node); for (const base of bases) { diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 65bdbbc2..2aca2980 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -939,15 +939,18 @@ export abstract class BaseOperationHandler { combinedWhere = Object.keys(combinedWhere).length > 0 ? { AND: [parentWhere, combinedWhere] } : parentWhere; } - // fill in automatically updated fields const modelDef = this.requireModel(model); let finalData = data; + + // fill in automatically updated fields + const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.updatedAt) { if (finalData === data) { finalData = clone(data); } finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); + autoUpdatedFields.push(fieldName); } } @@ -1027,7 +1030,13 @@ export abstract class BaseOperationHandler { } } - if (Object.keys(updateFields).length === 0) { + let hasFieldUpdate = Object.keys(updateFields).length > 0; + if (hasFieldUpdate) { + // check if only updating auto-updated fields, if so, we can skip the update + hasFieldUpdate = Object.keys(updateFields).some((f) => !autoUpdatedFields.includes(f)); + } + + if (!hasFieldUpdate) { // nothing to update, return the filter so that the caller can identify the entity return combinedWhere; } else { @@ -2073,22 +2082,11 @@ export abstract class BaseOperationHandler { } } - // Given a unique filter of a model, return the entity ids by trying to - // reused the filter if it's a complete id filter (without extra fields) - // otherwise, read the entity by the filter + // Given a unique filter of a model, load the entity and return its id fields private getEntityIds(kysely: ToKysely, model: GetModels, uniqueFilter: any) { - const idFields: string[] = requireIdFields(this.schema, model); - if ( - // all id fields are provided - idFields.every((f) => f in uniqueFilter && uniqueFilter[f] !== undefined) && - // no non-id filter exists - Object.keys(uniqueFilter).every((k) => idFields.includes(k)) - ) { - return uniqueFilter; - } - return this.readUnique(kysely, model, { where: uniqueFilter, + select: this.makeIdSelect(model), }); } diff --git a/packages/runtime/src/client/crud/validator/index.ts b/packages/runtime/src/client/crud/validator/index.ts index 90cc67e0..11d93350 100644 --- a/packages/runtime/src/client/crud/validator/index.ts +++ b/packages/runtime/src/client/crud/validator/index.ts @@ -976,9 +976,14 @@ export class InputValidator { ]) .optional(); + let upsertWhere = this.makeWhereSchema(fieldType, true); + if (!fieldDef.array) { + // to-one relation, can upsert without where clause + upsertWhere = upsertWhere.optional(); + } fields['upsert'] = this.orArray( z.strictObject({ - where: this.makeWhereSchema(fieldType, true), + where: upsertWhere, create: this.makeCreateDataSchema(fieldType, false, withoutFields), update: this.makeUpdateDataSchema(fieldType, withoutFields), }), diff --git a/packages/testtools/src/client.ts b/packages/testtools/src/client.ts index fcb1b1ec..cb3fded5 100644 --- a/packages/testtools/src/client.ts +++ b/packages/testtools/src/client.ts @@ -58,7 +58,6 @@ export async function createTestClient( const provider = options?.provider ?? getTestDbProvider() ?? 'sqlite'; const dbName = options?.dbName ?? getTestDbName(provider); - console.log(`Using provider: ${provider}, db: ${dbName}`); const dbUrl = provider === 'sqlite' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 740f983e..9236661a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -590,6 +590,9 @@ importers: '@zenstackhq/testtools': specifier: workspace:* version: link:../../packages/testtools + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 devDependencies: '@zenstackhq/cli': specifier: workspace:* diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index ab39d706..6f059c5a 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -38,7 +38,8 @@ describe('Client update tests', () => { email: user.email, name: user.name, }); - expect(updated.updatedAt.getTime()).toBeGreaterThan(user.updatedAt.getTime()); + // should not update updatedAt + expect(updated.updatedAt.getTime()).toEqual(user.updatedAt.getTime()); // id as filter updated = await client.user.update({ @@ -114,6 +115,21 @@ describe('Client update tests', () => { ).resolves.toMatchObject({ id: 'user2' }); }); + it('does not update updatedAt if no other scalar fields are updated', async () => { + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; + + await client.user.update({ + where: { id: user.id }, + data: { + posts: { create: { title: 'Post1' } }, + }, + }); + + const updatedUser = await client.user.findUnique({ where: { id: user.id } }); + expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + }); + it('works with numeric incremental update', async () => { await createUser(client, 'u1@test.com', { profile: { create: { id: '1', bio: 'bio' } }, diff --git a/tests/e2e/orm/policy/crud/read.test.ts b/tests/e2e/orm/policy/crud/read.test.ts index d57d6385..652e9f1e 100644 --- a/tests/e2e/orm/policy/crud/read.test.ts +++ b/tests/e2e/orm/policy/crud/read.test.ts @@ -294,7 +294,7 @@ model Bar { await db.$unuseAll().foo.create({ data: { id: 1 } }); await expect(db.foo.findMany()).resolves.toHaveLength(0); - await db.foo.update({ where: { id: 1 }, data: { bar: { create: { id: 1, y: 0 } } } }); + await db.$unuseAll().foo.update({ where: { id: 1 }, data: { bar: { create: { id: 1, y: 0 } } } }); await expect(db.foo.findMany()).resolves.toHaveLength(1); }); @@ -321,7 +321,7 @@ model Bar { await db.$unuseAll().foo.create({ data: { id: 1, bars: { create: [{ id: 1, y: 0 }] } } }); await expect(db.foo.findMany()).resolves.toHaveLength(0); - await db.foo.update({ where: { id: 1 }, data: { bars: { create: { id: 2, y: 1 } } } }); + await db.$unuseAll().foo.update({ where: { id: 1 }, data: { bars: { create: { id: 2, y: 1 } } } }); await expect(db.foo.findMany()).resolves.toHaveLength(1); }); diff --git a/tests/e2e/orm/policy/migrated/omit.test.ts b/tests/e2e/orm/policy/migrated/omit.test.ts index 84761f5b..c32a44f4 100644 --- a/tests/e2e/orm/policy/migrated/omit.test.ts +++ b/tests/e2e/orm/policy/migrated/omit.test.ts @@ -10,7 +10,7 @@ describe('prisma omit', () => { name String profile Profile? age Int - value Int @allow('read', age > 20) + value Int @@allow('all', age > 18) } diff --git a/tests/regression/package.json b/tests/regression/package.json index 576b870e..9bf71b47 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -8,13 +8,14 @@ "test": "pnpm generate && tsc && vitest run" }, "dependencies": { - "@zenstackhq/testtools": "workspace:*" + "@zenstackhq/testtools": "workspace:*", + "decimal.js": "^10.4.3" }, "devDependencies": { "@zenstackhq/cli": "workspace:*", - "@zenstackhq/sdk": "workspace:*", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", + "@zenstackhq/sdk": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*" } diff --git a/tests/regression/test/v2-migrated/issue-657.test.ts b/tests/regression/test/v2-migrated/issue-657.test.ts new file mode 100644 index 00000000..fc73bc31 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-657.test.ts @@ -0,0 +1,30 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import Decimal from 'decimal.js'; +import { expect, it } from 'vitest'; + +// TODO: zod support +it.skip('verifies issue 657', async () => { + const { zodSchemas } = await createTestClient(` +model Foo { + id Int @id @default(autoincrement()) + intNumber Int @gt(0) + floatNumber Float @gt(0) + decimalNumber Decimal @gt(0.1) @lte(10) +} + `); + + const schema = zodSchemas.models.FooUpdateSchema; + expect(schema.safeParse({ intNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ intNumber: 1 }).success).toBeTruthy(); + expect(schema.safeParse({ floatNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ floatNumber: 1.1 }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: '0' }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: new Decimal(0) }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: 11 }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: '11.123456789' }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: new Decimal('11.123456789') }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: 10 }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: '10' }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: new Decimal('10') }).success).toBeTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-665.test.ts b/tests/regression/test/v2-migrated/issue-665.test.ts new file mode 100644 index 00000000..0f5d0457 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-665.test.ts @@ -0,0 +1,38 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 665', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + admin Boolean @default(false) + username String @unique @allow("all", auth() == this) @allow("all", auth().admin) + password String @password @default("") @allow("all", auth() == this) @allow("all", auth().admin) + firstName String @default("") + lastName String @default("") + + @@allow('all', true) +} + `, + ); + + await db.$unuseAll().user.create({ data: { id: 1, username: 'test', password: 'test', admin: true } }); + + // admin + let r = await db.$setAuth({ id: 1, admin: true }).user.findFirst(); + expect(r.username).toEqual('test'); + + // owner + r = await db.$setAuth({ id: 1 }).user.findFirst(); + expect(r.username).toEqual('test'); + + // anonymous + r = await db.$setAuth({ id: 0 }).user.findFirst(); + expect(r.username).toBeUndefined(); + + // non-owner + r = await db.$setAuth({ id: 2 }).user.findFirst(); + expect(r.username).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-674.test.ts b/tests/regression/test/v2-migrated/issue-674.test.ts new file mode 100644 index 00000000..03ef6cec --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-674.test.ts @@ -0,0 +1,14 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 674', async () => { + await loadSchema( + ` +model Foo { + id Int @id +} + +enum MyUnUsedEnum { ABC CDE @@map('my_unused_enum') } + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-689.test.ts b/tests/regression/test/v2-migrated/issue-689.test.ts new file mode 100644 index 00000000..d62922ec --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-689.test.ts @@ -0,0 +1,71 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 689', async () => { + const db = await createPolicyTestClient( + ` + model UserRole { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int + role String + + @@allow('all', true) + } + + model User { + id Int @id @default(autoincrement()) + userRole UserRole[] + deleted Boolean @default(false) + + @@allow('create,read', true) + @@allow('read', auth() == this) + @@allow('read', userRole?[user == auth() && 'Admin' == role]) + @@allow('read', userRole?[user == auth()]) + } + `, + ); + + const rawDb = db.$unuseAll(); + + await rawDb.user.create({ + data: { + id: 1, + userRole: { + create: [ + { id: 1, role: 'Admin' }, + { id: 2, role: 'Student' }, + ], + }, + }, + }); + + await rawDb.user.create({ + data: { + id: 2, + userRole: { + connect: { id: 1 }, + }, + }, + }); + + const c1 = await rawDb.user.count({ + where: { + userRole: { + some: { role: 'Student' }, + }, + NOT: { deleted: true }, + }, + }); + + const c2 = await db.user.count({ + where: { + userRole: { + some: { role: 'Student' }, + }, + NOT: { deleted: true }, + }, + }); + + expect(c1).toEqual(c2); +}); diff --git a/tests/regression/test/v2-migrated/issue-703.test.ts b/tests/regression/test/v2-migrated/issue-703.test.ts new file mode 100644 index 00000000..73fdc7f5 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-703.test.ts @@ -0,0 +1,26 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 703', async () => { + await createPolicyTestClient( + ` + model User { + id Int @id @default(autoincrement()) + name String? + admin Boolean @default(false) + + companiesWorkedFor Company[] + + username String @unique @allow("all", auth() == this) @allow('read', companiesWorkedFor?[owner == auth()]) @allow("all", auth().admin) + } + + model Company { + id Int @id @default(autoincrement()) + name String? + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + } + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-714.test.ts b/tests/regression/test/v2-migrated/issue-714.test.ts new file mode 100644 index 00000000..854a5c19 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-714.test.ts @@ -0,0 +1,145 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 714', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id @default(autoincrement()) + username String @unique + + employedBy CompanyUser[] + properties PropertyUser[] + companies Company[] + + @@allow('all', true) + } + + model Company { + id Int @id @default(autoincrement()) + name String + + companyUsers CompanyUser[] + propertyUsers User[] + properties Property[] + + @@allow('all', true) + } + + model CompanyUser { + company Company @relation(fields: [companyId], references: [id]) + companyId Int + user User @relation(fields: [userId], references: [id]) + userId Int + + dummyField String + + @@id([companyId, userId]) + + @@allow('all', true) + } + + enum PropertyUserRoleType { + Owner + Administrator + } + + model PropertyUserRole { + id Int @id @default(autoincrement()) + type PropertyUserRoleType + + user PropertyUser @relation(fields: [userId], references: [id]) + userId Int + + @@allow('all', true) + } + + model PropertyUser { + id Int @id @default(autoincrement()) + dummyField String + + property Property @relation(fields: [propertyId], references: [id]) + propertyId Int + user User @relation(fields: [userId], references: [id]) + userId Int + + roles PropertyUserRole[] + + @@unique([propertyId, userId]) + + @@allow('all', true) + } + + model Property { + id Int @id @default(autoincrement()) + name String + + users PropertyUser[] + company Company @relation(fields: [companyId], references: [id]) + companyId Int + + @@allow('all', true) + } + `, + { usePrismaPush: true }, + ); + + await db.user.create({ + data: { + username: 'test@example.com', + }, + }); + + await db.company.create({ + data: { + name: 'My Company', + companyUsers: { + create: { + dummyField: '', + user: { + connect: { + id: 1, + }, + }, + }, + }, + propertyUsers: { + connect: { + id: 1, + }, + }, + properties: { + create: [ + { + name: 'Test', + }, + ], + }, + }, + }); + + await db.property.update({ + data: { + users: { + create: { + dummyField: '', + roles: { + createMany: { + data: { + type: 'Owner', + }, + }, + }, + user: { + connect: { + id: 1, + }, + }, + }, + }, + }, + where: { + id: 1, + }, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-735.test.ts b/tests/regression/test/v2-migrated/issue-735.test.ts new file mode 100644 index 00000000..0739f5d8 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-735.test.ts @@ -0,0 +1,19 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 735', async () => { + await loadSchema( + ` + model MyModel { + id String @id @default(cuid()) + view String + import Int + } + + model view { + id String @id @default(cuid()) + name String + } + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-756.test.ts b/tests/regression/test/v2-migrated/issue-756.test.ts new file mode 100644 index 00000000..85b004a6 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-756.test.ts @@ -0,0 +1,31 @@ +import { loadSchemaWithError } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 756', async () => { + await loadSchemaWithError( + ` + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + model User { + id Int @id @default(autoincrement()) + email Int + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + author User? @relation(fields: [authorId], references: [id]) + authorId Int + @@allow('all', auth().posts.authorId == authorId) + } + `, + `Could not resolve reference to MemberAccessTarget named 'authorId'.`, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-764.test.ts b/tests/regression/test/v2-migrated/issue-764.test.ts new file mode 100644 index 00000000..404616fb --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-764.test.ts @@ -0,0 +1,46 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 764', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + + post Post? @relation(fields: [postId], references: [id]) + postId Int? + + @@allow('all', true) +} + +model Post { + id Int @id @default(autoincrement()) + title String + User User[] + + @@allow('all', true) +} + `, + ); + + const user = await db.$unuseAll().user.create({ + data: { name: 'Me' }, + }); + + await db.user.update({ + where: { id: user.id }, + data: { + post: { + upsert: { + create: { + title: 'Hello World', + }, + update: { + title: 'Hello World', + }, + }, + }, + }, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-765.test.ts b/tests/regression/test/v2-migrated/issue-765.test.ts new file mode 100644 index 00000000..f4cc959b --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-765.test.ts @@ -0,0 +1,35 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 765', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + + post Post? @relation(fields: [postId], references: [id]) + postId Int? + + @@allow('all', true) +} + +model Post { + id Int @id @default(autoincrement()) + title String + User User[] + + @@allow('all', true) +} + `, + ); + + const r = await db.user.create({ + data: { + name: 'Me', + post: undefined, + }, + }); + expect(r.name).toBe('Me'); + expect(r.post).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-804.test.ts b/tests/regression/test/v2-migrated/issue-804.test.ts new file mode 100644 index 00000000..fcb1ab4d --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-804.test.ts @@ -0,0 +1,33 @@ +import { loadSchemaWithError } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 804', async () => { + await loadSchemaWithError( + ` + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + model User { + id Int @id @default(autoincrement()) + email Int + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + author User? @relation(fields: [authorId], references: [id]) + authorId Int + published Boolean + + @@allow('all', auth().posts?[published] == 'TRUE') + } + `, + 'incompatible operand types', + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-811.test.ts b/tests/regression/test/v2-migrated/issue-811.test.ts new file mode 100644 index 00000000..384b3d19 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-811.test.ts @@ -0,0 +1,70 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 811', async () => { + const db = await createPolicyTestClient( + ` + model Membership { + id String @id @default(uuid()) + role String @default('STANDARD') + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @unique + + @@auth + @@allow('create,update,delete', auth().role == 'ADMIN') + @@allow('update', auth() == this) + @@allow('read', true) + } + model User { + id String @id @default(uuid()) + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String @unique + memberships Membership[] + + @@allow('create,update,delete', auth().role == 'ADMIN') + @@allow('update', id == auth().userId) + @@allow('read', true) + } + model Profile { + id String @id @default(uuid()) + firstName String + users User[] + + @@allow('create,update,delete', auth().role == 'ADMIN') + @@allow('update', users?[id == auth().userId]) + @@allow('read', true) + } + `, + ); + + const r = await db.$unuseAll().user.create({ + data: { + profile: { + create: { firstName: 'Tom' }, + }, + memberships: { + create: { role: 'STANDARD' }, + }, + }, + include: { + profile: true, + memberships: true, + }, + }); + + const membershipId = r.memberships[0].id; + const userId = r.id; + const authDb = db.$setAuth({ id: membershipId, role: 'ADMIN', userId }); + + const r1 = await authDb.membership.update({ + data: { + role: 'VIP', + user: { update: { data: { profile: { update: { data: { firstName: 'Jerry' } } } } } }, + }, + include: { user: { include: { profile: true } } }, + where: { id: membershipId }, + }); + + expect(r1.role).toBe('VIP'); + expect(r1.user.profile.firstName).toBe('Jerry'); +}); diff --git a/tests/regression/test/v2-migrated/issue-814.test.ts b/tests/regression/test/v2-migrated/issue-814.test.ts new file mode 100644 index 00000000..40a260c9 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-814.test.ts @@ -0,0 +1,40 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 814', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + profile Profile? + + @@allow('all', true) +} + +model Profile { + id Int @id @default(autoincrement()) + name String @allow('read', !private) + private Boolean @default(false) + user User @relation(fields: [userId], references: [id]) + userId Int @unique + + @@allow('all', true) +} + `, + ); + + const user = await db.$unuseAll().user.create({ + data: { profile: { create: { name: 'Foo', private: true } } }, + include: { profile: true }, + }); + + const r = await db.profile.findFirst({ where: { id: user.profile.id } }); + expect(r.name).toBeUndefined(); + + const r1 = await db.user.findFirst({ + where: { id: user.id }, + include: { profile: true }, + }); + expect(r1.profile.name).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-825.test.ts b/tests/regression/test/v2-migrated/issue-825.test.ts new file mode 100644 index 00000000..56079097 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-825.test.ts @@ -0,0 +1,39 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 825', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + role String + + @@allow('read', true) + @@allow('update', auth().id == id || auth().role == 'superadmin' || auth().role == 'admin') + @@deny('update', + (role == 'superadmin' && auth().id != id) + || (role == 'admin' && auth().id != id && auth().role != 'superadmin')) + + @@deny('post-update', + (before().role != role && auth().role != 'admin' && auth().role != 'superadmin') + || (before().role != role && role == 'superadmin') + || (before().role != role && role == 'admin' && auth().role != 'superadmin')) +} + `, + ); + + const admin = await db.$unuseAll().user.create({ + data: { role: 'admin' }, + }); + + const user = await db.$unuseAll().user.create({ + data: { role: 'customer' }, + }); + + const r = await db.$setAuth(admin).user.update({ + where: { id: user.id }, + data: { role: 'staff' }, + }); + + expect(r.role).toEqual('staff'); +}); diff --git a/tests/regression/test/v2-migrated/issue-864.test.ts b/tests/regression/test/v2-migrated/issue-864.test.ts new file mode 100644 index 00000000..0a3cb373 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-864.test.ts @@ -0,0 +1,183 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, it } from 'vitest'; + +describe('Regression for issue 864', () => { + it('safe create', async () => { + const db = await createPolicyTestClient( + ` + model A { + id Int @id @default(autoincrement()) + aValue Int + b B[] + + @@allow('all', aValue > 0) + } + + model B { + id Int @id @default(autoincrement()) + bValue Int + aId Int + a A @relation(fields: [aId], references: [id]) + c C[] + + @@allow('all', bValue > 0) + } + + model C { + id Int @id @default(autoincrement()) + cValue Int + bId Int + b B @relation(fields: [bId], references: [id]) + + @@allow('all', cValue > 0) + } + `, + ); + + await db.$unuseAll().a.create({ + data: { id: 1, aValue: 1, b: { create: { id: 2, bValue: 2 } } }, + include: { b: true }, + }); + + await db.a.update({ + where: { id: 1 }, + data: { + b: { + update: [ + { + where: { id: 2 }, + data: { + c: { + create: [ + { + cValue: 3, + }, + ], + }, + }, + }, + ], + }, + }, + }); + }); + + it('unsafe create nested in to-many', async () => { + const db = await createPolicyTestClient( + ` + model A { + id Int @id @default(autoincrement()) + aValue Int + b B[] + + @@allow('all', aValue > 0) + } + + model B { + id Int @id @default(autoincrement()) + bValue Int + aId Int + a A @relation(fields: [aId], references: [id]) + c C[] + + @@allow('all', bValue > 0) + } + + model C { + id Int @id @default(autoincrement()) + cValue Int + bId Int + b B @relation(fields: [bId], references: [id]) + + @@allow('all', cValue > 0) + } + `, + ); + + await db.$unuseAll().a.create({ + data: { id: 1, aValue: 1, b: { create: { id: 2, bValue: 2 } } }, + include: { b: true }, + }); + + await db.a.update({ + where: { id: 1 }, + data: { + b: { + update: [ + { + where: { id: 2 }, + data: { + c: { + create: [ + { + id: 1, + cValue: 3, + }, + ], + }, + }, + }, + ], + }, + }, + }); + }); + + it('unsafe create nested in to-one', async () => { + const db = await createPolicyTestClient( + ` + model A { + id Int @id @default(autoincrement()) + aValue Int + b B? + + @@allow('all', aValue > 0) + } + + model B { + id Int @id @default(autoincrement()) + bValue Int + aId Int @unique + a A @relation(fields: [aId], references: [id]) + c C[] + + @@allow('all', bValue > 0) + } + + model C { + id Int @id @default(autoincrement()) + cValue Int + bId Int + b B @relation(fields: [bId], references: [id]) + + @@allow('all', cValue > 0) + } + `, + ); + + await db.$unuseAll().a.create({ + data: { id: 1, aValue: 1, b: { create: { id: 2, bValue: 2 } } }, + include: { b: true }, + }); + + await db.a.update({ + where: { id: 1 }, + data: { + b: { + update: { + data: { + c: { + create: [ + { + id: 1, + cValue: 3, + }, + ], + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-866.test.ts b/tests/regression/test/v2-migrated/issue-866.test.ts new file mode 100644 index 00000000..00f83cce --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-866.test.ts @@ -0,0 +1,22 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: zod schema support +it.skip('verifies issue 866', async () => { + const { zodSchemas } = await createTestClient( + ` + model Model { + id Int @id @default(autoincrement()) + a Int @default(100) + b String @default('') + c DateTime @default(now()) + } + `, + ); + + const r = zodSchemas.models.ModelSchema.parse({ id: 1 }); + expect(r.a).toBe(100); + expect(r.b).toBe(''); + expect(r.c).toBeInstanceOf(Date); + expect(r.id).toBe(1); +}); diff --git a/tests/regression/test/v2-migrated/issue-925.test.ts b/tests/regression/test/v2-migrated/issue-925.test.ts new file mode 100644 index 00000000..eede4bcd --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-925.test.ts @@ -0,0 +1,69 @@ +import { loadSchema, loadSchemaWithError } from '@zenstackhq/testtools'; +import { describe, it } from 'vitest'; + +describe('Regression for issue 925', () => { + it('member reference without using this', async () => { + await loadSchemaWithError( + ` + model User { + id Int @id @default(autoincrement()) + company Company[] + test Int + + @@allow('read', auth().company?[staff?[companyId == test]]) + } + + model Company { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int + + staff Staff[] + @@allow('read', true) + } + + model Staff { + id Int @id @default(autoincrement()) + + company Company @relation(fields: [companyId], references: [id]) + companyId Int + + @@allow('read', true) + } + `, + "Could not resolve reference to ReferenceTarget named 'test'.", + ); + }); + + it('reference with this', async () => { + await loadSchema( + ` + model User { + id Int @id @default(autoincrement()) + company Company[] + test Int + + @@allow('read', auth().company?[staff?[companyId == this.test]]) + } + + model Company { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int + + staff Staff[] + @@allow('read', true) + } + + model Staff { + id Int @id @default(autoincrement()) + + company Company @relation(fields: [companyId], references: [id]) + companyId Int + + @@allow('read', true) + } + `, + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-947.test.ts b/tests/regression/test/v2-migrated/issue-947.test.ts new file mode 100644 index 00000000..04a24538 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-947.test.ts @@ -0,0 +1,23 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 947', async () => { + await loadSchema( + ` +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Test { + id String @id + props TestEnum[] @default([]) + } + +enum TestEnum { + A + B +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-961.test.ts b/tests/regression/test/v2-migrated/issue-961.test.ts new file mode 100644 index 00000000..a1e112e5 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-961.test.ts @@ -0,0 +1,211 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 961', () => { + const schema = ` + model User { + id String @id @default(cuid()) + backups UserColumnBackup[] + } + + model UserColumnBackup { + id String @id @default(cuid()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + key String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt() + columns UserColumn[] + @@unique([userId, key]) + @@allow('all', auth().id == userId) + } + + model UserColumn { + id String @id @default(cuid()) + userColumnBackup UserColumnBackup @relation(fields: [userColumnBackupId], references: [id], onDelete: Cascade) + userColumnBackupId String + column String + version Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt() + + @@unique([userColumnBackupId, column]) + @@allow('all', auth().id == userColumnBackup.userId) + @@deny('update,delete', column == 'c2') + } + `; + + it('deleteMany', async () => { + const db = await createPolicyTestClient(schema); + + const user = await db.$unuseAll().user.create({ + data: { + backups: { + create: { + key: 'key1', + columns: { + create: [{ column: 'c1' }, { column: 'c2' }, { column: 'c3' }], + }, + }, + }, + }, + include: { backups: true }, + }); + const backup = user.backups[0]; + + const authDb = db.$setAuth({ id: user.id }); + + // delete with non-existing outer filter + await expect( + authDb.userColumnBackup.update({ + where: { id: 'abc' }, + data: { + columns: { + deleteMany: { + column: 'c1', + }, + }, + }, + }), + ).toBeRejectedNotFound(); + await expect(authDb.userColumn.findMany()).resolves.toHaveLength(3); + + // delete c1 + await authDb.userColumnBackup.update({ + where: { id: backup.id }, + data: { + columns: { + deleteMany: { + column: 'c1', + }, + }, + }, + include: { columns: true }, + }); + await expect(authDb.userColumn.findMany()).resolves.toHaveLength(2); + + // delete c1 again, no change + await authDb.userColumnBackup.update({ + where: { id: backup.id }, + data: { + columns: { + deleteMany: { + column: 'c1', + }, + }, + }, + }); + await expect(authDb.userColumn.findMany()).resolves.toHaveLength(2); + + // delete c2, filtered out by policy + await authDb.userColumnBackup.update({ + where: { id: backup.id }, + data: { + columns: { + deleteMany: { + column: 'c2', + }, + }, + }, + }); + await expect(authDb.userColumn.findMany()).resolves.toHaveLength(2); + + // delete c3, should succeed + await authDb.userColumnBackup.update({ + where: { id: backup.id }, + data: { + columns: { + deleteMany: { + column: 'c3', + }, + }, + }, + }); + await expect(authDb.userColumn.findMany()).resolves.toHaveLength(1); + }); + + it('updateMany', async () => { + const db = await createPolicyTestClient(schema); + + const user = await db.$unuseAll().user.create({ + data: { + backups: { + create: { + key: 'key1', + columns: { + create: [ + { column: 'c1', version: 1 }, + { column: 'c2', version: 2 }, + ], + }, + }, + }, + }, + include: { backups: true }, + }); + const backup = user.backups[0]; + + const authDb = db.$setAuth({ id: user.id }); + + // update with non-existing outer filter + await expect( + authDb.userColumnBackup.update({ + where: { id: 'abc' }, + data: { + columns: { + updateMany: { + where: { column: 'c1' }, + data: { version: { increment: 1 } }, + }, + }, + }, + }), + ).toBeRejectedNotFound(); + await expect(authDb.userColumn.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ column: 'c1', version: 1 }), + expect.objectContaining({ column: 'c2', version: 2 }), + ]), + ); + + // update c1 + await authDb.userColumnBackup.update({ + where: { id: backup.id }, + data: { + columns: { + updateMany: { + where: { column: 'c1' }, + data: { version: { increment: 1 } }, + }, + }, + }, + include: { columns: true }, + }); + await expect(authDb.userColumn.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ column: 'c1', version: 2 }), + expect.objectContaining({ column: 'c2', version: 2 }), + ]), + ); + + // update c2, filtered out by policy + await authDb.userColumnBackup.update({ + where: { id: backup.id }, + data: { + columns: { + updateMany: { + where: { column: 'c2' }, + data: { version: { increment: 1 } }, + }, + }, + }, + include: { columns: true }, + }); + await expect(authDb.userColumn.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ column: 'c1', version: 2 }), + expect.objectContaining({ column: 'c2', version: 2 }), + ]), + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-965.test.ts b/tests/regression/test/v2-migrated/issue-965.test.ts new file mode 100644 index 00000000..b46d767b --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-965.test.ts @@ -0,0 +1,53 @@ +import { loadSchema, loadSchemaWithError } from '@zenstackhq/testtools'; +import { describe, it } from 'vitest'; + +describe('Regression for issue 965', () => { + it('regression1', async () => { + await loadSchema(` + type Base { + id String @id @default(cuid()) + } + + type A { + URL String? @url + } + + type B { + anotherURL String? @url + } + + type C { + oneMoreURL String? @url + } + + model D with Base, A, B { + } + + model E with Base, B, C { + }`); + }); + + it('regression2', async () => { + await loadSchemaWithError( + ` + type A { + URL String? @url + } + + type B { + anotherURL String? @url + } + + type C { + oneMoreURL String? @url + } + + model D with A, B { + } + + model E with B, C { + }`, + 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.', + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-971.test.ts b/tests/regression/test/v2-migrated/issue-971.test.ts new file mode 100644 index 00000000..a20d5bb6 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-971.test.ts @@ -0,0 +1,22 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 971', async () => { + await loadSchema( + ` +type Level1 { + id String @id @default(cuid()) + URL String? + @@validate(URL != null, "URL must be provided") // works +} +type Level2 with Level1 { + @@validate(URL != null, "URL must be provided") // works +} +type Level3 with Level2 { + @@validate(URL != null, "URL must be provided") // doesn't work +} +model Foo with Level3 { +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-992.test.ts b/tests/regression/test/v2-migrated/issue-992.test.ts new file mode 100644 index 00000000..151afd34 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-992.test.ts @@ -0,0 +1,44 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: global omit support +it.skip('regression', async () => { + const db = await createPolicyTestClient( + ` +model Product { + id String @id @default(cuid()) + category Category @relation(fields: [categoryId], references: [id]) + categoryId String + + deleted Int @default(0) @omit + @@deny('read', deleted != 0) + @@allow('all', true) +} + +model Category { + id String @id @default(cuid()) + products Product[] + @@allow('all', true) +} + `, + ); + + await db.$unuseAll().category.create({ + data: { + products: { + create: [ + { + deleted: 0, + }, + { + deleted: 0, + }, + ], + }, + }, + }); + + const category = await db.category.findFirst({ include: { products: true } }); + expect(category.products[0].deleted).toBeUndefined(); + expect(category.products[1].deleted).toBeUndefined(); +}); From 0fa87c15f64f8c8056aeee321529462b89a95859 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sat, 11 Oct 2025 17:52:05 -0700 Subject: [PATCH 08/17] fix(delegate): sort by fields from delegate base (#294) * fix(delegate): sort by fields from delegate base * refactor * update * update * update --- packages/language/src/index.ts | 6 +- packages/plugins/policy/src/policy-handler.ts | 21 +- .../src/client/crud/dialects/base-dialect.ts | 384 +++++++----------- .../src/client/crud/dialects/postgresql.ts | 30 +- .../src/client/crud/dialects/sqlite.ts | 22 +- .../src/client/crud/operations/aggregate.ts | 5 +- .../src/client/crud/operations/base.ts | 20 +- .../src/client/crud/operations/count.ts | 5 +- .../src/client/crud/operations/group-by.ts | 7 +- packages/runtime/src/client/functions.ts | 2 +- packages/testtools/src/schema.ts | 19 +- .../test/v2-migrated/issue-1014.test.ts | 49 +++ .../test/v2-migrated/issue-1058.test.ts | 52 +++ .../test/v2-migrated/issue-1078.test.ts | 53 +++ .../test/v2-migrated/issue-1080.test.ts | 129 ++++++ .../test/v2-migrated/issue-1123.test.ts | 43 ++ .../test/v2-migrated/issue-1135.test.ts | 76 ++++ .../test/v2-migrated/issue-1149.test.ts | 90 ++++ .../test/v2-migrated/issue-1167.test.ts | 19 + .../test/v2-migrated/issue-1179.test.ts | 26 ++ .../test/v2-migrated/issue-1235.test.ts | 39 ++ .../test/v2-migrated/issue-1241.test.ts | 84 ++++ .../test/v2-migrated/issue-1243.test.ts | 52 +++ .../test/v2-migrated/issue-1257.test.ts | 48 +++ .../test/v2-migrated/issue-1265.test.ts | 26 ++ .../test/v2-migrated/issue-1271.test.ts | 188 +++++++++ .../test/v2-migrated/issue-1381.test.ts | 55 +++ .../test/v2-migrated/issue-1388.test.ts | 32 ++ .../test/v2-migrated/issue-1410.test.ts | 143 +++++++ .../test/v2-migrated/issue-1415.test.ts | 21 + .../test/v2-migrated/issue-1416.test.ts | 36 ++ .../test/v2-migrated/issue-1427.test.ts | 40 ++ .../test/v2-migrated/issue-1451.test.ts | 56 +++ .../test/v2-migrated/issue-1454.test.ts | 116 ++++++ .../test/v2-migrated/issue-1466.test.ts | 195 +++++++++ .../test/v2-migrated/issue-1467.test.ts | 44 ++ .../test/v2-migrated/issue-1483.test.ts | 67 +++ .../test/v2-migrated/issue-1487.test.ts | 52 +++ .../test/v2-migrated/issue-764.test.ts | 2 +- 39 files changed, 2056 insertions(+), 298 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-1014.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1058.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1078.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1080.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1123.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1135.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1149.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1167.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1179.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1235.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1241.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1243.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1257.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1265.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1271.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1381.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1388.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1410.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1415.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1416.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1427.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1451.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1454.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1466.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1467.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1483.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1487.test.ts diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index 4b578f31..ab577c7f 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -20,7 +20,7 @@ export class DocumentLoadError extends Error { export async function loadDocument( fileName: string, - pluginModelFiles: string[] = [], + additionalModelFiles: string[] = [], ): Promise< { success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] } > { @@ -50,9 +50,9 @@ export async function loadDocument( URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))), ); - // load plugin model files + // load additional model files const pluginDocs = await Promise.all( - pluginModelFiles.map((file) => + additionalModelFiles.map((file) => services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))), ), ); diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index f6daf04d..9bc6f664 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -127,6 +127,20 @@ export class PolicyHandler extends OperationNodeTransf // --- Post mutation work --- if (hasPostUpdatePolicies && result.rows.length > 0) { + // verify if before-update rows and post-update rows still id-match + if (beforeUpdateInfo) { + invariant(beforeUpdateInfo.rows.length === result.rows.length); + const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); + for (const postRow of result.rows) { + const beforeRow = beforeUpdateInfo.rows.find((r) => idFields.every((f) => r[f] === postRow[f])); + if (!beforeRow) { + throw new QueryError( + 'Before-update and after-update rows do not match by id. If you have post-update policies on a model, updating id fields is not supported.', + ); + } + } + } + // entities updated filter const idConditions = this.buildIdConditions(mutationModel, result.rows); @@ -234,10 +248,15 @@ export class PolicyHandler extends OperationNodeTransf if (!beforeUpdateAccessFields || beforeUpdateAccessFields.length === 0) { return undefined; } + + // combine update's where with policy filter + const policyFilter = this.buildPolicyFilter(model, model, 'update'); + const combinedFilter = where ? conjunction(this.dialect, [where.where, policyFilter]) : policyFilter; + const query: SelectQueryNode = { kind: 'SelectQueryNode', from: FromNode.create([TableNode.create(model)]), - where, + where: WhereNode.create(combinedFilter), selections: [...beforeUpdateAccessFields.map((f) => SelectionNode.create(ColumnNode.create(f)))], }; const result = await proceed(query); diff --git a/packages/runtime/src/client/crud/dialects/base-dialect.ts b/packages/runtime/src/client/crud/dialects/base-dialect.ts index 7357c8f5..642297b8 100644 --- a/packages/runtime/src/client/crud/dialects/base-dialect.ts +++ b/packages/runtime/src/client/crud/dialects/base-dialect.ts @@ -36,6 +36,8 @@ import { } from '../../query-utils'; export abstract class BaseCrudDialect { + protected eb = expressionBuilder(); + constructor( protected readonly schema: Schema, protected readonly options: ClientOptions, @@ -51,9 +53,9 @@ export abstract class BaseCrudDialect { // #region common query builders - buildSelectModel(eb: ExpressionBuilder, model: string, modelAlias: string) { + buildSelectModel(model: string, modelAlias: string) { const modelDef = requireModel(this.schema, model); - let result = eb.selectFrom(model === modelAlias ? model : `${model} as ${modelAlias}`); + let result = this.eb.selectFrom(model === modelAlias ? model : `${model} as ${modelAlias}`); // join all delegate bases let joinBase = modelDef.baseModel; while (joinBase) { @@ -73,7 +75,7 @@ export abstract class BaseCrudDialect { // where if (args.where) { - result = result.where((eb) => this.buildFilter(eb, model, modelAlias, args?.where)); + result = result.where(() => this.buildFilter(model, modelAlias, args?.where)); } // skip && take @@ -112,21 +114,16 @@ export abstract class BaseCrudDialect { return result; } - buildFilter( - eb: ExpressionBuilder, - model: string, - modelAlias: string, - where: boolean | object | undefined, - ) { + buildFilter(model: string, modelAlias: string, where: boolean | object | undefined) { if (where === true || where === undefined) { - return this.true(eb); + return this.true(); } if (where === false) { - return this.false(eb); + return this.false(); } - let result = this.true(eb); + let result = this.true(); const _where = flattenCompoundUniqueFilters(this.schema, model, where); for (const [key, payload] of Object.entries(_where)) { @@ -139,33 +136,28 @@ export abstract class BaseCrudDialect { } if (this.isLogicalCombinator(key)) { - result = this.and(eb, result, this.buildCompositeFilter(eb, model, modelAlias, key, payload)); + result = this.and(result, this.buildCompositeFilter(model, modelAlias, key, payload)); continue; } const fieldDef = requireField(this.schema, model, key); if (fieldDef.relation) { - result = this.and(eb, result, this.buildRelationFilter(eb, model, modelAlias, key, fieldDef, payload)); + result = this.and(result, this.buildRelationFilter(model, modelAlias, key, fieldDef, payload)); } else { // if the field is from a base model, build a reference from that model - const fieldRef = this.fieldRef( - fieldDef.originModel ?? model, - key, - eb, - fieldDef.originModel ?? modelAlias, - ); + const fieldRef = this.fieldRef(fieldDef.originModel ?? model, key, fieldDef.originModel ?? modelAlias); if (fieldDef.array) { - result = this.and(eb, result, this.buildArrayFilter(eb, fieldRef, fieldDef, payload)); + result = this.and(result, this.buildArrayFilter(fieldRef, fieldDef, payload)); } else { - result = this.and(eb, result, this.buildPrimitiveFilter(eb, fieldRef, fieldDef, payload)); + result = this.and(result, this.buildPrimitiveFilter(fieldRef, fieldDef, payload)); } } } // call expression builder and combine the results if ('$expr' in _where && typeof _where['$expr'] === 'function') { - result = this.and(eb, result, _where['$expr'](eb)); + result = this.and(result, _where['$expr'](this.eb)); } return result; @@ -183,9 +175,8 @@ export abstract class BaseCrudDialect { const orderByItems = ensureArray(_orderBy).flatMap((obj) => Object.entries(obj)); - const eb = expressionBuilder(); const subQueryAlias = `${model}$cursor$sub`; - const cursorFilter = this.buildFilter(eb, model, subQueryAlias, cursor); + const cursorFilter = this.buildFilter(model, subQueryAlias, cursor); let result = query; const filters: ExpressionWrapper[] = []; @@ -198,17 +189,17 @@ export abstract class BaseCrudDialect { const _order = negateOrderBy ? (order === 'asc' ? 'desc' : 'asc') : order; const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '='; andFilters.push( - eb( - eb.ref(`${modelAlias}.${field}`), + this.eb( + this.eb.ref(`${modelAlias}.${field}`), op, - this.buildSelectModel(eb, model, subQueryAlias) + this.buildSelectModel(model, subQueryAlias) .select(`${subQueryAlias}.${field}`) .where(cursorFilter), ), ); } - filters.push(eb.and(andFilters)); + filters.push(this.eb.and(andFilters)); } result = result.where((eb) => eb.or(filters)); @@ -221,7 +212,6 @@ export abstract class BaseCrudDialect { } protected buildCompositeFilter( - eb: ExpressionBuilder, model: string, modelAlias: string, key: (typeof LOGICAL_COMBINATORS)[number], @@ -229,38 +219,24 @@ export abstract class BaseCrudDialect { ): Expression { return match(key) .with('AND', () => - this.and( - eb, - ...enumerate(payload).map((subPayload) => this.buildFilter(eb, model, modelAlias, subPayload)), - ), + this.and(...enumerate(payload).map((subPayload) => this.buildFilter(model, modelAlias, subPayload))), ) .with('OR', () => - this.or( - eb, - ...enumerate(payload).map((subPayload) => this.buildFilter(eb, model, modelAlias, subPayload)), - ), + this.or(...enumerate(payload).map((subPayload) => this.buildFilter(model, modelAlias, subPayload))), ) - .with('NOT', () => eb.not(this.buildCompositeFilter(eb, model, modelAlias, 'AND', payload))) + .with('NOT', () => this.eb.not(this.buildCompositeFilter(model, modelAlias, 'AND', payload))) .exhaustive(); } - private buildRelationFilter( - eb: ExpressionBuilder, - model: string, - modelAlias: string, - field: string, - fieldDef: FieldDef, - payload: any, - ) { + private buildRelationFilter(model: string, modelAlias: string, field: string, fieldDef: FieldDef, payload: any) { if (!fieldDef.array) { - return this.buildToOneRelationFilter(eb, model, modelAlias, field, fieldDef, payload); + return this.buildToOneRelationFilter(model, modelAlias, field, fieldDef, payload); } else { - return this.buildToManyRelationFilter(eb, model, modelAlias, field, fieldDef, payload); + return this.buildToManyRelationFilter(model, modelAlias, field, fieldDef, payload); } } private buildToOneRelationFilter( - eb: ExpressionBuilder, model: string, modelAlias: string, field: string, @@ -272,10 +248,10 @@ export abstract class BaseCrudDialect { if (ownedByModel && !fieldDef.originModel) { // can be short-circuited to FK null check - return this.and(eb, ...keyPairs.map(({ fk }) => eb(sql.ref(`${modelAlias}.${fk}`), 'is', null))); + return this.and(...keyPairs.map(({ fk }) => this.eb(sql.ref(`${modelAlias}.${fk}`), 'is', null))); } else { // translate it to `{ is: null }` filter - return this.buildToOneRelationFilter(eb, model, modelAlias, field, fieldDef, { is: null }); + return this.buildToOneRelationFilter(model, modelAlias, field, fieldDef, { is: null }); } } @@ -290,10 +266,10 @@ export abstract class BaseCrudDialect { ); const filterResultField = `${field}$filter`; - const joinSelect = eb + const joinSelect = this.eb .selectFrom(`${fieldDef.type} as ${joinAlias}`) - .where(() => this.and(eb, ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))))) - .select(() => eb.fn.count(eb.lit(1)).as(filterResultField)); + .where(() => this.and(...joinPairs.map(([left, right]) => this.eb(sql.ref(left), '=', sql.ref(right))))) + .select(() => this.eb.fn.count(this.eb.lit(1)).as(filterResultField)); const conditions: Expression[] = []; @@ -301,12 +277,12 @@ export abstract class BaseCrudDialect { if ('is' in payload) { if (payload.is === null) { // check if not found - conditions.push(eb(joinSelect, '=', 0)); + conditions.push(this.eb(joinSelect, '=', 0)); } else { // check if found conditions.push( - eb( - joinSelect.where(() => this.buildFilter(eb, fieldDef.type, joinAlias, payload.is)), + this.eb( + joinSelect.where(() => this.buildFilter(fieldDef.type, joinAlias, payload.is)), '>', 0, ), @@ -317,16 +293,15 @@ export abstract class BaseCrudDialect { if ('isNot' in payload) { if (payload.isNot === null) { // check if found - conditions.push(eb(joinSelect, '>', 0)); + conditions.push(this.eb(joinSelect, '>', 0)); } else { conditions.push( this.or( - eb, // is null - eb(joinSelect, '=', 0), + this.eb(joinSelect, '=', 0), // found one that matches the filter - eb( - joinSelect.where(() => this.buildFilter(eb, fieldDef.type, joinAlias, payload.isNot)), + this.eb( + joinSelect.where(() => this.buildFilter(fieldDef.type, joinAlias, payload.isNot)), '=', 0, ), @@ -336,19 +311,18 @@ export abstract class BaseCrudDialect { } } else { conditions.push( - eb( - joinSelect.where(() => this.buildFilter(eb, fieldDef.type, joinAlias, payload)), + this.eb( + joinSelect.where(() => this.buildFilter(fieldDef.type, joinAlias, payload)), '>', 0, ), ); } - return this.and(eb, ...conditions); + return this.and(...conditions); } private buildToManyRelationFilter( - eb: ExpressionBuilder, model: string, modelAlias: string, field: string, @@ -357,7 +331,7 @@ export abstract class BaseCrudDialect { ) { // null check needs to be converted to fk "is null" checks if (payload === null) { - return eb(sql.ref(`${modelAlias}.${field}`), 'is', null); + return this.eb(sql.ref(`${modelAlias}.${field}`), 'is', null); } const relationModel = fieldDef.type; @@ -391,17 +365,15 @@ export abstract class BaseCrudDialect { } else { const relationKeyPairs = getRelationForeignKeyFieldPairs(this.schema, model, field); - let result = this.true(eb); + let result = this.true(); for (const { fk, pk } of relationKeyPairs.keyPairs) { if (relationKeyPairs.ownedByModel) { result = this.and( - eb, result, eb(sql.ref(`${modelAlias}.${fk}`), '=', sql.ref(`${relationFilterSelectAlias}.${pk}`)), ); } else { result = this.and( - eb, result, eb(sql.ref(`${modelAlias}.${pk}`), '=', sql.ref(`${relationFilterSelectAlias}.${fk}`)), ); @@ -411,7 +383,7 @@ export abstract class BaseCrudDialect { } }; - let result = this.true(eb); + let result = this.true(); for (const [key, subPayload] of Object.entries(payload)) { if (!subPayload) { @@ -421,15 +393,12 @@ export abstract class BaseCrudDialect { switch (key) { case 'some': { result = this.and( - eb, result, - eb( - this.buildSelectModel(eb, relationModel, relationFilterSelectAlias) - .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) - .where(buildPkFkWhereRefs(eb)) - .where((eb1) => - this.buildFilter(eb1, relationModel, relationFilterSelectAlias, subPayload), - ), + this.eb( + this.buildSelectModel(relationModel, relationFilterSelectAlias) + .select(() => this.eb.fn.count(this.eb.lit(1)).as('$count')) + .where(buildPkFkWhereRefs(this.eb)) + .where(() => this.buildFilter(relationModel, relationFilterSelectAlias, subPayload)), '>', 0, ), @@ -439,16 +408,13 @@ export abstract class BaseCrudDialect { case 'every': { result = this.and( - eb, result, - eb( - this.buildSelectModel(eb, relationModel, relationFilterSelectAlias) + this.eb( + this.buildSelectModel(relationModel, relationFilterSelectAlias) .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) - .where(buildPkFkWhereRefs(eb)) - .where((eb1) => - eb1.not( - this.buildFilter(eb1, relationModel, relationFilterSelectAlias, subPayload), - ), + .where(buildPkFkWhereRefs(this.eb)) + .where(() => + this.eb.not(this.buildFilter(relationModel, relationFilterSelectAlias, subPayload)), ), '=', 0, @@ -459,15 +425,12 @@ export abstract class BaseCrudDialect { case 'none': { result = this.and( - eb, result, - eb( - this.buildSelectModel(eb, relationModel, relationFilterSelectAlias) - .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) - .where(buildPkFkWhereRefs(eb)) - .where((eb1) => - this.buildFilter(eb1, relationModel, relationFilterSelectAlias, subPayload), - ), + this.eb( + this.buildSelectModel(relationModel, relationFilterSelectAlias) + .select(() => this.eb.fn.count(this.eb.lit(1)).as('$count')) + .where(buildPkFkWhereRefs(this.eb)) + .where(() => this.buildFilter(relationModel, relationFilterSelectAlias, subPayload)), '=', 0, ), @@ -480,12 +443,7 @@ export abstract class BaseCrudDialect { return result; } - private buildArrayFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - fieldDef: FieldDef, - payload: any, - ) { + private buildArrayFilter(fieldRef: Expression, fieldDef: FieldDef, payload: any) { const clauses: Expression[] = []; const fieldType = fieldDef.type as BuiltinType; @@ -498,27 +456,27 @@ export abstract class BaseCrudDialect { switch (key) { case 'equals': { - clauses.push(this.buildLiteralFilter(eb, fieldRef, fieldType, eb.val(value))); + clauses.push(this.buildLiteralFilter(fieldRef, fieldType, this.eb.val(value))); break; } case 'has': { - clauses.push(eb(fieldRef, '@>', eb.val([value]))); + clauses.push(this.eb(fieldRef, '@>', this.eb.val([value]))); break; } case 'hasEvery': { - clauses.push(eb(fieldRef, '@>', eb.val(value))); + clauses.push(this.eb(fieldRef, '@>', this.eb.val(value))); break; } case 'hasSome': { - clauses.push(eb(fieldRef, '&&', eb.val(value))); + clauses.push(this.eb(fieldRef, '&&', this.eb.val(value))); break; } case 'isEmpty': { - clauses.push(eb(fieldRef, value === true ? '=' : '!=', eb.val([]))); + clauses.push(this.eb(fieldRef, value === true ? '=' : '!=', this.eb.val([]))); break; } @@ -528,27 +486,27 @@ export abstract class BaseCrudDialect { } } - return this.and(eb, ...clauses); + return this.and(...clauses); } - buildPrimitiveFilter(eb: ExpressionBuilder, fieldRef: Expression, fieldDef: FieldDef, payload: any) { + buildPrimitiveFilter(fieldRef: Expression, fieldDef: FieldDef, payload: any) { if (payload === null) { - return eb(fieldRef, 'is', null); + return this.eb(fieldRef, 'is', null); } if (isEnum(this.schema, fieldDef.type)) { - return this.buildEnumFilter(eb, fieldRef, fieldDef, payload); + return this.buildEnumFilter(fieldRef, fieldDef, payload); } return ( match(fieldDef.type as BuiltinType) - .with('String', () => this.buildStringFilter(eb, fieldRef, payload)) + .with('String', () => this.buildStringFilter(fieldRef, payload)) .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.buildNumberFilter(eb, fieldRef, type, payload), + this.buildNumberFilter(fieldRef, type, payload), ) - .with('Boolean', () => this.buildBooleanFilter(eb, fieldRef, payload)) - .with('DateTime', () => this.buildDateTimeFilter(eb, fieldRef, payload)) - .with('Bytes', () => this.buildBytesFilter(eb, fieldRef, payload)) + .with('Boolean', () => this.buildBooleanFilter(fieldRef, payload)) + .with('DateTime', () => this.buildDateTimeFilter(fieldRef, payload)) + .with('Bytes', () => this.buildBytesFilter(fieldRef, payload)) // TODO: JSON filters .with('Json', () => { throw new InternalError('JSON filters are not supported yet'); @@ -560,12 +518,11 @@ export abstract class BaseCrudDialect { ); } - private buildLiteralFilter(eb: ExpressionBuilder, lhs: Expression, type: BuiltinType, rhs: unknown) { - return eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type, false) : rhs); + private buildLiteralFilter(lhs: Expression, type: BuiltinType, rhs: unknown) { + return this.eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type, false) : rhs); } private buildStandardFilter( - eb: ExpressionBuilder, type: BuiltinType, payload: any, lhs: Expression, @@ -577,7 +534,7 @@ export abstract class BaseCrudDialect { ) { if (payload === null || !isPlainObject(payload)) { return { - conditions: [this.buildLiteralFilter(eb, lhs, type, payload)], + conditions: [this.buildLiteralFilter(lhs, type, payload)], consumedKeys: [], }; } @@ -594,41 +551,40 @@ export abstract class BaseCrudDialect { } const rhs = Array.isArray(value) ? value.map(getRhs) : getRhs(value); const condition = match(op) - .with('equals', () => (rhs === null ? eb(lhs, 'is', null) : eb(lhs, '=', rhs))) + .with('equals', () => (rhs === null ? this.eb(lhs, 'is', null) : this.eb(lhs, '=', rhs))) .with('in', () => { invariant(Array.isArray(rhs), 'right hand side must be an array'); if (rhs.length === 0) { - return this.false(eb); + return this.false(); } else { - return eb(lhs, 'in', rhs); + return this.eb(lhs, 'in', rhs); } }) .with('notIn', () => { invariant(Array.isArray(rhs), 'right hand side must be an array'); if (rhs.length === 0) { - return this.true(eb); + return this.true(); } else { - return eb.not(eb(lhs, 'in', rhs)); + return this.eb.not(this.eb(lhs, 'in', rhs)); } }) - .with('lt', () => eb(lhs, '<', rhs)) - .with('lte', () => eb(lhs, '<=', rhs)) - .with('gt', () => eb(lhs, '>', rhs)) - .with('gte', () => eb(lhs, '>=', rhs)) - .with('not', () => eb.not(recurse(value))) + .with('lt', () => this.eb(lhs, '<', rhs)) + .with('lte', () => this.eb(lhs, '<=', rhs)) + .with('gt', () => this.eb(lhs, '>', rhs)) + .with('gte', () => this.eb(lhs, '>=', rhs)) + .with('not', () => this.eb.not(recurse(value))) // aggregations .with(P.union(...AGGREGATE_OPERATORS), (op) => { const innerResult = this.buildStandardFilter( - eb, type, value, - aggregate(eb, lhs, op), + aggregate(this.eb, lhs, op), getRhs, recurse, throwIfInvalid, ); consumedKeys.push(...innerResult.consumedKeys); - return this.and(eb, ...innerResult.conditions); + return this.and(...innerResult.conditions); }) .otherwise(() => { if (throwIfInvalid) { @@ -647,23 +603,18 @@ export abstract class BaseCrudDialect { return { conditions, consumedKeys }; } - private buildStringFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - payload: StringFilter, - ) { + private buildStringFilter(fieldRef: Expression, payload: StringFilter) { let mode: 'default' | 'insensitive' | undefined; if (payload && typeof payload === 'object' && 'mode' in payload) { mode = payload.mode; } const { conditions, consumedKeys } = this.buildStandardFilter( - eb, 'String', payload, - mode === 'insensitive' ? eb.fn('lower', [fieldRef]) : fieldRef, - (value) => this.prepStringCasing(eb, value, mode), - (value) => this.buildStringFilter(eb, fieldRef, value as StringFilter), + mode === 'insensitive' ? this.eb.fn('lower', [fieldRef]) : fieldRef, + (value) => this.prepStringCasing(this.eb, value, mode), + (value) => this.buildStringFilter(fieldRef, value as StringFilter), ); if (payload && typeof payload === 'object') { @@ -676,18 +627,18 @@ export abstract class BaseCrudDialect { const condition = match(key) .with('contains', () => mode === 'insensitive' - ? eb(fieldRef, 'ilike', sql.val(`%${value}%`)) - : eb(fieldRef, 'like', sql.val(`%${value}%`)), + ? this.eb(fieldRef, 'ilike', sql.val(`%${value}%`)) + : this.eb(fieldRef, 'like', sql.val(`%${value}%`)), ) .with('startsWith', () => mode === 'insensitive' - ? eb(fieldRef, 'ilike', sql.val(`${value}%`)) - : eb(fieldRef, 'like', sql.val(`${value}%`)), + ? this.eb(fieldRef, 'ilike', sql.val(`${value}%`)) + : this.eb(fieldRef, 'like', sql.val(`${value}%`)), ) .with('endsWith', () => mode === 'insensitive' - ? eb(fieldRef, 'ilike', sql.val(`%${value}`)) - : eb(fieldRef, 'like', sql.val(`%${value}`)), + ? this.eb(fieldRef, 'ilike', sql.val(`%${value}`)) + : this.eb(fieldRef, 'like', sql.val(`%${value}`)), ) .otherwise(() => { throw new QueryError(`Invalid string filter key: ${key}`); @@ -699,7 +650,7 @@ export abstract class BaseCrudDialect { } } - return this.and(eb, ...conditions); + return this.and(...conditions); } private prepStringCasing( @@ -720,93 +671,66 @@ export abstract class BaseCrudDialect { } } - private buildNumberFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - type: BuiltinType, - payload: any, - ) { + private buildNumberFilter(fieldRef: Expression, type: BuiltinType, payload: any) { const { conditions } = this.buildStandardFilter( - eb, type, payload, fieldRef, (value) => this.transformPrimitive(value, type, false), - (value) => this.buildNumberFilter(eb, fieldRef, type, value), + (value) => this.buildNumberFilter(fieldRef, type, value), ); - return this.and(eb, ...conditions); + return this.and(...conditions); } - private buildBooleanFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - payload: BooleanFilter, - ) { + private buildBooleanFilter(fieldRef: Expression, payload: BooleanFilter) { const { conditions } = this.buildStandardFilter( - eb, 'Boolean', payload, fieldRef, (value) => this.transformPrimitive(value, 'Boolean', false), - (value) => this.buildBooleanFilter(eb, fieldRef, value as BooleanFilter), + (value) => this.buildBooleanFilter(fieldRef, value as BooleanFilter), true, ['equals', 'not'], ); - return this.and(eb, ...conditions); + return this.and(...conditions); } - private buildDateTimeFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - payload: DateTimeFilter, - ) { + private buildDateTimeFilter(fieldRef: Expression, payload: DateTimeFilter) { const { conditions } = this.buildStandardFilter( - eb, 'DateTime', payload, fieldRef, (value) => this.transformPrimitive(value, 'DateTime', false), - (value) => this.buildDateTimeFilter(eb, fieldRef, value as DateTimeFilter), + (value) => this.buildDateTimeFilter(fieldRef, value as DateTimeFilter), true, ); - return this.and(eb, ...conditions); + return this.and(...conditions); } - private buildBytesFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - payload: BytesFilter, - ) { + private buildBytesFilter(fieldRef: Expression, payload: BytesFilter) { const conditions = this.buildStandardFilter( - eb, 'Bytes', payload, fieldRef, (value) => this.transformPrimitive(value, 'Bytes', false), - (value) => this.buildBytesFilter(eb, fieldRef, value as BytesFilter), + (value) => this.buildBytesFilter(fieldRef, value as BytesFilter), true, ['equals', 'in', 'notIn', 'not'], ); - return this.and(eb, ...conditions.conditions); + return this.and(...conditions.conditions); } - private buildEnumFilter( - eb: ExpressionBuilder, - fieldRef: Expression, - fieldDef: FieldDef, - payload: any, - ) { + private buildEnumFilter(fieldRef: Expression, fieldDef: FieldDef, payload: any) { const conditions = this.buildStandardFilter( - eb, 'String', payload, fieldRef, (value) => value, - (value) => this.buildEnumFilter(eb, fieldRef, fieldDef, value), + (value) => this.buildEnumFilter(fieldRef, fieldDef, value), true, ['equals', 'in', 'notIn', 'not'], ); - return this.and(eb, ...conditions.conditions); + return this.and(...conditions.conditions); } buildOrderBy( @@ -826,6 +750,14 @@ export abstract class BaseCrudDialect { } let result = query; + + const buildFieldRef = (model: string, field: string, modelAlias: string) => { + const fieldDef = requireField(this.schema, model, field); + return fieldDef.originModel + ? this.fieldRef(fieldDef.originModel, field, fieldDef.originModel) + : this.fieldRef(model, field, modelAlias); + }; + enumerate(orderBy).forEach((orderBy) => { for (const [field, value] of Object.entries(orderBy)) { if (!value) { @@ -838,8 +770,7 @@ export abstract class BaseCrudDialect { for (const [k, v] of Object.entries(value)) { invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( - (eb) => - aggregate(eb, this.fieldRef(model, k, eb, modelAlias), field as AGGREGATE_OPERATORS), + (eb) => aggregate(eb, buildFieldRef(model, k, modelAlias), field as AGGREGATE_OPERATORS), sql.raw(this.negateSort(v, negated)), ); } @@ -852,7 +783,7 @@ export abstract class BaseCrudDialect { for (const [k, v] of Object.entries(value)) { invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( - (eb) => eb.fn.count(this.fieldRef(model, k, eb, modelAlias)), + (eb) => eb.fn.count(buildFieldRef(model, k, modelAlias)), sql.raw(this.negateSort(v, negated)), ); } @@ -865,7 +796,7 @@ export abstract class BaseCrudDialect { const fieldDef = requireField(this.schema, model, field); if (!fieldDef.relation) { - const fieldRef = this.fieldRef(model, field, expressionBuilder(), modelAlias); + const fieldRef = buildFieldRef(model, field, modelAlias); if (value === 'asc' || value === 'desc') { result = result.orderBy(fieldRef, this.negateSort(value, negated)); } else if ( @@ -898,11 +829,10 @@ export abstract class BaseCrudDialect { const sort = this.negateSort(value._count, negated); result = result.orderBy((eb) => { const subQueryAlias = `${modelAlias}$orderBy$${field}$count`; - let subQuery = this.buildSelectModel(eb, relationModel, subQueryAlias); + let subQuery = this.buildSelectModel(relationModel, subQueryAlias); const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, subQueryAlias); subQuery = subQuery.where(() => this.and( - eb, ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))), ), ); @@ -915,10 +845,7 @@ export abstract class BaseCrudDialect { result = result.leftJoin(relationModel, (join) => { const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel); return join.on((eb) => - this.and( - eb, - ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))), - ), + this.and(...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right)))), ); }); result = this.buildOrderBy(result, fieldDef.type, relationModel, value, false, negated); @@ -964,7 +891,7 @@ export abstract class BaseCrudDialect { } jsonObject[field] = eb.ref(`${subModel.name}.${field}`); } - return this.buildJsonObject(eb, jsonObject).as(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`); + return this.buildJsonObject(jsonObject).as(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`); }); } @@ -972,13 +899,12 @@ export abstract class BaseCrudDialect { } protected buildModelSelect( - eb: ExpressionBuilder, model: GetModels, subQueryAlias: string, payload: true | FindArgs, true>, selectAllFields: boolean, ) { - let subQuery = this.buildSelectModel(eb, model, subQueryAlias); + let subQuery = this.buildSelectModel(model, subQueryAlias); if (selectAllFields) { subQuery = this.buildSelectAllFields( @@ -1005,7 +931,7 @@ export abstract class BaseCrudDialect { const fieldDef = requireField(this.schema, model, field); if (fieldDef.computed) { // TODO: computed field from delegate base? - return query.select((eb) => this.fieldRef(model, field, eb, modelAlias).as(field)); + return query.select(() => this.fieldRef(model, field, modelAlias).as(field)); } else if (!fieldDef.originModel) { // regular field return query.select(sql.ref(`${modelAlias}.${field}`).as(field)); @@ -1085,14 +1011,14 @@ export abstract class BaseCrudDialect { value.where && typeof value.where === 'object' ) { - const filter = this.buildFilter(eb, fieldModel, fieldModel, value.where); + const filter = this.buildFilter(fieldModel, fieldModel, value.where); fieldCountQuery = fieldCountQuery.where(filter); } jsonObject[field] = fieldCountQuery; } - return this.buildJsonObject(eb, jsonObject); + return this.buildJsonObject(jsonObject); } // #endregion @@ -1103,12 +1029,12 @@ export abstract class BaseCrudDialect { return negated ? (sort === 'asc' ? 'desc' : 'asc') : sort; } - public true(eb: ExpressionBuilder): Expression { - return eb.lit(this.transformPrimitive(true, 'Boolean', false) as boolean); + public true(): Expression { + return this.eb.lit(this.transformPrimitive(true, 'Boolean', false) as boolean); } - public false(eb: ExpressionBuilder): Expression { - return eb.lit(this.transformPrimitive(false, 'Boolean', false) as boolean); + public false(): Expression { + return this.eb.lit(this.transformPrimitive(false, 'Boolean', false) as boolean); } public isTrue(expression: Expression) { @@ -1127,40 +1053,34 @@ export abstract class BaseCrudDialect { return (node as ValueNode).value === false || (node as ValueNode).value === 0; } - and(eb: ExpressionBuilder, ...args: Expression[]) { + and(...args: Expression[]) { const nonTrueArgs = args.filter((arg) => !this.isTrue(arg)); if (nonTrueArgs.length === 0) { - return this.true(eb); + return this.true(); } else if (nonTrueArgs.length === 1) { return nonTrueArgs[0]!; } else { - return eb.and(nonTrueArgs); + return this.eb.and(nonTrueArgs); } } - or(eb: ExpressionBuilder, ...args: Expression[]) { + or(...args: Expression[]) { const nonFalseArgs = args.filter((arg) => !this.isFalse(arg)); if (nonFalseArgs.length === 0) { - return this.false(eb); + return this.false(); } else if (nonFalseArgs.length === 1) { return nonFalseArgs[0]!; } else { - return eb.or(nonFalseArgs); + return this.eb.or(nonFalseArgs); } } - not(eb: ExpressionBuilder, ...args: Expression[]) { - return eb.not(this.and(eb, ...args)); + not(...args: Expression[]) { + return this.eb.not(this.and(...args)); } - fieldRef( - model: string, - field: string, - eb: ExpressionBuilder, - modelAlias?: string, - inlineComputedField = true, - ) { - return buildFieldRef(this.schema, model, field, this.options, eb, modelAlias, inlineComputedField); + fieldRef(model: string, field: string, modelAlias?: string, inlineComputedField = true) { + return buildFieldRef(this.schema, model, field, this.options, this.eb, modelAlias, inlineComputedField); } protected canJoinWithoutNestedSelect( @@ -1221,18 +1141,12 @@ export abstract class BaseCrudDialect { /** * Builds an Kysely expression that returns a JSON object for the given key-value pairs. */ - abstract buildJsonObject( - eb: ExpressionBuilder, - value: Record>, - ): ExpressionWrapper; + abstract buildJsonObject(value: Record>): ExpressionWrapper; /** * Builds an Kysely expression that returns the length of an array. */ - abstract buildArrayLength( - eb: ExpressionBuilder, - array: Expression, - ): ExpressionWrapper; + abstract buildArrayLength(array: Expression): ExpressionWrapper; /** * Builds an array literal SQL string for the given values. diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index b6c40661..0a60c350 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -153,7 +153,7 @@ export class PostgresCrudDialect extends BaseCrudDiale if (this.canJoinWithoutNestedSelect(relationModelDef, payload)) { // build join directly - tbl = this.buildModelSelect(eb, relationModel, relationSelectName, payload, false); + tbl = this.buildModelSelect(relationModel, relationSelectName, payload, false); // parent join filter tbl = this.buildRelationJoinFilter( @@ -167,13 +167,7 @@ export class PostgresCrudDialect extends BaseCrudDiale } else { // join with a nested query tbl = eb.selectFrom(() => { - let subQuery = this.buildModelSelect( - eb, - relationModel, - `${relationSelectName}$t`, - payload, - true, - ); + let subQuery = this.buildModelSelect(relationModel, `${relationSelectName}$t`, payload, true); // parent join filter subQuery = this.buildRelationJoinFilter( @@ -237,7 +231,7 @@ export class PostgresCrudDialect extends BaseCrudDiale } else { const joinPairs = buildJoinPairs(this.schema, model, parentAlias, relationField, relationModelAlias); query = query.where((eb) => - this.and(eb, ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right)))), + this.and(...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right)))), ); } return query; @@ -303,10 +297,7 @@ 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, relationModelAlias, false), - ]) + .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, relationModelAlias, false)]) .flatMap((v) => v), ); } else if (payload.select) { @@ -329,7 +320,7 @@ export class PostgresCrudDialect extends BaseCrudDiale ? // reference the synthesized JSON field eb.ref(`${parentResultName}$${field}.$data`) : // reference a plain field - this.fieldRef(relationModel, field, eb, relationModelAlias, false); + this.fieldRef(relationModel, field, relationModelAlias, false); return [sql.lit(field), fieldValue]; } }) @@ -396,8 +387,8 @@ export class PostgresCrudDialect extends BaseCrudDiale return query; } - override buildJsonObject(eb: ExpressionBuilder, value: Record>) { - return eb.fn( + override buildJsonObject(value: Record>) { + return this.eb.fn( 'jsonb_build_object', Object.entries(value).flatMap(([key, value]) => [sql.lit(key), value]), ); @@ -415,11 +406,8 @@ export class PostgresCrudDialect extends BaseCrudDiale return true; } - override buildArrayLength( - eb: ExpressionBuilder, - array: Expression, - ): ExpressionWrapper { - return eb.fn('array_length', [array]); + override buildArrayLength(array: Expression): ExpressionWrapper { + return this.eb.fn('array_length', [array]); } override buildArrayLiteralSQL(values: unknown[]): string { diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 5c024dfb..e163f464 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -155,7 +155,7 @@ export class SqliteCrudDialect extends BaseCrudDialect if (this.canJoinWithoutNestedSelect(relationModelDef, payload)) { // join without needing a nested select on relation model - tbl = this.buildModelSelect(eb, relationModel, subQueryName, payload, false); + tbl = this.buildModelSelect(relationModel, subQueryName, payload, false); // add parent join filter tbl = this.buildRelationJoinFilter(tbl, model, relationField, subQueryName, parentAlias); @@ -166,7 +166,7 @@ export class SqliteCrudDialect extends BaseCrudDialect const selectModelAlias = `${parentAlias}$${relationField}$sub`; // select all fields - let selectModelQuery = this.buildModelSelect(eb, relationModel, selectModelAlias, payload, true); + let selectModelQuery = this.buildModelSelect(relationModel, selectModelAlias, payload, true); // add parent join filter selectModelQuery = this.buildRelationJoinFilter( @@ -203,10 +203,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, subQueryName, false), - ]) + .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, subQueryName, false)]) .flatMap((v) => v), ); } else if (payload.select) { @@ -237,7 +234,7 @@ export class SqliteCrudDialect extends BaseCrudDialect } else { return [ sql.lit(field), - this.fieldRef(relationModel, field, eb, subQueryName, false) as ArgsType, + this.fieldRef(relationModel, field, subQueryName, false) as ArgsType, ]; } } @@ -345,8 +342,8 @@ export class SqliteCrudDialect extends BaseCrudDialect return query; } - override buildJsonObject(eb: ExpressionBuilder, value: Record>) { - return eb.fn( + override buildJsonObject(value: Record>) { + return this.eb.fn( 'json_object', Object.entries(value).flatMap(([key, value]) => [sql.lit(key), value]), ); @@ -364,11 +361,8 @@ export class SqliteCrudDialect extends BaseCrudDialect return false; } - override buildArrayLength( - eb: ExpressionBuilder, - array: Expression, - ): ExpressionWrapper { - return eb.fn('json_array_length', [array]); + override buildArrayLength(array: Expression): ExpressionWrapper { + return this.eb.fn('json_array_length', [array]); } override buildArrayLiteralSQL(_values: unknown[]): string { diff --git a/packages/runtime/src/client/crud/operations/aggregate.ts b/packages/runtime/src/client/crud/operations/aggregate.ts index fe111481..5df07608 100644 --- a/packages/runtime/src/client/crud/operations/aggregate.ts +++ b/packages/runtime/src/client/crud/operations/aggregate.ts @@ -1,4 +1,3 @@ -import type { ExpressionBuilder } from 'kysely'; import { sql } from 'kysely'; import { match } from 'ts-pattern'; import type { SchemaDef } from '../../../schema'; @@ -18,8 +17,8 @@ export class AggregateOperationHandler extends BaseOpe // table and where let subQuery = this.dialect - .buildSelectModel(eb as ExpressionBuilder, this.model, this.model) - .where((eb1) => this.dialect.buildFilter(eb1, this.model, this.model, parsedArgs?.where)); + .buildSelectModel(this.model, this.model) + .where(() => this.dialect.buildFilter(this.model, this.model, parsedArgs?.where)); // select fields: collect fields from aggregation body const selectedFields: string[] = []; diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 2aca2980..1832874a 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -143,7 +143,7 @@ export abstract class BaseOperationHandler { args: FindArgs, true> | undefined, ): Promise { // table - let query = this.dialect.buildSelectModel(expressionBuilder(), model, model); + let query = this.dialect.buildSelectModel(model, model); if (args) { query = this.dialect.buildFilterSortTake(model, args, query, model); @@ -1043,7 +1043,7 @@ export abstract class BaseOperationHandler { const idFields = requireIdFields(this.schema, model); const query = kysely .updateTable(model) - .where((eb) => this.dialect.buildFilter(eb, model, model, combinedWhere)) + .where(() => this.dialect.buildFilter(model, model, combinedWhere)) .set(updateFields) .returning(idFields as any) .modifyEnd( @@ -1155,7 +1155,7 @@ export abstract class BaseOperationHandler { const key = Object.keys(payload)[0]; const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, false); const eb = expressionBuilder(); - const fieldRef = this.dialect.fieldRef(model, field, eb); + const fieldRef = this.dialect.fieldRef(model, field); return match(key) .with('set', () => value) @@ -1178,7 +1178,7 @@ export abstract class BaseOperationHandler { const key = Object.keys(payload)[0]; const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, true); const eb = expressionBuilder(); - const fieldRef = this.dialect.fieldRef(model, field, eb); + const fieldRef = this.dialect.fieldRef(model, field); return match(key) .with('set', () => value) @@ -1273,7 +1273,7 @@ export abstract class BaseOperationHandler { if (!shouldFallbackToIdFilter) { // simple filter query = query - .where((eb) => this.dialect.buildFilter(eb, model, model, where)) + .where(() => this.dialect.buildFilter(model, model, where)) .$if(limit !== undefined, (qb) => qb.limit(limit!)); } else { query = query.where((eb) => @@ -1284,8 +1284,8 @@ export abstract class BaseOperationHandler { ), 'in', this.dialect - .buildSelectModel(eb, filterModel, filterModel) - .where(this.dialect.buildFilter(eb, filterModel, filterModel, where)) + .buildSelectModel(filterModel, filterModel) + .where(this.dialect.buildFilter(filterModel, filterModel, where)) .select(this.buildIdFieldRefs(kysely, filterModel)) .$if(limit !== undefined, (qb) => qb.limit(limit!)), ), @@ -1968,7 +1968,7 @@ export abstract class BaseOperationHandler { } if (!needIdFilter) { - query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where)); + query = query.where(() => this.dialect.buildFilter(model, model, where)); } else { query = query.where((eb) => eb( @@ -1978,8 +1978,8 @@ export abstract class BaseOperationHandler { ), 'in', this.dialect - .buildSelectModel(eb, filterModel, filterModel) - .where((eb) => this.dialect.buildFilter(eb, filterModel, filterModel, where)) + .buildSelectModel(filterModel, filterModel) + .where(() => this.dialect.buildFilter(filterModel, filterModel, where)) .select(this.buildIdFieldRefs(kysely, filterModel)) .$if(limit !== undefined, (qb) => qb.limit(limit!)), ), diff --git a/packages/runtime/src/client/crud/operations/count.ts b/packages/runtime/src/client/crud/operations/count.ts index 9c321d98..90451745 100644 --- a/packages/runtime/src/client/crud/operations/count.ts +++ b/packages/runtime/src/client/crud/operations/count.ts @@ -1,4 +1,3 @@ -import type { ExpressionBuilder } from 'kysely'; import { sql } from 'kysely'; import type { SchemaDef } from '../../../schema'; import { BaseOperationHandler } from './base'; @@ -16,8 +15,8 @@ export class CountOperationHandler extends BaseOperati // nested query for filtering and pagination let subQuery = this.dialect - .buildSelectModel(eb as ExpressionBuilder, this.model, this.model) - .where((eb1) => this.dialect.buildFilter(eb1, this.model, this.model, parsedArgs?.where)); + .buildSelectModel(this.model, this.model) + .where(() => this.dialect.buildFilter(this.model, this.model, parsedArgs?.where)); if (parsedArgs?.select && typeof parsedArgs.select === 'object') { // select fields diff --git a/packages/runtime/src/client/crud/operations/group-by.ts b/packages/runtime/src/client/crud/operations/group-by.ts index 14bb77b5..4f4a083f 100644 --- a/packages/runtime/src/client/crud/operations/group-by.ts +++ b/packages/runtime/src/client/crud/operations/group-by.ts @@ -1,4 +1,3 @@ -import { expressionBuilder } from 'kysely'; import { match } from 'ts-pattern'; import type { SchemaDef } from '../../../schema'; import { aggregate, getField } from '../../query-utils'; @@ -19,7 +18,7 @@ export class GroupByOperationHandler extends BaseOpera let subQuery = eb .selectFrom(this.model) .selectAll() - .where((eb1) => this.dialect.buildFilter(eb1, this.model, this.model, parsedArgs?.where)); + .where(() => this.dialect.buildFilter(this.model, this.model, parsedArgs?.where)); // skip & take const skip = parsedArgs?.skip; @@ -44,7 +43,7 @@ export class GroupByOperationHandler extends BaseOpera return subQuery.as('$sub'); }); - const fieldRef = (field: string) => this.dialect.fieldRef(this.model, field, expressionBuilder(), '$sub'); + const fieldRef = (field: string) => this.dialect.fieldRef(this.model, field, '$sub'); // groupBy const bys = typeof parsedArgs.by === 'string' ? [parsedArgs.by] : (parsedArgs.by as string[]); @@ -56,7 +55,7 @@ export class GroupByOperationHandler extends BaseOpera } if (parsedArgs.having) { - query = query.having((eb) => this.dialect.buildFilter(eb, this.model, '$sub', parsedArgs.having)); + query = query.having(() => this.dialect.buildFilter(this.model, '$sub', parsedArgs.having)); } // select all by fields diff --git a/packages/runtime/src/client/functions.ts b/packages/runtime/src/client/functions.ts index 35390916..3f1bc806 100644 --- a/packages/runtime/src/client/functions.ts +++ b/packages/runtime/src/client/functions.ts @@ -100,7 +100,7 @@ export const isEmpty: ZModelFunction = (eb, args, { dialect }: ZModelFuncti if (!field) { throw new Error('"field" parameter is required'); } - return eb(dialect.buildArrayLength(eb, field), '=', sql.lit(0)); + return eb(dialect.buildArrayLength(field), '=', sql.lit(0)); }; export const now: ZModelFunction = () => sql.raw('CURRENT_TIMESTAMP'); diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index fd234378..7662fba0 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -91,14 +91,29 @@ export async function generateTsSchemaInPlace(schemaPath: string) { return compileAndLoad(workDir); } -export async function loadSchema(schema: string) { +export async function loadSchema(schema: string, additionalSchemas?: Record) { if (!schema.includes('datasource ')) { schema = `${makePrelude('sqlite')}\n\n${schema}`; } + // create a temp folder + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zenstack-schema')); + // create a temp file - const tempFile = path.join(os.tmpdir(), `zenstack-schema-${crypto.randomUUID()}.zmodel`); + const tempFile = path.join(tempDir, `schema.zmodel`); fs.writeFileSync(tempFile, schema); + + if (additionalSchemas) { + for (const [fileName, content] of Object.entries(additionalSchemas)) { + let name = fileName; + if (!name.endsWith('.zmodel')) { + name += '.zmodel'; + } + const filePath = path.join(tempDir, name); + fs.writeFileSync(filePath, content); + } + } + const r = await loadDocument(tempFile); expect(r).toSatisfy( (r) => r.success, diff --git a/tests/regression/test/v2-migrated/issue-1014.test.ts b/tests/regression/test/v2-migrated/issue-1014.test.ts new file mode 100644 index 00000000..70917ff2 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1014.test.ts @@ -0,0 +1,49 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// TODO: field-level policy support +describe.skip('Regression for issue 1014', () => { + it('update', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id() @default(autoincrement()) + name String + posts Post[] + } + + model Post { + id Int @id() @default(autoincrement()) + title String + content String? + author User? @relation(fields: [authorId], references: [id]) + authorId Int? @allow('update', true, true) + + @@allow('read', true) + } + `, + ); + + const user = await db.$unuseAll().user.create({ data: { name: 'User1' } }); + const post = await db.$unuseAll().post.create({ data: { title: 'Post1' } }); + await expect(db.post.update({ where: { id: post.id }, data: { authorId: user.id } })).toResolveTruthy(); + }); + + it('read', async () => { + const db = await createPolicyTestClient( + ` + model Post { + id Int @id() @default(autoincrement()) + title String @allow('read', true, true) + content String + } + `, + ); + + const post = await db.$unuseAll().post.create({ data: { title: 'Post1', content: 'Content' } }); + await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); + await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ + title: 'Post1', + }); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1058.test.ts b/tests/regression/test/v2-migrated/issue-1058.test.ts new file mode 100644 index 00000000..fed09565 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1058.test.ts @@ -0,0 +1,52 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1058', async () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String + + userRankings UserRanking[] + userFavorites UserFavorite[] + } + + model Entity { + id String @id @default(cuid()) + name String + type String + userRankings UserRanking[] + userFavorites UserFavorite[] + + @@delegate(type) + } + + model Person extends Entity { + } + + model Studio extends Entity { + } + + + model UserRanking { + id String @id @default(cuid()) + rank Int + + entityId String + entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + } + + model UserFavorite { + id String @id @default(cuid()) + + entityId String + entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + } + `; + + await createTestClient(schema, { provider: 'postgresql' }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1078.test.ts b/tests/regression/test/v2-migrated/issue-1078.test.ts new file mode 100644 index 00000000..a3af1620 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1078.test.ts @@ -0,0 +1,53 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1078', () => { + it('regression1', async () => { + const db = await createPolicyTestClient( + ` + model Counter { + id String @id + + name String + value Int + + @@validate(value >= 0) + @@allow('all', true) + } + `, + ); + + await expect( + db.counter.create({ + data: { id: '1', name: 'It should create', value: 1 }, + }), + ).toResolveTruthy(); + + //! This query fails validation + await expect( + db.counter.update({ + where: { id: '1' }, + data: { name: 'It should update' }, + }), + ).toResolveTruthy(); + }); + + // TODO: field-level policy support + it.skip('regression2', async () => { + const db = await createPolicyTestClient( + ` + model Post { + id Int @id() @default(autoincrement()) + title String @allow('read', true, true) + content String + } + `, + ); + + const post = await db.$unuseAll().post.create({ data: { title: 'Post1', content: 'Content' } }); + await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); + await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ + title: 'Post1', + }); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1080.test.ts b/tests/regression/test/v2-migrated/issue-1080.test.ts new file mode 100644 index 00000000..0f46beca --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1080.test.ts @@ -0,0 +1,129 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1080', async () => { + const db = await createPolicyTestClient( + ` +model Project { + id String @id @unique @default(uuid()) + Fields Field[] + + @@allow('all', true) +} + +model Field { + id String @id @unique @default(uuid()) + name String + Project Project @relation(fields: [projectId], references: [id]) + projectId String + + @@allow('all', true) +} + `, + ); + + const project = await db.project.create({ + include: { Fields: true }, + data: { + Fields: { + create: [{ name: 'first' }, { name: 'second' }], + }, + }, + }); + + let updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: [ + { + where: { id: project.Fields[0].id }, + create: { name: 'first1' }, + update: { name: 'first1' }, + }, + { + where: { id: project.Fields[1].id }, + create: { name: 'second1' }, + update: { name: 'second1' }, + }, + ], + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first1' }), + expect.objectContaining({ name: 'second1' }), + ]), + }); + + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: project.Fields[0].id }, + create: { name: 'first2' }, + update: { name: 'first2' }, + }, + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first2' }), + expect.objectContaining({ name: 'second1' }), + ]), + }); + + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: project.Fields[0].id }, + create: { name: 'first3' }, + update: { name: 'first3' }, + }, + update: { + where: { id: project.Fields[1].id }, + data: { name: 'second3' }, + }, + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first3' }), + expect.objectContaining({ name: 'second3' }), + ]), + }); + + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: 'non-exist' }, + create: { name: 'third1' }, + update: { name: 'third1' }, + }, + update: { + where: { id: project.Fields[1].id }, + data: { name: 'second4' }, + }, + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first3' }), + expect.objectContaining({ name: 'second4' }), + expect.objectContaining({ name: 'third1' }), + ]), + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1123.test.ts b/tests/regression/test/v2-migrated/issue-1123.test.ts new file mode 100644 index 00000000..3c1cb4d0 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1123.test.ts @@ -0,0 +1,43 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1123', async () => { + const db = await createPolicyTestClient( + ` +model Content { + id String @id @default(cuid()) + published Boolean @default(false) + contentType String + likes Like[] + @@delegate(contentType) + @@allow('all', true) +} + +model Post extends Content { + title String +} + +model Image extends Content { + url String +} + +model Like { + id String @id @default(cuid()) + content Content @relation(fields: [contentId], references: [id]) + contentId String + @@allow('all', true) +} + `, + ); + + await db.post.create({ + data: { + title: 'a post', + likes: { create: {} }, + }, + }); + + await expect(db.content.findFirst({ include: { _count: { select: { likes: true } } } })).resolves.toMatchObject({ + _count: { likes: 1 }, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1135.test.ts b/tests/regression/test/v2-migrated/issue-1135.test.ts new file mode 100644 index 00000000..41df934f --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1135.test.ts @@ -0,0 +1,76 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1135', async () => { + const db = await createTestClient( + ` +model Attachment { + id String @id @default(cuid()) + url String + myEntityId String + myEntity Entity @relation(fields: [myEntityId], references: [id], onUpdate: NoAction) +} + +model Entity { + id String @id @default(cuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @default(now()) + + attachments Attachment[] + + type String + @@delegate(type) +} + +model Person extends Entity { + age Int? +} + `, + { + extraSourceFiles: { + 'main.ts': ` +import { ZenStackClient } from '@zenstackhq/runtime'; +import { schema } from './schema'; + +const db = new ZenStackClient(schema, {} as any); + +db.person.create({ + data: { + name: 'test', + attachments: { + create: { + url: 'https://...', + }, + }, + }, +}); + `, + }, + }, + ); + + await expect( + db.person.create({ + data: { + name: 'test', + attachments: { + create: { + url: 'https://...', + }, + }, + }, + include: { attachments: true }, + }), + ).resolves.toMatchObject({ + id: expect.any(String), + name: 'test', + attachments: [ + { + id: expect.any(String), + url: 'https://...', + myEntityId: expect.any(String), + }, + ], + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1149.test.ts b/tests/regression/test/v2-migrated/issue-1149.test.ts new file mode 100644 index 00000000..404c3969 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1149.test.ts @@ -0,0 +1,90 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1149', async () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String + + userRankings UserRanking[] + userFavorites UserFavorite[] + } + + model Entity { + id String @id @default(cuid()) + name String + type String + userRankings UserRanking[] + userFavorites UserFavorite[] + + @@delegate(type) + } + + model Person extends Entity { + } + + model Studio extends Entity { + } + + + model UserRanking { + id String @id @default(cuid()) + rank Int + + entityId String + entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + } + + model UserFavorite { + id String @id @default(cuid()) + + entityId String + entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + } + `; + + const db = await createTestClient(schema); + + const user = await db.user.create({ data: { name: 'user' } }); + const person = await db.person.create({ data: { name: 'person' } }); + + await expect( + db.userRanking.createMany({ + data: { + rank: 1, + entityId: person.id, + userId: user.id, + }, + }), + ).resolves.toMatchObject({ count: 1 }); + + await expect( + db.userRanking.createMany({ + data: [ + { + rank: 2, + entityId: person.id, + userId: user.id, + }, + { + rank: 3, + entityId: person.id, + userId: user.id, + }, + ], + }), + ).resolves.toMatchObject({ count: 2 }); + + await expect(db.userRanking.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ rank: 1 }), + expect.objectContaining({ rank: 2 }), + expect.objectContaining({ rank: 3 }), + ]), + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1167.test.ts b/tests/regression/test/v2-migrated/issue-1167.test.ts new file mode 100644 index 00000000..9a18c374 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1167.test.ts @@ -0,0 +1,19 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1167', async () => { + await loadSchema( + ` +model FileAsset { + id String @id @default(cuid()) + delegate_type String + @@delegate(delegate_type) + @@map("file_assets") +} + +model ImageAsset extends FileAsset { + @@map("image_assets") +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1179.test.ts b/tests/regression/test/v2-migrated/issue-1179.test.ts new file mode 100644 index 00000000..b6a21879 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1179.test.ts @@ -0,0 +1,26 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('regression', async () => { + await loadSchema( + ` +type Base { + id String @id @default(uuid()) +} + +model User with Base { + email String + posts Post[] + @@allow('all', auth() == this) +} + +model Post { + id String @id @default(uuid()) + + user User @relation(fields: [userId], references: [id]) + userId String + @@allow('all', auth().id == userId) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1235.test.ts b/tests/regression/test/v2-migrated/issue-1235.test.ts new file mode 100644 index 00000000..e5d17b6a --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1235.test.ts @@ -0,0 +1,39 @@ +import { createPolicyTestClient, testLogger } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1235', () => { + it('regression1', async () => { + const db = await createPolicyTestClient( + ` +model Post { + id Int @id @default(autoincrement()) + @@deny('post-update', before().id != id) + @@allow('all', true) +} + `, + { log: testLogger }, + ); + + const post = await db.post.create({ data: {} }); + await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).rejects.toThrow( + /updating id fields is not supported/, + ); + }); + + it('regression2', async () => { + const db = await createPolicyTestClient( + ` +model Post { + id Int @id @default(autoincrement()) + @@deny('post-update', before().id != this.id) + @@allow('all', true) +} + `, + ); + + const post = await db.post.create({ data: {} }); + await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).rejects.toThrow( + /updating id fields is not supported/, + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1241.test.ts b/tests/regression/test/v2-migrated/issue-1241.test.ts new file mode 100644 index 00000000..bddfa4e8 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1241.test.ts @@ -0,0 +1,84 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { randomBytes } from 'crypto'; +import { expect, it } from 'vitest'; + +it('verifies issue 1241', async () => { + const db = await createPolicyTestClient( + ` +model User { + id String @id @default(uuid()) + todos Todo[] + + @@auth + @@allow('all', true) +} + +model Todo { + id String @id @default(uuid()) + + user_id String + user User @relation(fields: [user_id], references: [id]) + + images File[] @relation("todo_images") + documents File[] @relation("todo_documents") + + @@allow('all', true) +} + +model File { + id String @id @default(uuid()) + s3_key String @unique + label String + + todo_image_id String? + todo_image Todo? @relation("todo_images", fields: [todo_image_id], references: [id]) + + todo_document_id String? + todo_document Todo? @relation("todo_documents", fields: [todo_document_id], references: [id]) + + @@allow('all', true) +} + `, + ); + + const user = await db.$unuseAll().user.create({ + data: {}, + }); + await db.$unuseAll().todo.create({ + data: { + user_id: user.id, + + images: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `img-label-${i + 1}`, + })), + }, + + documents: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `doc-label-${i + 1}`, + })), + }, + }, + }); + + const todo = await db.todo.findFirst({ where: {}, include: { documents: true } }); + await expect( + db.todo.update({ + where: { id: todo.id }, + data: { + documents: { + update: todo.documents.map((doc: any) => { + return { + where: { s3_key: doc.s3_key }, + data: { label: 'updated' }, + }; + }), + }, + }, + include: { documents: true }, + }), + ).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1243.test.ts b/tests/regression/test/v2-migrated/issue-1243.test.ts new file mode 100644 index 00000000..1122f90f --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1243.test.ts @@ -0,0 +1,52 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, it } from 'vitest'; + +describe('Regression for issue 1243', () => { + it('uninheritable fields', async () => { + const schema = ` + model Base { + id String @id @default(cuid()) + type String + foo String + + @@delegate(type) + @@index([foo]) + @@map('base') + @@unique([foo]) + } + + model Item1 extends Base { + x String + } + + model Item2 extends Base { + y String + } + `; + + await createTestClient(schema); + }); + + it('multiple id fields', async () => { + const schema = ` + model Base { + id1 String + id2 String + type String + + @@delegate(type) + @@id([id1, id2]) + } + + model Item1 extends Base { + x String + } + + model Item2 extends Base { + y String + } + `; + + await createTestClient(schema); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1257.test.ts b/tests/regression/test/v2-migrated/issue-1257.test.ts new file mode 100644 index 00000000..38fc799e --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1257.test.ts @@ -0,0 +1,48 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1257', async () => { + await loadSchema( + ` +import "./user" +import "./image" + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +}`, + { + base: ` +type Base { + id Int @id @default(autoincrement()) +} +`, + user: ` +import "./base" +import "./image" + +enum Role { + Admin +} + +model User with Base { + email String @unique + role Role + @@auth +} +`, + image: ` +import "./user" +import "./base" + +model Image with Base { + width Int @default(0) + height Int @default(0) + + @@allow('read', true) + @@allow('all', auth().role == Admin) +} +`, + }, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1265.test.ts b/tests/regression/test/v2-migrated/issue-1265.test.ts new file mode 100644 index 00000000..d97be964 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1265.test.ts @@ -0,0 +1,26 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: zod schema support +it.skip('verifies issue 1265', async () => { + const { zodSchemas } = await createTestClient( + ` + model User { + id String @id @default(uuid()) + posts Post[] + @@allow('all', true) + } + + model Post { + id String @id @default(uuid()) + title String @default('xyz') + userId String @default(auth().id) + user User @relation(fields: [userId], references: [id]) + @@allow('all', true) + } + `, + ); + + expect(zodSchemas.models.PostCreateSchema.safeParse({ title: 'Post 1' }).success).toBeTruthy(); + expect(zodSchemas.input.PostInputSchema.create.safeParse({ data: { title: 'Post 1' } }).success).toBeTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1271.test.ts b/tests/regression/test/v2-migrated/issue-1271.test.ts new file mode 100644 index 00000000..d6acb7e6 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1271.test.ts @@ -0,0 +1,188 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1271', async () => { + const db = await createPolicyTestClient( + ` +model User { + id String @id @default(uuid()) + + @@auth + @@allow('all', true) +} + +model Test { + id String @id @default(uuid()) + linkingTable LinkingTable[] + key String @default('test') + locale String @default('EN') + + @@unique([key, locale]) + @@allow("all", true) +} + +model LinkingTable { + test_id String + test Test @relation(fields: [test_id], references: [id]) + + another_test_id String + another_test AnotherTest @relation(fields: [another_test_id], references: [id]) + + @@id([test_id, another_test_id]) + @@allow("all", true) +} + +model AnotherTest { + id String @id @default(uuid()) + status String + linkingTable LinkingTable[] + + @@allow("all", true) +} + `, + ); + + const test = await db.test.create({ + data: { + key: 'test1', + }, + }); + const anotherTest = await db.anotherTest.create({ + data: { + status: 'available', + }, + }); + + const updated = await db.test.upsert({ + where: { + key_locale: { + key: test.key, + locale: test.locale, + }, + }, + create: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + include: { + linkingTable: true, + }, + }); + + expect(updated.linkingTable).toHaveLength(1); + expect(updated.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const test2 = await db.test.upsert({ + where: { + key_locale: { + key: 'test2', + locale: 'locale2', + }, + }, + create: { + key: 'test2', + locale: 'locale2', + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + include: { + linkingTable: true, + }, + }); + expect(test2).toMatchObject({ key: 'test2', locale: 'locale2' }); + expect(test2.linkingTable).toHaveLength(1); + expect(test2.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const linkingTable = test2.linkingTable[0]; + + // connectOrCreate: connect case + const test3 = await db.test.create({ + data: { + key: 'test3', + locale: 'locale3', + }, + }); + console.log('test3 created:', test3); + const updated2 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: linkingTable.test_id, + another_test_id: linkingTable.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: test3.key, + locale: test3.locale, + }, + }, + create: { + key: 'test4', + locale: 'locale4', + }, + }, + }, + another_test: { connect: { id: anotherTest.id } }, + }, + include: { test: true }, + }); + expect(updated2).toMatchObject({ + test: expect.objectContaining({ key: 'test3', locale: 'locale3' }), + another_test_id: anotherTest.id, + }); + + // connectOrCreate: create case + const updated3 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: updated2.test_id, + another_test_id: updated2.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: 'test4', + locale: 'locale4', + }, + }, + create: { + key: 'test4', + locale: 'locale4', + }, + }, + }, + another_test: { connect: { id: anotherTest.id } }, + }, + include: { test: true }, + }); + expect(updated3).toMatchObject({ + test: expect.objectContaining({ key: 'test4', locale: 'locale4' }), + another_test_id: anotherTest.id, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1381.test.ts b/tests/regression/test/v2-migrated/issue-1381.test.ts new file mode 100644 index 00000000..3f168270 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1381.test.ts @@ -0,0 +1,55 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1381', async () => { + await loadSchema( + ` +enum MemberRole { + owner + admin +} + +enum SpaceType { + contractor + public + private +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique @lower + memberships Membership[] +} + +model Membership { + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + spaceId String + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + role MemberRole + @@id([userId, spaceId]) +} + +model Space { + id String @id @default(cuid()) + name String + type SpaceType @default(private) + memberships Membership[] + options Option[] +} + +model Option { + id String @id @default(cuid()) + name String? + spaceId String? + space Space? @relation(fields: [spaceId], references: [id], onDelete: SetNull) + + @@allow("post-update", + space.type in [contractor, public] && + space.memberships?[space.type in [contractor, public] && auth() == user && role in [owner, admin]] + ) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1388.test.ts b/tests/regression/test/v2-migrated/issue-1388.test.ts new file mode 100644 index 00000000..ab3f4701 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1388.test.ts @@ -0,0 +1,32 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1388', async () => { + await loadSchema( + ` +import './auth' +import './post' + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +`, + { + auth: ` +model User { + id String @id @default(cuid()) + role String +} + `, + post: ` +model Post { + id String @id @default(nanoid(6)) + title String + @@deny('all', auth() == null) + @@allow('all', auth().id == 'user1') +} + `, + }, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1410.test.ts b/tests/regression/test/v2-migrated/issue-1410.test.ts new file mode 100644 index 00000000..c4f7c2db --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1410.test.ts @@ -0,0 +1,143 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1410', async () => { + const db = await createTestClient( + ` + model Drink { + id Int @id @default(autoincrement()) + slug String @unique + + manufacturer_id Int + manufacturer Manufacturer @relation(fields: [manufacturer_id], references: [id]) + + type String + + name String @unique + description String + abv Float + image String? + + gluten Boolean + lactose Boolean + organic Boolean + + containers Container[] + + @@delegate(type) + + @@allow('all', true) + } + + model Beer extends Drink { + style_id Int + style BeerStyle @relation(fields: [style_id], references: [id]) + + ibu Float? + + @@allow('all', true) + } + + model BeerStyle { + id Int @id @default(autoincrement()) + + name String @unique + color String + + beers Beer[] + + @@allow('all', true) + } + + model Wine extends Drink { + style_id Int + style WineStyle @relation(fields: [style_id], references: [id]) + + heavy_score Int? + tannine_score Int? + dry_score Int? + fresh_score Int? + notes String? + + @@allow('all', true) + } + + model WineStyle { + id Int @id @default(autoincrement()) + + name String @unique + color String + + wines Wine[] + + @@allow('all', true) + } + + model Soda extends Drink { + carbonated Boolean + + @@allow('all', true) + } + + model Cocktail extends Drink { + mix Boolean + + @@allow('all', true) + } + + model Container { + barcode String @id + + drink_id Int + drink Drink @relation(fields: [drink_id], references: [id]) + + type String + volume Int + portions Int? + + inventory Int @default(0) + + @@allow('all', true) + } + + model Manufacturer { + id Int @id @default(autoincrement()) + + country_id String + country Country @relation(fields: [country_id], references: [code]) + + name String @unique + description String? + image String? + + drinks Drink[] + + @@allow('all', true) + } + + model Country { + code String @id + name String + + manufacturers Manufacturer[] + + @@allow('all', true) + } + `, + ); + + await db.beer.findMany({ + include: { style: true, manufacturer: true }, + where: { NOT: { gluten: true } }, + }); + + await db.beer.findMany({ + include: { style: true, manufacturer: true }, + where: { AND: [{ gluten: true }, { abv: { gt: 50 } }] }, + }); + + await db.beer.findMany({ + include: { style: true, manufacturer: true }, + where: { OR: [{ AND: [{ NOT: { gluten: true } }] }, { abv: { gt: 50 } }] }, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1415.test.ts b/tests/regression/test/v2-migrated/issue-1415.test.ts new file mode 100644 index 00000000..0ebbf7e9 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1415.test.ts @@ -0,0 +1,21 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1415', async () => { + await loadSchema( + ` +model User { + id String @id @default(cuid()) + prices Price[] +} + +model Price { + id String @id @default(cuid()) + owner User @relation(fields: [ownerId], references: [id]) + ownerId String @default(auth().id) + priceType String + @@delegate(priceType) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1416.test.ts b/tests/regression/test/v2-migrated/issue-1416.test.ts new file mode 100644 index 00000000..461ff068 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1416.test.ts @@ -0,0 +1,36 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1416', async () => { + await loadSchema( + ` +model User { + id String @id @default(cuid()) + role String +} + +model Price { + id String @id @default(nanoid(6)) + entity Entity? @relation(fields: [entityId], references: [id]) + entityId String? + priceType String + @@delegate(priceType) +} + +model MyPrice extends Price { + foo String +} + +model Entity { + id String @id @default(nanoid(6)) + price Price[] + type String + @@delegate(type) +} + +model MyEntity extends Entity { + foo String +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1427.test.ts b/tests/regression/test/v2-migrated/issue-1427.test.ts new file mode 100644 index 00000000..f111288f --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1427.test.ts @@ -0,0 +1,40 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1427', async () => { + const db = await createTestClient( + ` +model User { + id String @id @default(cuid()) + name String + profile Profile? + @@allow('all', true) +} + +model Profile { + id String @id @default(cuid()) + user User @relation(fields: [userId], references: [id]) + userId String @unique + @@allow('all', true) +} + `, + ); + + await db.$unuseAll().user.create({ + data: { + name: 'John', + profile: { + create: {}, + }, + }, + }); + + const found = await db.user.findFirst({ + select: { + id: true, + name: true, + profile: false, + }, + }); + expect(found.profile).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1451.test.ts b/tests/regression/test/v2-migrated/issue-1451.test.ts new file mode 100644 index 00000000..f6891819 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1451.test.ts @@ -0,0 +1,56 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 1451', async () => { + const db = await createTestClient( + ` +model User { + id String @id + memberships Membership[] +} + +model Space { + id String @id + memberships Membership[] +} + +model Membership { + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + spaceId String + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + + role String @deny("update", auth() == user) + employeeReference String? @deny("read, update", space.memberships?[auth() == user && !(role in ['owner', 'admin'])]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@id([userId, spaceId]) + @@allow('all', true) +} + `, + ); + + await db.$unuseAll().user.create({ + data: { id: '1' }, + }); + + await db.$unuseAll().space.create({ + data: { id: '1' }, + }); + + await db.$unuseAll().membership.create({ + data: { + user: { connect: { id: '1' } }, + space: { connect: { id: '1' } }, + role: 'foo', + employeeReference: 'xyz', + }, + }); + + const r = await db.membership.findMany(); + expect(r).toHaveLength(1); + expect(r[0].employeeReference).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1454.test.ts b/tests/regression/test/v2-migrated/issue-1454.test.ts new file mode 100644 index 00000000..d78b74fb --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1454.test.ts @@ -0,0 +1,116 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1454', () => { + it('regression1', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + sensitiveInformation String + username String + + purchases Purchase[] + + @@allow('read', auth() == this) +} + +model Purchase { + id Int @id @default(autoincrement()) + purchasedAt DateTime @default(now()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@allow('read', true) +} + `, + ); + + await db.$unuseAll().user.create({ + data: { username: 'user1', sensitiveInformation: 'sensitive', purchases: { create: {} } }, + }); + + await expect(db.purchase.findMany({ where: { user: { username: 'user1' } } })).resolves.toHaveLength(0); + await expect(db.purchase.findMany({ where: { user: { is: { username: 'user1' } } } })).resolves.toHaveLength(0); + }); + + // TODO: field-level policy support + it.skip('regression2', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + username String @allow('read', false) + + purchases Purchase[] + + @@allow('read', true) +} + +model Purchase { + id Int @id @default(autoincrement()) + purchasedAt DateTime @default(now()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@allow('read', true) +} + `, + ); + + const user = await db.$unuseAll().user.create({ + data: { username: 'user1', purchases: { create: {} } }, + }); + + await expect(db.purchase.findMany({ where: { user: { id: user.id } } })).resolves.toHaveLength(1); + await expect(db.purchase.findMany({ where: { user: { username: 'user1' } } })).resolves.toHaveLength(0); + await expect(db.purchase.findMany({ where: { user: { is: { username: 'user1' } } } })).resolves.toHaveLength(0); + }); + + // TODO: field-level policy support + it.skip('regression3', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + sensitiveInformation String + username String @allow('read', true, true) + + purchases Purchase[] + + @@allow('read', auth() == this) +} + +model Purchase { + id Int @id @default(autoincrement()) + purchasedAt DateTime @default(now()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@allow('read', true) +} + `, + ); + + await db.$unuseAll().user.create({ + data: { username: 'user1', sensitiveInformation: 'sensitive', purchases: { create: {} } }, + }); + + await expect(db.purchase.findMany({ where: { user: { username: 'user1' } } })).resolves.toHaveLength(1); + await expect(db.purchase.findMany({ where: { user: { is: { username: 'user1' } } } })).resolves.toHaveLength(1); + await expect( + db.purchase.findMany({ where: { user: { sensitiveInformation: 'sensitive' } } }), + ).resolves.toHaveLength(0); + await expect( + db.purchase.findMany({ where: { user: { is: { sensitiveInformation: 'sensitive' } } } }), + ).resolves.toHaveLength(0); + await expect( + db.purchase.findMany({ where: { user: { username: 'user1', sensitiveInformation: 'sensitive' } } }), + ).resolves.toHaveLength(0); + await expect( + db.purchase.findMany({ + where: { OR: [{ user: { username: 'user1' } }, { user: { sensitiveInformation: 'sensitive' } }] }, + }), + ).resolves.toHaveLength(1); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1466.test.ts b/tests/regression/test/v2-migrated/issue-1466.test.ts new file mode 100644 index 00000000..932369f7 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1466.test.ts @@ -0,0 +1,195 @@ +import { createTestClient, loadSchema } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1466', () => { + it('regression1', async () => { + const db = await createTestClient( + ` +model UserLongLongLongLongLongLongLongLongName { + id Int @id @default(autoincrement()) + level Int @default(0) + asset AssetLongLongLongLongLongLongLongLongName @relation(fields: [assetId], references: [id]) + assetId Int @unique +} + +model AssetLongLongLongLongLongLongLongLongName { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + viewCount Int @default(0) + owner UserLongLongLongLongLongLongLongLongName? + assetType String + + @@delegate(assetType) +} + +model VideoLongLongLongLongLongLongLongLongName extends AssetLongLongLongLongLongLongLongLongName { + duration Int +} + `, + { + usePrismaPush: true, + }, + ); + + const video = await db.VideoLongLongLongLongLongLongLongLongName.create({ + data: { duration: 100 }, + }); + + await db.UserLongLongLongLongLongLongLongLongName.create({ + data: { + asset: { connect: { id: video.id } }, + }, + }); + + const userWithAsset = await db.UserLongLongLongLongLongLongLongLongName.findFirst({ + include: { asset: true }, + }); + + expect(userWithAsset).toMatchObject({ + asset: { assetType: 'VideoLongLongLongLongLongLongLongLongName', duration: 100 }, + }); + }); + + it('regression2', async () => { + const db = await createTestClient( + ` + model UserLongLongLongLongName { + id Int @id @default(autoincrement()) + level Int @default(0) + asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) + assetId Int + + @@unique([assetId]) + } + + model AssetLongLongLongLongName { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + viewCount Int @default(0) + owner UserLongLongLongLongName? + assetType String + + @@delegate(assetType) + } + + model VideoLongLongLongLongName extends AssetLongLongLongLongName { + duration Int + } + `, + { + usePrismaPush: true, + }, + ); + + const video = await db.VideoLongLongLongLongName.create({ + data: { duration: 100 }, + }); + + await db.UserLongLongLongLongName.create({ + data: { + asset: { connect: { id: video.id } }, + }, + }); + + const userWithAsset = await db.UserLongLongLongLongName.findFirst({ + include: { asset: true }, + }); + + expect(userWithAsset).toMatchObject({ + asset: { assetType: 'VideoLongLongLongLongName', duration: 100 }, + }); + }); + + it('regression3', async () => { + await loadSchema( + ` +model UserLongLongLongLongName { + id Int @id @default(autoincrement()) + level Int @default(0) + asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) + assetId Int @unique +} + +model AssetLongLongLongLongName { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + viewCount Int @default(0) + owner UserLongLongLongLongName? + assetType String + + @@delegate(assetType) +} + +model VideoLongLongLongLongName1 extends AssetLongLongLongLongName { + duration Int +} + +model VideoLongLongLongLongName2 extends AssetLongLongLongLongName { + format String +} + `, + ); + }); + + it('regression4', async () => { + await loadSchema( + ` +model UserLongLongLongLongName { + id Int @id @default(autoincrement()) + level Int @default(0) + asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) + assetId Int @unique +} + +model AssetLongLongLongLongName { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + viewCount Int @default(0) + owner UserLongLongLongLongName? + assetType String + + @@delegate(assetType) +} + +model VideoLongLongLongLongName1 extends AssetLongLongLongLongName { + duration Int +} + +model VideoLongLongLongLongName2 extends AssetLongLongLongLongName { + format String +} + `, + ); + }); + + it('regression5', async () => { + await loadSchema( + ` +model UserLongLongLongLongName { + id Int @id @default(autoincrement()) + level Int @default(0) + asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) + assetId Int @unique(map: 'assetId_unique') +} + +model AssetLongLongLongLongName { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + viewCount Int @default(0) + owner UserLongLongLongLongName? + assetType String + + @@delegate(assetType) +} + +model VideoLongLongLongLongName1 extends AssetLongLongLongLongName { + duration Int +} + +model VideoLongLongLongLongName2 extends AssetLongLongLongLongName { + format String +} + `, + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1467.test.ts b/tests/regression/test/v2-migrated/issue-1467.test.ts new file mode 100644 index 00000000..042ef8b6 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1467.test.ts @@ -0,0 +1,44 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1467', async () => { + const db = await createTestClient( + ` + model User { + id Int @id @default(autoincrement()) + type String + } + + model Container { + id Int @id @default(autoincrement()) + drink Drink @relation(fields: [drinkId], references: [id]) + drinkId Int + } + + model Drink { + id Int @id @default(autoincrement()) + name String @unique + containers Container[] + type String + + @@delegate(type) + } + + model Beer extends Drink { + } + `, + ); + + await db.beer.create({ + data: { id: 1, name: 'Beer1' }, + }); + + await db.container.create({ data: { drink: { connect: { id: 1 } } } }); + await db.container.create({ data: { drink: { connect: { id: 1 } } } }); + + const beers = await db.beer.findFirst({ + select: { id: true, name: true, _count: { select: { containers: true } } }, + orderBy: { name: 'asc' }, + }); + expect(beers).toMatchObject({ _count: { containers: 2 } }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1483.test.ts b/tests/regression/test/v2-migrated/issue-1483.test.ts new file mode 100644 index 00000000..8802a312 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1483.test.ts @@ -0,0 +1,67 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1483', async () => { + const db = await createTestClient( + ` +model User { + @@auth + id String @id + edits Edit[] + @@allow('all', true) +} + +model Entity { + + id String @id @default(cuid()) + name String + edits Edit[] + + type String + @@delegate(type) + + @@allow('all', true) +} + +model Person extends Entity { +} + +model Edit { + id String @id @default(cuid()) + + authorId String? + author User? @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + entityId String + entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@allow('all', true) +} + `, + ); + + await db.edit.deleteMany({}); + await db.person.deleteMany({}); + await db.user.deleteMany({}); + + const person = await db.person.create({ + data: { + name: 'test', + }, + }); + + await db.edit.create({ + data: { + entityId: person.id, + }, + }); + + await expect( + db.edit.findMany({ + include: { + author: true, + entity: true, + }, + }), + ).resolves.toHaveLength(1); +}); diff --git a/tests/regression/test/v2-migrated/issue-1487.test.ts b/tests/regression/test/v2-migrated/issue-1487.test.ts new file mode 100644 index 00000000..acf39ead --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1487.test.ts @@ -0,0 +1,52 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import Decimal from 'decimal.js'; +import { expect, it } from 'vitest'; + +it('verifies issue 1487', async () => { + const db = await createTestClient( + ` +model LineItem { + id Int @id @default(autoincrement()) + price Decimal + createdAt DateTime @default(now()) + + orderId Int + order Order @relation(fields: [orderId], references: [id]) +} +model Order extends BaseType { + total Decimal + createdAt DateTime @default(now()) + lineItems LineItem[] +} +model BaseType { + id Int @id @default(autoincrement()) + entityType String + + @@delegate(entityType) +} + `, + ); + + const create = await db.Order.create({ + data: { + total: new Decimal(100_100.99), + lineItems: { create: [{ price: 90_000.66 }, { price: 20_100.33 }] }, + }, + }); + + const order = await db.Order.findFirst({ where: { id: create.id }, include: { lineItems: true } }); + expect(Decimal.isDecimal(order.total)).toBe(true); + expect(order.createdAt instanceof Date).toBe(true); + expect(order.total.toString()).toEqual('100100.99'); + order.lineItems.forEach((item: any) => { + expect(Decimal.isDecimal(item.price)).toBe(true); + expect(item.price.toString()).not.toEqual('[object Object]'); + }); + + const lineItems = await db.LineItem.findMany(); + lineItems.forEach((item: any) => { + expect(item.createdAt instanceof Date).toBe(true); + expect(Decimal.isDecimal(item.price)).toBe(true); + expect(item.price.toString()).not.toEqual('[object Object]'); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-764.test.ts b/tests/regression/test/v2-migrated/issue-764.test.ts index 404616fb..b34f1bac 100644 --- a/tests/regression/test/v2-migrated/issue-764.test.ts +++ b/tests/regression/test/v2-migrated/issue-764.test.ts @@ -24,7 +24,7 @@ model Post { `, ); - const user = await db.$unuseAll().user.create({ + const user = await db.user.create({ data: { name: 'Me' }, }); From 332b1dbb3c89713753c1a9df94127eb1bab64f9e Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 12 Oct 2025 12:55:30 -0700 Subject: [PATCH 09/17] fix(delegate): column name mapping issue when delegates are involved (#296) * fix(delegate): column name mapping issue when delegates are involved * fix build * fix tests --- .../src/client/crud/dialects/base-dialect.ts | 38 ++-- .../src/client/crud/dialects/postgresql.ts | 2 +- .../src/client/crud/operations/aggregate.ts | 7 +- .../src/client/crud/operations/base.ts | 4 +- .../src/client/crud/operations/count.ts | 3 +- .../src/client/executor/name-mapper.ts | 16 +- .../orm/client-api/computed-fields.test.ts | 3 +- .../test/v2-migrated/issue-1235.test.ts | 3 +- .../test/v2-migrated/issue-1506.test.ts | 35 +++ .../test/v2-migrated/issue-1507.test.ts | 25 +++ .../test/v2-migrated/issue-1518.test.ts | 30 +++ .../test/v2-migrated/issue-1520.test.ts | 67 ++++++ .../test/v2-migrated/issue-1522.test.ts | 90 ++++++++ .../test/v2-migrated/issue-1530.test.ts | 34 +++ .../test/v2-migrated/issue-1533.test.ts | 53 +++++ .../test/v2-migrated/issue-1551.test.ts | 26 +++ .../test/v2-migrated/issue-1562.test.ts | 25 +++ .../test/v2-migrated/issue-1563.test.ts | 26 +++ .../test/v2-migrated/issue-1574.test.ts | 107 ++++++++++ .../test/v2-migrated/issue-1575.test.ts | 29 +++ .../test/v2-migrated/issue-1576.test.ts | 61 ++++++ .../test/v2-migrated/issue-1585.test.ts | 29 +++ .../test/v2-migrated/issue-1627.test.ts | 49 +++++ .../test/v2-migrated/issue-1642.test.ts | 40 ++++ .../test/v2-migrated/issue-1644.test.ts | 24 +++ .../test/v2-migrated/issue-1645.test.ts | 201 ++++++++++++++++++ 26 files changed, 998 insertions(+), 29 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-1506.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1507.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1518.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1520.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1522.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1530.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1533.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1551.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1562.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1563.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1574.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1575.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1576.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1585.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1627.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1642.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1644.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1645.test.ts diff --git a/packages/runtime/src/client/crud/dialects/base-dialect.ts b/packages/runtime/src/client/crud/dialects/base-dialect.ts index 642297b8..17d300ac 100644 --- a/packages/runtime/src/client/crud/dialects/base-dialect.ts +++ b/packages/runtime/src/client/crud/dialects/base-dialect.ts @@ -102,7 +102,7 @@ export abstract class BaseCrudDialect { if ('distinct' in args && (args as any).distinct) { const distinct = ensureArray((args as any).distinct) as string[]; if (this.supportsDistinctOn) { - result = result.distinctOn(distinct.map((f) => sql.ref(`${modelAlias}.${f}`))); + result = result.distinctOn(distinct.map((f) => this.eb.ref(`${modelAlias}.${f}`))); } else { throw new QueryError(`"distinct" is not supported by "${this.schema.provider.type}" provider`); } @@ -248,7 +248,7 @@ export abstract class BaseCrudDialect { if (ownedByModel && !fieldDef.originModel) { // can be short-circuited to FK null check - return this.and(...keyPairs.map(({ fk }) => this.eb(sql.ref(`${modelAlias}.${fk}`), 'is', null))); + return this.and(...keyPairs.map(({ fk }) => this.eb(this.eb.ref(`${modelAlias}.${fk}`), 'is', null))); } else { // translate it to `{ is: null }` filter return this.buildToOneRelationFilter(model, modelAlias, field, fieldDef, { is: null }); @@ -268,7 +268,9 @@ export abstract class BaseCrudDialect { const joinSelect = this.eb .selectFrom(`${fieldDef.type} as ${joinAlias}`) - .where(() => this.and(...joinPairs.map(([left, right]) => this.eb(sql.ref(left), '=', sql.ref(right))))) + .where(() => + this.and(...joinPairs.map(([left, right]) => this.eb(this.eb.ref(left), '=', this.eb.ref(right)))), + ) .select(() => this.eb.fn.count(this.eb.lit(1)).as(filterResultField)); const conditions: Expression[] = []; @@ -331,7 +333,7 @@ export abstract class BaseCrudDialect { ) { // null check needs to be converted to fk "is null" checks if (payload === null) { - return this.eb(sql.ref(`${modelAlias}.${field}`), 'is', null); + return this.eb(this.eb.ref(`${modelAlias}.${field}`), 'is', null); } const relationModel = fieldDef.type; @@ -351,15 +353,15 @@ export abstract class BaseCrudDialect { invariant(relationIdFields.length === 1, 'many-to-many relation must have exactly one id field'); return eb( - sql.ref(`${relationFilterSelectAlias}.${relationIdFields[0]}`), + this.eb.ref(`${relationFilterSelectAlias}.${relationIdFields[0]}`), 'in', eb .selectFrom(m2m.joinTable) .select(`${m2m.joinTable}.${m2m.otherFkName}`) .whereRef( - sql.ref(`${m2m.joinTable}.${m2m.parentFkName}`), + this.eb.ref(`${m2m.joinTable}.${m2m.parentFkName}`), '=', - sql.ref(`${modelAlias}.${modelIdFields[0]}`), + this.eb.ref(`${modelAlias}.${modelIdFields[0]}`), ), ); } else { @@ -370,12 +372,20 @@ export abstract class BaseCrudDialect { if (relationKeyPairs.ownedByModel) { result = this.and( result, - eb(sql.ref(`${modelAlias}.${fk}`), '=', sql.ref(`${relationFilterSelectAlias}.${pk}`)), + eb( + this.eb.ref(`${modelAlias}.${fk}`), + '=', + this.eb.ref(`${relationFilterSelectAlias}.${pk}`), + ), ); } else { result = this.and( result, - eb(sql.ref(`${modelAlias}.${pk}`), '=', sql.ref(`${relationFilterSelectAlias}.${fk}`)), + eb( + this.eb.ref(`${modelAlias}.${pk}`), + '=', + this.eb.ref(`${relationFilterSelectAlias}.${fk}`), + ), ); } } @@ -833,7 +843,9 @@ export abstract class BaseCrudDialect { const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, subQueryAlias); subQuery = subQuery.where(() => this.and( - ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))), + ...joinPairs.map(([left, right]) => + eb(this.eb.ref(left), '=', this.eb.ref(right)), + ), ), ); subQuery = subQuery.select(() => eb.fn.count(eb.lit(1)).as('_count')); @@ -845,7 +857,9 @@ export abstract class BaseCrudDialect { result = result.leftJoin(relationModel, (join) => { const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel); return join.on((eb) => - this.and(...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right)))), + this.and( + ...joinPairs.map(([left, right]) => eb(this.eb.ref(left), '=', this.eb.ref(right))), + ), ); }); result = this.buildOrderBy(result, fieldDef.type, relationModel, value, false, negated); @@ -934,7 +948,7 @@ export abstract class BaseCrudDialect { return query.select(() => this.fieldRef(model, field, modelAlias).as(field)); } else if (!fieldDef.originModel) { // regular field - return query.select(sql.ref(`${modelAlias}.${field}`).as(field)); + return query.select(this.eb.ref(`${modelAlias}.${field}`).as(field)); } else { return this.buildSelectField(query, fieldDef.originModel, fieldDef.originModel, field); } diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index 0a60c350..2aec3a67 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -231,7 +231,7 @@ export class PostgresCrudDialect extends BaseCrudDiale } else { const joinPairs = buildJoinPairs(this.schema, model, parentAlias, relationField, relationModelAlias); query = query.where((eb) => - this.and(...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right)))), + this.and(...joinPairs.map(([left, right]) => eb(this.eb.ref(left), '=', this.eb.ref(right)))), ); } return query; diff --git a/packages/runtime/src/client/crud/operations/aggregate.ts b/packages/runtime/src/client/crud/operations/aggregate.ts index 5df07608..6362fbe6 100644 --- a/packages/runtime/src/client/crud/operations/aggregate.ts +++ b/packages/runtime/src/client/crud/operations/aggregate.ts @@ -1,4 +1,3 @@ -import { sql } from 'kysely'; import { match } from 'ts-pattern'; import type { SchemaDef } from '../../../schema'; import { getField } from '../../query-utils'; @@ -80,7 +79,9 @@ export class AggregateOperationHandler extends BaseOpe ); } else { query = query.select((eb) => - eb.cast(eb.fn.count(sql.ref(`$sub.${field}`)), 'integer').as(`${key}.${field}`), + eb + .cast(eb.fn.count(eb.ref(`$sub.${field}` as any)), 'integer') + .as(`${key}.${field}`), ); } } @@ -102,7 +103,7 @@ export class AggregateOperationHandler extends BaseOpe .with('_max', () => eb.fn.max) .with('_min', () => eb.fn.min) .exhaustive(); - return fn(sql.ref(`$sub.${field}`)).as(`${key}.${field}`); + return fn(eb.ref(`$sub.${field}` as any)).as(`${key}.${field}`); }); } }); diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 1832874a..a33913d3 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -1540,7 +1540,9 @@ export abstract class BaseOperationHandler { if (!relationFieldDef.array) { const query = kysely .updateTable(model) - .where((eb) => eb.and(keyPairs.map(({ fk, pk }) => eb(sql.ref(fk), '=', fromRelation.ids[pk])))) + .where((eb) => + eb.and(keyPairs.map(({ fk, pk }) => eb(eb.ref(fk as any), '=', fromRelation.ids[pk]))), + ) .set(keyPairs.reduce((acc, { fk }) => ({ ...acc, [fk]: null }), {} as any)) .modifyEnd( this.makeContextComment({ diff --git a/packages/runtime/src/client/crud/operations/count.ts b/packages/runtime/src/client/crud/operations/count.ts index 90451745..0b31d795 100644 --- a/packages/runtime/src/client/crud/operations/count.ts +++ b/packages/runtime/src/client/crud/operations/count.ts @@ -1,4 +1,3 @@ -import { sql } from 'kysely'; import type { SchemaDef } from '../../../schema'; import { BaseOperationHandler } from './base'; @@ -40,7 +39,7 @@ export class CountOperationHandler extends BaseOperati Object.keys(parsedArgs.select!).map((key) => key === '_all' ? eb.cast(eb.fn.countAll(), 'integer').as('_all') - : eb.cast(eb.fn.count(sql.ref(`${subQueryName}.${key}`)), 'integer').as(key), + : eb.cast(eb.fn.count(eb.ref(`${subQueryName}.${key}` as any)), 'integer').as(key), ), ); const result = await this.executeQuery(this.kysely, query, 'count'); diff --git a/packages/runtime/src/client/executor/name-mapper.ts b/packages/runtime/src/client/executor/name-mapper.ts index 410aa7b7..dcea8152 100644 --- a/packages/runtime/src/client/executor/name-mapper.ts +++ b/packages/runtime/src/client/executor/name-mapper.ts @@ -38,10 +38,10 @@ export class QueryNameMapper extends OperationNodeTransformer { this.modelToTableMap.set(modelName, mappedName); } - for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + for (const fieldDef of this.getModelFields(modelDef)) { const mappedName = this.getMappedName(fieldDef); if (mappedName) { - this.fieldToColumnMap.set(`${modelName}.${fieldName}`, mappedName); + this.fieldToColumnMap.set(`${modelName}.${fieldDef.name}`, mappedName); } } } @@ -72,11 +72,14 @@ export class QueryNameMapper extends OperationNodeTransformer { on: this.transformNode(join.on), })) : undefined; + const selections = this.processSelectQuerySelections(node); + const baseResult = super.transformSelectQuery(node); + return { - ...super.transformSelectQuery(node), + ...baseResult, from: FromNode.create(processedFroms.map((f) => f.node)), joins, - selections: this.processSelectQuerySelections(node), + selections, }; }); } @@ -132,7 +135,8 @@ export class QueryNameMapper extends OperationNodeTransformer { mappedTableName ? TableNode.create(mappedTableName) : undefined, ); } else { - return super.transformReference(node); + // no name mapping needed + return node; } } @@ -270,7 +274,7 @@ export class QueryNameMapper extends OperationNodeTransformer { if (!modelDef) { continue; } - if (modelDef.fields[name]) { + if (this.getModelFields(modelDef).some((f) => f.name === name)) { return scope; } } diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index 4969d21d..84d006b8 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1,4 +1,3 @@ -import { sql } from '@zenstackhq/runtime/helpers'; import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, describe, expect, it } from 'vitest'; @@ -226,7 +225,7 @@ model Post { postCount: (eb: any, context: { modelAlias: string }) => eb .selectFrom('Post') - .whereRef('Post.authorId', '=', sql.ref(`${context.modelAlias}.id`)) + .whereRef('Post.authorId', '=', eb.ref(`${context.modelAlias}.id`)) .select(() => eb.fn.countAll().as('count')), }, }, diff --git a/tests/regression/test/v2-migrated/issue-1235.test.ts b/tests/regression/test/v2-migrated/issue-1235.test.ts index e5d17b6a..93b637af 100644 --- a/tests/regression/test/v2-migrated/issue-1235.test.ts +++ b/tests/regression/test/v2-migrated/issue-1235.test.ts @@ -1,4 +1,4 @@ -import { createPolicyTestClient, testLogger } from '@zenstackhq/testtools'; +import { createPolicyTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; describe('Regression for issue 1235', () => { @@ -11,7 +11,6 @@ model Post { @@allow('all', true) } `, - { log: testLogger }, ); const post = await db.post.create({ data: {} }); diff --git a/tests/regression/test/v2-migrated/issue-1506.test.ts b/tests/regression/test/v2-migrated/issue-1506.test.ts new file mode 100644 index 00000000..759ec279 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1506.test.ts @@ -0,0 +1,35 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1506', async () => { + await createPolicyTestClient( + ` +model A { + id Int @id @default(autoincrement()) + value Int + b B @relation(fields: [bId], references: [id]) + bId Int @unique + + @@allow('read', true) +} + +model B { + id Int @id @default(autoincrement()) + value Int + a A? + c C @relation(fields: [cId], references: [id]) + cId Int @unique + + @@allow('read', value > c.value) +} + +model C { + id Int @id @default(autoincrement()) + value Int + b B? + + @@allow('read', true) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1507.test.ts b/tests/regression/test/v2-migrated/issue-1507.test.ts new file mode 100644 index 00000000..aee6fddd --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1507.test.ts @@ -0,0 +1,25 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1507', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + age Int +} + +model Profile { + id Int @id @default(autoincrement()) + age Int + + @@allow('read', auth().age == age) +} + `, + ); + + await db.$unuseAll().profile.create({ data: { age: 18 } }); + await db.$unuseAll().profile.create({ data: { age: 20 } }); + await expect(db.$setAuth({ id: 1, age: 18 }).profile.findMany()).resolves.toHaveLength(1); + await expect(db.$setAuth({ id: 1, age: 18 }).profile.count()).resolves.toBe(1); +}); diff --git a/tests/regression/test/v2-migrated/issue-1518.test.ts b/tests/regression/test/v2-migrated/issue-1518.test.ts new file mode 100644 index 00000000..1dfa436e --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1518.test.ts @@ -0,0 +1,30 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1518', async () => { + const db = await createTestClient( + ` +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) +} + `, + ); + + await db.taskActivity.create({ + data: { + id: '00000000-0000-0000-0000-111111111111', + title: 'Test Activity', + description: 'Description of task', + }, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1520.test.ts b/tests/regression/test/v2-migrated/issue-1520.test.ts new file mode 100644 index 00000000..79b8fc54 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1520.test.ts @@ -0,0 +1,67 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1520', async () => { + const db = await createTestClient( + ` +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) +} + `, + ); + + 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/test/v2-migrated/issue-1522.test.ts b/tests/regression/test/v2-migrated/issue-1522.test.ts new file mode 100644 index 00000000..da18046a --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1522.test.ts @@ -0,0 +1,90 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1522', async () => { + const db = await createTestClient( + ` +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 +} + `, + ); + + 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', + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1530.test.ts b/tests/regression/test/v2-migrated/issue-1530.test.ts new file mode 100644 index 00000000..55fb1089 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1530.test.ts @@ -0,0 +1,34 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1530', async () => { + const db = await createTestClient( + ` +model Category { + id Int @id @default(autoincrement()) + name String @unique + + parentId Int? + parent Category? @relation("ParentChildren", fields: [parentId], references: [id]) + children Category[] @relation("ParentChildren") + @@allow('all', true) +} + `, + { usePrismaPush: true }, + ); + + await db.$unuseAll().category.create({ + data: { id: 1, name: 'C1' }, + }); + + await db.category.update({ + where: { id: 1 }, + data: { parent: { connect: { id: 1 } } }, + }); + + const r = await db.category.update({ + where: { id: 1 }, + data: { parent: { disconnect: true } }, + }); + expect(r.parent).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1533.test.ts b/tests/regression/test/v2-migrated/issue-1533.test.ts new file mode 100644 index 00000000..4b1e131b --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1533.test.ts @@ -0,0 +1,53 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: JSON null support +it.skip('verifies issue 1533', async () => { + const db = await createTestClient( + ` +model Test { + id String @id @default(uuid()) @db.Uuid + metadata Json + @@allow('all', true) +} + `, + ); + + const testWithMetadata = await db.test.create({ + data: { + metadata: { + test: 'test', + }, + }, + }); + const testWithEmptyMetadata = await db.test.create({ + data: { + metadata: {}, + }, + }); + + let result = await db.test.findMany({ + where: { + metadata: { + path: ['test'], + // @ts-expect-error + equals: Prisma.DbNull, + }, + }, + }); + + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ id: testWithEmptyMetadata.id })])); + + result = await db.test.findMany({ + where: { + metadata: { + path: ['test'], + equals: 'test', + }, + }, + }); + + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ id: testWithMetadata.id })])); +}); diff --git a/tests/regression/test/v2-migrated/issue-1551.test.ts b/tests/regression/test/v2-migrated/issue-1551.test.ts new file mode 100644 index 00000000..441eed43 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1551.test.ts @@ -0,0 +1,26 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1551', async () => { + await loadSchema( + ` +model User { + id Int @id + profile Profile? @relation(fields: [profileId], references: [id]) + profileId Int? @unique @map('profile_id') +} + +model Profile { + id Int @id + contentType String + user User? + + @@delegate(contentType) +} + +model IndividualProfile extends Profile { + name String +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1562.test.ts b/tests/regression/test/v2-migrated/issue-1562.test.ts new file mode 100644 index 00000000..98e3e98a --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1562.test.ts @@ -0,0 +1,25 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1562', async () => { + const db = await createTestClient( + ` +type Base { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt() + + // require login + @@allow('all', true) +} + +model User with Base { + name String @unique @regex('^[a-zA-Z0-9_]{3,30}$') + + @@allow('read', true) +} + `, + ); + + await expect(db.user.create({ data: { name: '1 2 3 4' } })).toBeRejectedByValidation(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1563.test.ts b/tests/regression/test/v2-migrated/issue-1563.test.ts new file mode 100644 index 00000000..5b7c1b28 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1563.test.ts @@ -0,0 +1,26 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1563', async () => { + const db = await createTestClient( + ` +model ModelA { + id String @id @default(cuid()) + ref ModelB[] +} + +model ModelB { + id String @id @default(cuid()) + ref ModelA? @relation(fields: [refId], references: [id]) + refId String? + + @@validate(refId != null, "refId must be set") +} + `, + ); + + const a = await db.modelA.create({ data: {} }); + const b = await db.modelB.create({ data: { refId: a.id } }); + + await expect(db.modelB.update({ where: { id: b.id }, data: { refId: a.id } })).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1574.test.ts b/tests/regression/test/v2-migrated/issue-1574.test.ts new file mode 100644 index 00000000..895852b3 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1574.test.ts @@ -0,0 +1,107 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 1574', async () => { + const db = await createPolicyTestClient( + ` +model User { + id String @id @default(cuid()) + modelA ModelA[] +} + +// +// ModelA has model-level access-all by owner, but read-all override for the name property +// +model ModelA { + id String @id @default(cuid()) + + owner User @relation(fields: [ownerId], references: [id]) + ownerId String + + name String @allow('read', true, true) + prop2 String? + + refsB ModelB[] + refsC ModelC[] + + @@allow('all', owner == auth()) +} + +// +// ModelB and ModelC are both allow-all everyone. +// They both have a reference to ModelA, but in ModelB that reference is optional. +// +model ModelB { + id String @id @default(cuid()) + + ref ModelA? @relation(fields: [refId], references: [id]) + refId String? + + @@allow('all', true) +} +model ModelC { + id String @id @default(cuid()) + + ref ModelA @relation(fields: [refId], references: [id]) + refId String + + @@allow('all', true) +} + `, + ); + + // create two users + const user1 = await db.$unuseAll().user.create({ data: { id: '1' } }); + const user2 = await db.$unuseAll().user.create({ data: { id: '2' } }); + + // create two db instances, enhanced for users 1 and 2 + const db1 = db.$setAuth(user1); + const db2 = db.$setAuth(user2); + + // create a ModelA owned by user1 + const a = await db1.modelA.create({ data: { name: 'a', ownerId: user1.id } }); + + // create a ModelB and a ModelC with refs to ModelA + await db1.modelB.create({ data: { refId: a.id } }); + await db2.modelC.create({ data: { refId: a.id } }); + + // works: user1 should be able to read b as well as the entire referenced a + const t1 = await db1.modelB.findFirst({ select: { ref: true } }); + expect(t1.ref.name).toBeTruthy(); + + // works: user1 also should be able to read b as well as the name of the referenced a + const t2 = await db1.modelB.findFirst({ select: { ref: { select: { name: true } } } }); + expect(t2.ref.name).toBeTruthy(); + + // works: user2 also should be able to read b as well as the name of the referenced a + const t3 = await db2.modelB.findFirst({ select: { ref: { select: { name: true } } } }); + expect(t3.ref.name).toBeTruthy(); + + // works: but user2 should not be able to read b with the entire referenced a + const t4 = await db2.modelB.findFirst({ select: { ref: true } }); + expect(t4.ref).toBeFalsy(); + + // + // The following are essentially the same tests, but with ModelC instead of ModelB + // + + // works: user1 should be able to read c as well as the entire referenced a + const t5 = await db1.modelC.findFirst({ select: { ref: true } }); + expect(t5.ref.name).toBeTruthy(); + + // works: user1 also should be able to read c as well as the name of the referenced a + const t6 = await db1.modelC.findFirst({ select: { ref: { select: { name: true } } } }); + expect(t6.ref.name).toBeTruthy(); + + // works: user2 should not be able to read b along with the a reference. + // In this case, the entire query returns null because of the required (but inaccessible) ref. + await expect(db2.modelC.findFirst({ select: { ref: true } })).toResolveFalsy(); + + // works: if user2 queries c directly and gets the refId to a, it can get the a.name directly + const t7 = await db2.modelC.findFirstOrThrow(); + await expect(db2.modelA.findFirst({ select: { name: true }, where: { id: t7.refId } })).toResolveTruthy(); + + // fails: since the last query worked, we'd expect to be able to query c along with the name of the referenced a directly + await expect(db2.modelC.findFirst({ select: { ref: { select: { name: true } } } })).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1575.test.ts b/tests/regression/test/v2-migrated/issue-1575.test.ts new file mode 100644 index 00000000..c09ac5aa --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1575.test.ts @@ -0,0 +1,29 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1575', async () => { + await loadSchema( + ` +model UserAssets { + id String @id @default(cuid()) + videoId String + videoStream Asset @relation("userVideo", fields: [videoId], references: [id]) + subtitleId String + subtitlesAsset Asset @relation("userSubtitles", fields: [subtitleId], references: [id]) +} + +model Asset { + id String @id @default(cuid()) + type String + userVideo UserAssets[] @relation("userVideo") + userSubtitles UserAssets[] @relation("userSubtitles") + + @@delegate(type) +} + +model Movie extends Asset { + duration Int +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1576.test.ts b/tests/regression/test/v2-migrated/issue-1576.test.ts new file mode 100644 index 00000000..078fd7cc --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1576.test.ts @@ -0,0 +1,61 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1576', async () => { + const db = await createTestClient( + ` +model Profile { + id Int @id @default(autoincrement()) + name String + items Item[] + type String + @@delegate(type) + @@allow('all', true) +} + +model GoldProfile extends Profile { + ticket Int +} + +model Item { + id Int @id @default(autoincrement()) + profileId Int + profile Profile @relation(fields: [profileId], references: [id]) + type String + @@delegate(type) + @@allow('all', true) +} + +model GoldItem extends Item { + inventory Boolean +} + `, + ); + + const profile = await db.goldProfile.create({ + data: { + name: 'hello', + ticket: 5, + }, + }); + + await expect( + db.goldItem.createManyAndReturn({ + data: [ + { + profileId: profile.id, + inventory: true, + }, + { + profileId: profile.id, + inventory: true, + }, + ], + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + ]), + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1585.test.ts b/tests/regression/test/v2-migrated/issue-1585.test.ts new file mode 100644 index 00000000..388e0cde --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1585.test.ts @@ -0,0 +1,29 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1585', async () => { + const db = await createTestClient( + ` + model Asset { + id Int @id @default(autoincrement()) + type String + views Int + + @@allow('all', true) + @@delegate(type) + } + + model Post extends Asset { + title String + } + `, + ); + + await db.post.create({ data: { title: 'Post1', views: 0 } }); + await db.post.create({ data: { title: 'Post2', views: 1 } }); + await expect( + db.post.count({ + where: { views: { gt: 0 } }, + }), + ).resolves.toBe(1); +}); diff --git a/tests/regression/test/v2-migrated/issue-1627.test.ts b/tests/regression/test/v2-migrated/issue-1627.test.ts new file mode 100644 index 00000000..a086b42f --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1627.test.ts @@ -0,0 +1,49 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1627', async () => { + const db = await createPolicyTestClient( + ` +model User { + id String @id + memberships GymUser[] +} + +model Gym { + id String @id + members GymUser[] + + @@allow('all', true) +} + +model GymUser { + id String @id + userID String + user User @relation(fields: [userID], references: [id]) + gymID String? + gym Gym? @relation(fields: [gymID], references: [id]) + role String + + @@allow('read',gym.members?[user == auth() && (role == "ADMIN" || role == "TRAINER")]) + @@unique([userID, gymID]) +} + `, + ); + + await db.$unuseAll().user.create({ data: { id: '1' } }); + + await db.$unuseAll().gym.create({ + data: { + id: '1', + members: { + create: { + id: '1', + user: { connect: { id: '1' } }, + role: 'ADMIN', + }, + }, + }, + }); + + await expect(db.gymUser.findMany()).resolves.toHaveLength(0); +}); diff --git a/tests/regression/test/v2-migrated/issue-1642.test.ts b/tests/regression/test/v2-migrated/issue-1642.test.ts new file mode 100644 index 00000000..3fc9b542 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1642.test.ts @@ -0,0 +1,40 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1642', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id + name String + posts Post[] + + @@allow('read', true) + @@allow('all', auth().id == 1) +} + +model Post { + id Int @id + title String + description String + author User @relation(fields: [authorId], references: [id]) + authorId Int + + // delegate all access policies to the author: + @@allow('all', check(author)) + @@allow('update', true) + @@allow('post-update', title == 'hello') +} + `, + ); + + await db.$unuseAll().user.create({ data: { id: 1, name: 'User1' } }); + await db.$unuseAll().post.create({ data: { id: 1, title: 'hello', description: 'desc1', authorId: 1 } }); + + const authDb = db.$setAuth({ id: 2 }); + await expect( + authDb.post.update({ where: { id: 1 }, data: { title: 'world', description: 'desc2' } }), + ).toBeRejectedByPolicy(); + + await expect(authDb.post.update({ where: { id: 1 }, data: { description: 'desc2' } })).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1644.test.ts b/tests/regression/test/v2-migrated/issue-1644.test.ts new file mode 100644 index 00000000..e6c691d2 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1644.test.ts @@ -0,0 +1,24 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 1644', async () => { + const db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + email String @unique @email @length(6, 32) @allow('read', auth() == this) + + // full access to all + @@allow('all', true) +} + `, + ); + + await db.$unuseAll().user.create({ data: { id: 1, email: 'a@example.com' } }); + await db.$unuseAll().user.create({ data: { id: 2, email: 'b@example.com' } }); + + const authDb = db.$setAuth({ id: 1 }); + await expect(authDb.user.count({ where: { email: { contains: 'example.com' } } })).resolves.toBe(1); + await expect(authDb.user.findMany({ where: { email: { contains: 'example.com' } } })).resolves.toHaveLength(1); +}); diff --git a/tests/regression/test/v2-migrated/issue-1645.test.ts b/tests/regression/test/v2-migrated/issue-1645.test.ts new file mode 100644 index 00000000..a04fca7d --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1645.test.ts @@ -0,0 +1,201 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1645', async () => { + const db = await createPolicyTestClient( + ` +model Product { + id String @id @default(cuid()) + name String + slug String + description String? + sku String + price Int + onSale Boolean @default(false) + salePrice Int @default(0) + saleStartDateTime DateTime? + saleEndDateTime DateTime? + scheduledAvailability Boolean @default(false) + availabilityStartDateTime DateTime? + availabilityEndDateTime DateTime? + type String @default('VARIABLE') + image String + orderItems OrderItem[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([slug]) + + @@allow('all', true) +} + +model BaseOrder { + id String @id @default(cuid()) + orderNumber String @unique @default(nanoid(16)) + lineItems OrderItem[] + status String @default('PENDING') + type String @default('PARENT') + userType String? + billingAddress BillingAddress @relation(fields: [billingAddressId], references: [id]) + billingAddressId String @map("billing_address_id") + shippingAddress ShippingAddress @relation(fields: [shippingAddressId], references: [id]) + shippingAddressId String @map("shipping_address_id") + notes String? @default('') + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@allow('all', true) + @@delegate(userType) +} + +model Order extends BaseOrder { + parentId String? @map("parent_id") + parent Order? @relation("OrderToParent", fields: [parentId], references: [id]) + groupedOrders Order[] @relation("OrderToParent") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @map("user_id") +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + orders Order[] + billingAddresses BillingAddress[] + shippingAddresses ShippingAddress[] + + @@allow('create,read', true) + @@allow('update,delete', auth().id == this.id) +} + +model GuestUser { + id String @id @default(cuid()) + name String? + email String @unique + orders GuestOrder[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@auth + @@allow('all', true) +} + +model GuestOrder extends BaseOrder { + guestUser GuestUser @relation(fields: [guestUserId], references: [id], onDelete: Cascade) + guestUserId String @map("guest_user_id") + parentId String? @map("parent_id") + parent GuestOrder? @relation("OrderToParent", fields: [parentId], references: [id]) + groupedOrders GuestOrder[] @relation("OrderToParent") +} + +model OrderItem { + id String @id @default(cuid()) + order BaseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade) + orderId String @map("order_id") + product Product @relation(fields: [productId], references: [id]) + productId String @map("product_id") + quantity Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@allow('all', true) +} + +model OrderAddress { + id String @id @default(cuid()) + firstName String + lastName String + address1 String + address2 String? + city String + state String + postalCode String + country String + email String + phone String + type String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@allow('all', true) + @@delegate(type) +} + +model BillingAddress extends OrderAddress { + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @map("user_id") + order BaseOrder[] +} + +model ShippingAddress extends OrderAddress { + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @map("user_id") + order BaseOrder[] +} + `, + { usePrismaPush: true }, + ); + + await db.user.create({ data: { id: '1', name: 'John', email: 'john@example.com' } }); + + const shipping = await db.shippingAddress.create({ + data: { + id: '1', + firstName: 'John', + lastName: 'Doe', + address1: '123 Main St', + city: 'Anytown', + state: 'CA', + postalCode: '12345', + country: 'US', + email: 'john@example.com', + phone: '123-456-7890', + user: { connect: { id: '1' } }, + }, + }); + + const billing = await db.billingAddress.create({ + data: { + id: '2', + firstName: 'John', + lastName: 'Doe', + address1: '123 Main St', + city: 'Anytown', + state: 'CA', + postalCode: '12345', + country: 'US', + email: 'john@example.com', + phone: '123-456-7890', + user: { connect: { id: '1' } }, + }, + }); + + await db.order.create({ + data: { + id: '1', + orderNumber: '1', + status: 'PENDING', + type: 'PARENT', + shippingAddress: { connect: { id: '1' } }, + billingAddress: { connect: { id: '2' } }, + user: { connect: { id: '1' } }, + }, + }); + + const updated = await db.order.update({ + where: { id: '1' }, + include: { + lineItems: true, + billingAddress: true, + shippingAddress: true, + }, + data: { + type: 'CAMPAIGN', + }, + }); + + expect(updated.shippingAddress).toEqual(shipping); + expect(updated.billingAddress).toEqual(billing); +}); From 8d9f2960a82fc74f6dc7a0939a924cce8a3e0fba Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 12 Oct 2025 22:21:40 -0700 Subject: [PATCH 10/17] fix: relation include orderBy validation issue, more test migrations (#297) --- packages/language/src/utils.ts | 7 +- .../src/client/crud/validator/index.ts | 4 +- packages/testtools/src/schema.ts | 2 +- .../test/v2-migrated/issue-1647.test.ts | 51 +++++++++ .../test/v2-migrated/issue-1648.test.ts | 42 +++++++ .../test/v2-migrated/issue-1674.test.ts | 80 ++++++++++++++ .../test/v2-migrated/issue-1681.test.ts | 29 +++++ .../test/v2-migrated/issue-1693.test.ts | 18 +++ .../test/v2-migrated/issue-1695.test.ts | 21 ++++ .../test/v2-migrated/issue-1698.test.ts | 72 ++++++++++++ .../test/v2-migrated/issue-1734.test.ts | 104 ++++++++++++++++++ .../test/v2-migrated/issue-1745.test.ts | 94 ++++++++++++++++ .../test/v2-migrated/issue-1755.test.ts | 58 ++++++++++ .../test/v2-migrated/issue-1758.test.ts | 27 +++++ .../test/v2-migrated/issue-1763.test.ts | 42 +++++++ .../test/v2-migrated/issue-1786.test.ts | 46 ++++++++ .../test/v2-migrated/issue-1835.test.ts | 23 ++++ .../test/v2-migrated/issue-1849.test.ts | 27 +++++ .../test/v2-migrated/issue-1857.test.ts | 39 +++++++ .../test/v2-migrated/issue-1870.test.ts | 14 +++ .../test/v2-migrated/issue-1894.test.ts | 46 ++++++++ 21 files changed, 841 insertions(+), 5 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-1647.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1648.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1674.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1681.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1693.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1695.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1698.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1734.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1745.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1755.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1758.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1763.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1786.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1835.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1849.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1857.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1870.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1894.test.ts diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index 19220f07..762187b3 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -166,10 +166,11 @@ export function getRecursiveBases( return result; } seen.add(decl); - decl.mixins.forEach((mixin) => { - // avoid using mixin.ref since this function can be called before linking + const bases = [...decl.mixins, ...(isDataModel(decl) && decl.baseModel ? [decl.baseModel] : [])]; + bases.forEach((base) => { + // avoid using .ref since this function can be called before linking const baseDecl = decl.$container.declarations.find( - (d): d is TypeDef => isTypeDef(d) && d.name === mixin.$refText, + (d): d is TypeDef | DataModel => isTypeDef(d) || (isDataModel(d) && d.name === base.$refText), ); if (baseDecl) { if (!includeDelegate && isDelegateModel(baseDecl)) { diff --git a/packages/runtime/src/client/crud/validator/index.ts b/packages/runtime/src/client/crud/validator/index.ts index 11d93350..fd3be7ac 100644 --- a/packages/runtime/src/client/crud/validator/index.ts +++ b/packages/runtime/src/client/crud/validator/index.ts @@ -677,7 +677,9 @@ export class InputValidator { ...(fieldDef.array ? { // to-many relations can be ordered, skipped, taken, and cursor-located - orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), + orderBy: z + .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true)) + .optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), cursor: this.makeCursorSchema(fieldDef.type).optional(), diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 7662fba0..516d445a 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -54,7 +54,7 @@ export async function generateTsSchema( if (extraSourceFiles) { for (const [fileName, content] of Object.entries(extraSourceFiles)) { - const filePath = path.resolve(workDir, `${fileName}.ts`); + const filePath = path.resolve(workDir, !fileName.endsWith('.ts') ? `${fileName}.ts` : fileName); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content); } diff --git a/tests/regression/test/v2-migrated/issue-1647.test.ts b/tests/regression/test/v2-migrated/issue-1647.test.ts new file mode 100644 index 00000000..faebabed --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1647.test.ts @@ -0,0 +1,51 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { describe, it } from 'vitest'; + +// TODO: multi-schema support +describe.skip('Regression for issue 1647', () => { + it('inherits @@schema by default', async () => { + await loadSchema( + ` + model Asset { + id Int @id + type String + @@delegate(type) + @@schema('public') + } + + model Post extends Asset { + title String + } + `, + ); + }); + + it('respects sub model @@schema overrides', async () => { + await loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + schemas = ['public', 'post'] + } + + generator client { + provider = 'prisma-client-js' + previewFeatures = ['multiSchema'] + } + + model Asset { + id Int @id + type String + @@delegate(type) + @@schema('public') + } + + model Post extends Asset { + title String + @@schema('post') + } + `, + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1648.test.ts b/tests/regression/test/v2-migrated/issue-1648.test.ts new file mode 100644 index 00000000..7fe06686 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1648.test.ts @@ -0,0 +1,42 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1648', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + profile Profile? + posts Post[] +} + +model Profile { + id Int @id @default(autoincrement()) + someText String + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + +model Post { + id Int @id @default(autoincrement()) + title String + + userId Int + user User @relation(fields: [userId], references: [id]) + + // this will always be true, even if the someText field is "canUpdate" + @@deny("post-update", user.profile.someText != "canUpdate") + + @@allow("all", true) +} + `, + ); + + await db.$unuseAll().user.create({ data: { id: 1, profile: { create: { someText: 'canUpdate' } } } }); + await db.$unuseAll().user.create({ data: { id: 2, profile: { create: { someText: 'nothing' } } } }); + await db.$unuseAll().post.create({ data: { id: 1, title: 'Post1', userId: 1 } }); + await db.$unuseAll().post.create({ data: { id: 2, title: 'Post2', userId: 2 } }); + + await expect(db.post.update({ where: { id: 1 }, data: { title: 'Post1-1' } })).toResolveTruthy(); + await expect(db.post.update({ where: { id: 2 }, data: { title: 'Post2-2' } })).toBeRejectedByPolicy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1674.test.ts b/tests/regression/test/v2-migrated/issue-1674.test.ts new file mode 100644 index 00000000..759f7d82 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1674.test.ts @@ -0,0 +1,80 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1674', async () => { + const db = await createPolicyTestClient( + ` +model User { + id String @id @default(cuid()) + email String @unique @email @length(6, 32) + posts Post[] + + // everybody can signup + @@allow('create', true) + + // full access by self + @@allow('all', auth() == this) +} + +model Blog { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) + postId String? +} + +model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String @length(1, 256) + content String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String + + blogs Blog[] + + type String + + @@delegate(type) +} + +model PostA extends Post { +} + +model PostB extends Post { +} + `, + ); + + const user = await db.$unuseAll().user.create({ + data: { email: 'abc@def.com' }, + }); + + const blog = await db.$unuseAll().blog.create({ + data: {}, + }); + + const authDb = db.$setAuth(user); + await expect( + authDb.postA.create({ + data: { + content: 'content', + title: 'title', + blogs: { + connect: { + id: blog.id, + }, + }, + author: { + connect: { + id: user.id, + }, + }, + }, + }), + ).toBeRejectedByPolicy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1681.test.ts b/tests/regression/test/v2-migrated/issue-1681.test.ts new file mode 100644 index 00000000..5fb20312 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1681.test.ts @@ -0,0 +1,29 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1681', async () => { + const db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + posts Post[] + @@allow('all', true) +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int @default(auth().id) + @@allow('all', true) +} + `, + ); + + const authDb = db.$setAuth({ id: 1 }); + const user = await db.user.create({ data: {} }); + await expect(authDb.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 }); + + const r = await authDb.post.createManyAndReturn({ data: [{ title: 'Post2' }] }); + expect(r[0].authorId).toBe(user.id); +}); diff --git a/tests/regression/test/v2-migrated/issue-1693.test.ts b/tests/regression/test/v2-migrated/issue-1693.test.ts new file mode 100644 index 00000000..04c4075b --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1693.test.ts @@ -0,0 +1,18 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1693', async () => { + await loadSchema( + ` +model Animal { + id String @id @default(uuid()) + animalType String @default("") + @@delegate(animalType) +} + +model Dog extends Animal { + name String +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1695.test.ts b/tests/regression/test/v2-migrated/issue-1695.test.ts new file mode 100644 index 00000000..a713f1af --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1695.test.ts @@ -0,0 +1,21 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1695', async () => { + await loadSchema( + ` +type SoftDelete { + deleted Int @default(0) +} + +model MyModel with SoftDelete { + id String @id @default(cuid()) + name String + + @@deny('update', deleted != 0) + @@deny('post-update', deleted != 0) + @@deny('read', this.deleted != 0) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1698.test.ts b/tests/regression/test/v2-migrated/issue-1698.test.ts new file mode 100644 index 00000000..c93dfa03 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1698.test.ts @@ -0,0 +1,72 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1698', async () => { + const db = await createTestClient( + ` +model House { + id Int @id @default(autoincrement()) + doorTypeId Int + door Door @relation(fields: [doorTypeId], references: [id]) + houseType String + @@delegate(houseType) +} + +model PrivateHouse extends House { + size Int +} + +model Skyscraper extends House { + height Int +} + +model Door { + id Int @id @default(autoincrement()) + color String + doorType String + houses House[] + @@delegate(doorType) +} + +model IronDoor extends Door { + strength Int +} + +model WoodenDoor extends Door { + texture String +} + `, + ); + + const door1 = await db.ironDoor.create({ + data: { strength: 100, color: 'blue' }, + }); + console.log(door1); + + const door2 = await db.woodenDoor.create({ + data: { texture: 'pine', color: 'red' }, + }); + console.log(door2); + + const house1 = await db.privateHouse.create({ + data: { size: 5000, door: { connect: { id: door1.id } } }, + }); + console.log(house1); + + const house2 = await db.skyscraper.create({ + data: { height: 3000, door: { connect: { id: door2.id } } }, + }); + console.log(house2); + + const r1 = await db.privateHouse.findFirst({ include: { door: true } }); + console.log(r1); + expect(r1).toMatchObject({ + door: { color: 'blue', strength: 100 }, + }); + + const r2 = (await db.skyscraper.findMany({ include: { door: true } }))[0]; + console.log(r2); + expect(r2).toMatchObject({ + door: { color: 'red', texture: 'pine' }, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1734.test.ts b/tests/regression/test/v2-migrated/issue-1734.test.ts new file mode 100644 index 00000000..15061653 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1734.test.ts @@ -0,0 +1,104 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 1734', async () => { + const db = await createPolicyTestClient( + ` +type Base { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Profile with Base { + displayName String + type String + + @@allow('read', true) + @@delegate(type) +} + +model User extends Profile { + username String @unique + access Access[] + organization Organization[] +} + +model Access with Base { + user User @relation(fields: [userId], references: [id]) + userId String + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId String + + manage Boolean @default(false) + + superadmin Boolean @default(false) + + @@unique([userId,organizationId]) +} + +model Organization extends Profile { + owner User @relation(fields: [ownerId], references: [id]) + ownerId String @default(auth().id) + published Boolean @default(false) @allow('read', access?[user == auth()]) + access Access[] +} + + `, + ); + + const user = await db.$unuseAll().user.create({ + data: { + username: 'test', + displayName: 'test', + }, + }); + + const organization = await db.$unuseAll().organization.create({ + data: { + displayName: 'test', + owner: { + connect: { + id: user.id, + }, + }, + access: { + create: { + user: { + connect: { + id: user.id, + }, + }, + manage: true, + superadmin: true, + }, + }, + }, + }); + + const foundUser = await db.profile.findFirst({ + where: { + id: user.id, + }, + }); + expect(foundUser).toMatchObject(user); + + const foundOrg = await db.profile.findFirst({ + where: { + id: organization.id, + }, + }); + // published field not readable + expect(foundOrg).toMatchObject({ id: organization.id, displayName: 'test', type: 'Organization' }); + expect(foundOrg.published).toBeUndefined(); + + const foundOrg1 = await db.$setAuth({ id: user.id }).profile.findFirst({ + where: { + id: organization.id, + }, + }); + // published field readable + expect(foundOrg1.published).not.toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1745.test.ts b/tests/regression/test/v2-migrated/issue-1745.test.ts new file mode 100644 index 00000000..9140ad84 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1745.test.ts @@ -0,0 +1,94 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1745', async () => { + await loadSchema( + ` +datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') +} + +enum BuyerType { + STORE + RESTAURANT + WHOLESALER +} + +enum ChainStore { + ALL + CHAINSTORE_1 + CHAINSTORE_2 + CHAINSTORE_3 +} + +type Id { + id String @id @default(cuid()) +} + +type Base with Id { + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Ad with Base { + serial Int @unique @default(autoincrement()) + buyerTypes BuyerType[] + chainStores ChainStore[] + listPrice Float + isSold Boolean @default(false) + + supplier Supplier @relation(fields: [supplierId], references: [id]) + supplierId String @default(auth().companyId) + + // @@allow('all', auth().company.companyType == 'Buyer' && has(buyerTypes, auth().company.buyerType)) + // @@allow('all', auth().company.companyType == 'Supplier' && auth().companyId == supplierId) + // @@allow('all', auth().isAdmin) +} + +model Company with Base { + name String @unique + organizationNumber String @unique + users User[] + buyerType BuyerType + + companyType String + @@delegate(companyType) + + @@allow('read, update', auth().companyId == id) + @@allow('all', auth().isAdmin) +} + +model Buyer extends Company { + storeName String + type String + chainStore ChainStore @default(ALL) + + @@allow('read, update', auth().company.companyType == 'Buyer' && auth().companyId == id) + @@allow('all', auth().isAdmin) +} + +model Supplier extends Company { + ads Ad[] + + @@allow('all', auth().company.companyType == 'Supplier' && auth().companyId == id) + @@allow('all', auth().isAdmin) +} + +model User with Base { + firstName String + lastName String + email String @unique + username String @unique + isAdmin Boolean @default(false) + + company Company? @relation(fields: [companyId], references: [id]) + companyId String? + + @@allow('read', auth().id == id) + @@allow('read', auth().companyId == companyId) + @@allow('all', auth().isAdmin) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1755.test.ts b/tests/regression/test/v2-migrated/issue-1755.test.ts new file mode 100644 index 00000000..47ac92a1 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1755.test.ts @@ -0,0 +1,58 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1755', async () => { + const db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + contents Content[] +} + +model Content { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id]) + userId Int + contentType String + @@delegate(contentType) +} + +model Post extends Content { + title String +} + +model Video extends Content { + name String + duration Int +} + `, + ); + + const user = await db.user.create({ data: {} }); + const now = Date.now(); + await db.post.create({ + data: { title: 'post1', createdAt: new Date(now - 1000), user: { connect: { id: user.id } } }, + }); + await db.post.create({ + data: { title: 'post2', createdAt: new Date(now), user: { connect: { id: user.id } } }, + }); + + // scalar orderBy + await expect(db.post.findFirst({ orderBy: { createdAt: 'desc' } })).resolves.toMatchObject({ + title: 'post2', + }); + + // array orderBy + await expect(db.post.findFirst({ orderBy: [{ createdAt: 'desc' }] })).resolves.toMatchObject({ + title: 'post2', + }); + + // nested orderBy + await expect( + db.user.findFirst({ include: { contents: { orderBy: [{ createdAt: 'desc' }] } } }), + ).resolves.toMatchObject({ + id: user.id, + contents: [{ title: 'post2' }, { title: 'post1' }], + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1758.test.ts b/tests/regression/test/v2-migrated/issue-1758.test.ts new file mode 100644 index 00000000..d62282e6 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1758.test.ts @@ -0,0 +1,27 @@ +import { loadSchemaWithError } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1758', async () => { + await loadSchemaWithError( + ` +model Organization { + id String @id @default(cuid()) + contents Content[] @relation("OrganizationContents") +} + +model Content { + id String @id @default(cuid()) + contentType String + organization Organization @relation("OrganizationContents", fields: [organizationId], references: [id]) + organizationId String + @@delegate(contentType) +} + +model Store extends Content { + name String + @@unique([organizationId, name]) +} + `, + 'Cannot use fields inherited from a polymorphic base model in `@@unique`', + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1763.test.ts b/tests/regression/test/v2-migrated/issue-1763.test.ts new file mode 100644 index 00000000..ec3d5748 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1763.test.ts @@ -0,0 +1,42 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1763', async () => { + await createTestClient( + ` +model Post { + id Int @id @default(autoincrement()) + name String + + type String + @@delegate(type) + + // full access by author + @@allow('all', true) +} + +model ConcretePost extends Post { + age Int +} + `, + + { + extraSourceFiles: { + main: ` +import { ZenStackClient } from '@zenstackhq/runtime'; +import { schema } from './schema'; + +async function test() { + const db = new ZenStackClient(schema, {} as any); + await db.concretePost.create({ + data: { + id: 5, + name: 'a name', + age: 20, + }, + }); +}`, + }, + }, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1786.test.ts b/tests/regression/test/v2-migrated/issue-1786.test.ts new file mode 100644 index 00000000..f67a98a7 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1786.test.ts @@ -0,0 +1,46 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1786', async () => { + await loadSchema( + ` + model User { + id String @id @default(cuid()) + email String @unique @email @length(6, 32) + contents Content[] + + // everybody can signup + @@allow('create', true) + + // full access by self + @@allow('all', auth() == this) + } + + type BaseContent { + published Boolean @default(false) + + @@index([published]) + } + + model Content with BaseContent { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id]) + ownerId String + contentType String + + @@delegate(contentType) + } + + model Post extends Content { + title String + } + + model Video extends Content { + name String + duration Int + } + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1835.test.ts b/tests/regression/test/v2-migrated/issue-1835.test.ts new file mode 100644 index 00000000..2cc6f088 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1835.test.ts @@ -0,0 +1,23 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1835', async () => { + await loadSchema( + ` +enum Enum { + SOME_VALUE + ANOTHER_VALUE +} + +model Model { + id String @id @default(cuid()) + value Enum + @@ignore +} + +model AnotherModel { + id String @id @default(cuid()) +} +`, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1849.test.ts b/tests/regression/test/v2-migrated/issue-1849.test.ts new file mode 100644 index 00000000..cf99d6f3 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1849.test.ts @@ -0,0 +1,27 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1849', async () => { + await loadSchema( + ` +import './enum' + +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} + +model Post { + id Int @id + status Status @default(PUBLISHED) +}`, + { + enum: ` +enum Status { + PENDING + PUBLISHED +} +`, + }, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1857.test.ts b/tests/regression/test/v2-migrated/issue-1857.test.ts new file mode 100644 index 00000000..2df875b2 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1857.test.ts @@ -0,0 +1,39 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1857', async () => { + await createTestClient( + ` + type JSONContent { + type String + text String? + } + + model Post { + id String @id @default(uuid()) + content JSONContent @json + @@allow('all', true) + } + `, + { + extraSourceFiles: { + main: ` + import { ZenStackClient } from '@zenstackhq/runtime'; + import { schema } from './schema'; + + async function main() { + const db = new ZenStackClient(schema, {} as any); + await db.post.create({ + data: { + content: { type: 'foo', text: null } + } + }); + } + `, + }, + }, + ); + + // TODO: zod schema support + // zodSchemas.models.JSONContentSchema.parse({ type: 'foo', text: null }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1870.test.ts b/tests/regression/test/v2-migrated/issue-1870.test.ts new file mode 100644 index 00000000..15c8b668 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1870.test.ts @@ -0,0 +1,14 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1870', async () => { + await loadSchema( + ` +model Polygon { + id Int @id @default(autoincrement()) + geometry Unsupported("geometry(MultiPolygon, 4326)") + @@index([geometry], name: "parcel_polygon_idx", type: Gist) +} +`, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1894.test.ts b/tests/regression/test/v2-migrated/issue-1894.test.ts new file mode 100644 index 00000000..92f4710e --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1894.test.ts @@ -0,0 +1,46 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1894', async () => { + const db = await createTestClient( + ` +model A { + id Int @id @default(autoincrement()) + b B[] +} + +model B { + id Int @id @default(autoincrement()) + a A @relation(fields: [aId], references: [id]) + aId Int + + type String + @@delegate(type) +} + +model C extends B { + f String? +} + `, + { + extraSourceFiles: { + main: ` + import { ZenStackClient } from '@zenstackhq/runtime'; + import { schema } from './schema'; + + async function main() { + const db = new ZenStackClient(schema, {} as any); + await db.a.create({ data: { id: 0 } }); + await db.c.create({ data: { a: { connect: { id: 0 } } } }); + } + + main(); + +`, + }, + }, + ); + + await db.a.create({ data: { id: 0 } }); + await expect(db.c.create({ data: { a: { connect: { id: 0 } } } })).toResolveTruthy(); +}); From 9699d5bed068a6e0906eb27e9415483ea647dfec Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 14 Oct 2025 22:54:14 -0700 Subject: [PATCH 11/17] fix(delegate,policy): several issues due to interaction between delegate and policy (#299) --- .../policy/src/expression-transformer.ts | 67 +++++- packages/plugins/policy/src/functions.ts | 58 +++++- packages/plugins/policy/src/policy-handler.ts | 5 + packages/testtools/package.json | 3 +- packages/testtools/tsup.config.ts | 5 +- pnpm-lock.yaml | 192 ------------------ tests/e2e/orm/policy/delegate.test.ts | 176 ++++++++++++++++ .../test/v2-migrated/issue-1930.test.ts | 76 +++++++ 8 files changed, 372 insertions(+), 210 deletions(-) create mode 100644 tests/e2e/orm/policy/delegate.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1930.test.ts diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index 58bcea2e..02d3f756 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -575,20 +575,34 @@ export class ExpressionTransformer { } const fromModel = context.model; + const relationFieldDef = QueryUtils.requireField(this.schema, fromModel, field); const { keyPairs, ownedByModel } = QueryUtils.getRelationForeignKeyFieldPairs(this.schema, fromModel, field); let condition: OperationNode; if (ownedByModel) { // `fromModel` owns the fk + condition = conjunction( this.dialect, - keyPairs.map(({ fk, pk }) => - BinaryOperationNode.create( - ReferenceNode.create(ColumnNode.create(fk), TableNode.create(context.alias ?? fromModel)), + keyPairs.map(({ fk, pk }) => { + let fkRef: OperationNode = ReferenceNode.create( + ColumnNode.create(fk), + TableNode.create(context.alias ?? fromModel), + ); + if (relationFieldDef.originModel && relationFieldDef.originModel !== fromModel) { + fkRef = this.buildDelegateBaseFieldSelect( + fromModel, + context.alias ?? fromModel, + fk, + relationFieldDef.originModel, + ); + } + return BinaryOperationNode.create( + fkRef, OperatorNode.create('='), ReferenceNode.create(ColumnNode.create(pk), TableNode.create(relationModel)), - ), - ), + ); + }), ); } else { // `relationModel` owns the fk @@ -633,8 +647,47 @@ export class ExpressionTransformer { return relationQuery.toOperationNode(); } - private createColumnRef(column: string, context: ExpressionTransformerContext): ReferenceNode { - return ReferenceNode.create(ColumnNode.create(column), TableNode.create(context.alias ?? context.model)); + private createColumnRef(column: string, context: ExpressionTransformerContext) { + // if field comes from a delegate base model, we need to use the join alias + // of that base model + + const tableName = context.alias ?? context.model; + + // "create" policies evaluate table from "VALUES" node so no join from delegate bases are + // created and thus we should directly use the model table name + if (context.operation === 'create') { + return ReferenceNode.create(ColumnNode.create(column), TableNode.create(tableName)); + } + + const fieldDef = QueryUtils.requireField(this.schema, context.model, column); + if (!fieldDef.originModel || fieldDef.originModel === context.model) { + return ReferenceNode.create(ColumnNode.create(column), TableNode.create(tableName)); + } + + return this.buildDelegateBaseFieldSelect(context.model, tableName, column, fieldDef.originModel); + } + + private buildDelegateBaseFieldSelect(model: string, modelAlias: string, field: string, baseModel: string) { + const idFields = QueryUtils.requireIdFields(this.client.$schema, model); + return { + kind: 'SelectQueryNode', + from: FromNode.create([TableNode.create(baseModel)]), + selections: [ + SelectionNode.create(ReferenceNode.create(ColumnNode.create(field), TableNode.create(baseModel))), + ], + where: WhereNode.create( + conjunction( + this.dialect, + idFields.map((idField) => + BinaryOperationNode.create( + ReferenceNode.create(ColumnNode.create(idField), TableNode.create(baseModel)), + OperatorNode.create('='), + ReferenceNode.create(ColumnNode.create(idField), TableNode.create(modelAlias)), + ), + ), + ), + ), + } satisfies SelectQueryNode; } private isAuthCall(value: unknown): value is CallExpression { diff --git a/packages/plugins/policy/src/functions.ts b/packages/plugins/policy/src/functions.ts index 5d6d6621..d131f933 100644 --- a/packages/plugins/policy/src/functions.ts +++ b/packages/plugins/policy/src/functions.ts @@ -36,18 +36,60 @@ export const check: ZModelFunction = ( invariant(!fieldDef.array, `Field "${fieldName}" is a to-many relation, which is not supported by "check"`); const relationModel = fieldDef.type; - const op = arg2Node ? (arg2Node.value as CRUD) : operation; + // build the join condition between the current model and the related model + const joinConditions: Expression[] = []; + const fkInfo = QueryUtils.getRelationForeignKeyFieldPairs(client.$schema, model, fieldName); + const idFields = QueryUtils.requireIdFields(client.$schema, model); - const policyHandler = new PolicyHandler(client); + // helper to build a base model select for delegate models + const buildBaseSelect = (baseModel: string, field: string): Expression => { + return eb + .selectFrom(baseModel) + .select(field) + .where( + eb.and( + idFields.map((idField) => + eb(eb.ref(`${fieldDef.originModel}.${idField}`), '=', eb.ref(`${modelAlias}.${idField}`)), + ), + ), + ); + }; + + if (fkInfo.ownedByModel) { + // model owns the relation + joinConditions.push( + ...fkInfo.keyPairs.map(({ fk, pk }) => { + let fkRef: Expression; + if (fieldDef.originModel && fieldDef.originModel !== model) { + // relation is actually defined in a delegate base model, select from there + fkRef = buildBaseSelect(fieldDef.originModel, fk); + } else { + fkRef = eb.ref(`${modelAlias}.${fk}`); + } + return eb(fkRef, '=', eb.ref(`${relationModel}.${pk}`)); + }), + ); + } else { + // related model owns the relation + joinConditions.push( + ...fkInfo.keyPairs.map(({ fk, pk }) => { + let pkRef: Expression; + if (fieldDef.originModel && fieldDef.originModel !== model) { + // relation is actually defined in a delegate base model, select from there + pkRef = buildBaseSelect(fieldDef.originModel, pk); + } else { + pkRef = eb.ref(`${modelAlias}.${pk}`); + } + return eb(pkRef, '=', eb.ref(`${relationModel}.${fk}`)); + }), + ); + } - // join with parent model - const joinPairs = QueryUtils.buildJoinPairs(client.$schema, model, modelAlias, fieldName, relationModel); - const joinCondition = - joinPairs.length === 1 - ? eb(eb.ref(joinPairs[0]![0]), '=', eb.ref(joinPairs[0]![1])) - : eb.and(joinPairs.map(([left, right]) => eb(eb.ref(left), '=', eb.ref(right)))); + const joinCondition = joinConditions.length === 1 ? joinConditions[0]! : eb.and(joinConditions); // policy condition of the related model + const policyHandler = new PolicyHandler(client); + const op = arg2Node ? (arg2Node.value as CRUD) : operation; const policyCondition = policyHandler.buildPolicyFilter(relationModel, undefined, op); // build the final nested select that evaluates the policy condition diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index 9bc6f664..92f5e74c 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -297,6 +297,10 @@ export class PolicyHandler extends OperationNodeTransf // #region overrides protected override transformSelectQuery(node: SelectQueryNode) { + if (!node.from) { + return super.transformSelectQuery(node); + } + let whereNode = this.transformNode(node.where); // get combined policy filter for all froms, and merge into where clause @@ -327,6 +331,7 @@ export class PolicyHandler extends OperationNodeTransf // build a nested query with policy filter applied const filter = this.buildPolicyFilter(table.model, table.alias, 'read'); + const nestedSelect: SelectQueryNode = { kind: 'SelectQueryNode', from: FromNode.create([node.table]), diff --git a/packages/testtools/package.json b/packages/testtools/package.json index f01ffe20..29d574d9 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -4,7 +4,7 @@ "description": "ZenStack Test Tools", "type": "module", "scripts": { - "build": "tsc --noEmit && tsup-node && copyfiles -f ./src/types.d.ts ./dist", + "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" @@ -53,7 +53,6 @@ "@types/pg": "^8.11.11", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", - "copyfiles": "^2.4.1", "typescript": "catalog:" } } diff --git a/packages/testtools/tsup.config.ts b/packages/testtools/tsup.config.ts index 5a74a9dd..f45bb68e 100644 --- a/packages/testtools/tsup.config.ts +++ b/packages/testtools/tsup.config.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { defineConfig } from 'tsup'; export default defineConfig({ @@ -7,7 +8,9 @@ export default defineConfig({ outDir: 'dist', splitting: false, sourcemap: true, - clean: true, dts: true, format: ['cjs', 'esm'], + async onSuccess() { + fs.cpSync('src/types.d.ts', 'dist/types.d.ts', { force: true }); + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9236661a..eff0e66f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,9 +485,6 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../config/typescript-config - copyfiles: - specifier: ^2.4.1 - version: 2.4.1 typescript: specifier: 'catalog:' version: 5.8.3 @@ -1520,9 +1517,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1563,13 +1557,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - copyfiles@2.4.1: - resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} - hasBin: true - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1674,10 +1661,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1802,9 +1785,6 @@ packages: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1813,10 +1793,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1855,10 +1831,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1911,10 +1883,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1945,12 +1913,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2117,11 +2079,6 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -2154,9 +2111,6 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - noms@0.0.0: - resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} - nypm@0.6.1: resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==} engines: {node: ^14.16.0 || >=16.10.0} @@ -2213,10 +2167,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2387,9 +2337,6 @@ packages: typescript: optional: true - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -2417,12 +2364,6 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} - readable-stream@1.0.34: - resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2431,10 +2372,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2462,9 +2399,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2531,12 +2465,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2582,9 +2510,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2736,10 +2661,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2885,23 +2806,11 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} hasBin: true - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3686,12 +3595,6 @@ snapshots: cli-spinners@2.9.2: {} - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clone@1.0.4: {} color-convert@2.0.1: @@ -3716,18 +3619,6 @@ snapshots: consola@3.4.2: {} - copyfiles@2.4.1: - dependencies: - glob: 7.2.3 - minimatch: 3.1.2 - mkdirp: 1.0.4 - noms: 0.0.0 - through2: 2.0.5 - untildify: 4.0.0 - yargs: 16.2.0 - - core-util-is@1.0.3: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3858,8 +3749,6 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 - escalade@3.2.0: {} - escape-string-regexp@4.0.0: {} eslint-scope@8.4.0: @@ -4008,15 +3897,11 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4080,15 +3965,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - globals@14.0.0: {} gopd@1.2.0: {} @@ -4129,11 +4005,6 @@ snapshots: imurmurhash@0.1.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} ini@1.3.8: {} @@ -4152,10 +4023,6 @@ snapshots: is-unicode-supported@0.1.0: {} - isarray@0.0.1: {} - - isarray@1.0.0: {} - isarray@2.0.5: {} isexe@2.0.0: {} @@ -4312,8 +4179,6 @@ snapshots: mkdirp-classic@0.5.3: {} - mkdirp@1.0.4: {} - mlly@1.7.4: dependencies: acorn: 8.15.0 @@ -4343,11 +4208,6 @@ snapshots: node-fetch-native@1.6.7: {} - noms@0.0.0: - dependencies: - inherits: 2.0.4 - readable-stream: 1.0.34 - nypm@0.6.1: dependencies: citty: 0.1.6 @@ -4411,8 +4271,6 @@ snapshots: path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-scurry@1.11.1: @@ -4565,8 +4423,6 @@ snapshots: transitivePeerDependencies: - magicast - process-nextick-args@2.0.1: {} - pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -4594,23 +4450,6 @@ snapshots: react@19.1.0: {} - readable-stream@1.0.34: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -4619,8 +4458,6 @@ snapshots: readdirp@4.1.2: {} - require-directory@2.1.1: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -4664,8 +4501,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} semver@7.7.2: {} @@ -4725,12 +4560,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 - string_decoder@0.10.31: {} - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -4788,11 +4617,6 @@ snapshots: dependencies: any-promise: 1.3.0 - through2@2.0.5: - dependencies: - readable-stream: 2.3.8 - xtend: 4.0.2 - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4930,8 +4754,6 @@ snapshots: universalify@2.0.1: {} - untildify@4.0.0: {} - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5079,22 +4901,8 @@ snapshots: xtend@4.0.2: {} - y18n@5.0.8: {} - yaml@2.8.0: {} - yargs-parser@20.2.9: {} - - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - yocto-queue@0.1.0: {} zod-validation-error@4.0.1(zod@3.25.76): diff --git a/tests/e2e/orm/policy/delegate.test.ts b/tests/e2e/orm/policy/delegate.test.ts new file mode 100644 index 00000000..5b526a07 --- /dev/null +++ b/tests/e2e/orm/policy/delegate.test.ts @@ -0,0 +1,176 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Delegate interaction tests', () => { + it('inherits policies from delegate base models', async () => { + const db = await createPolicyTestClient( + ` +model A { + id Int @id @default(autoincrement()) + a Int + aType String + @@delegate(aType) + @@allow('all', true) + @@deny('all', a <= 0) +} + +model B extends A { + b Int + bType String + @@delegate(bType) + @@deny('all', b <= 0) +} + +model C extends B { + c Int + @@deny('all', c <= 0) +} +`, + ); + + await expect(db.c.create({ data: { a: 0, b: 1, c: 1 } })).toBeRejectedByPolicy(); + await expect(db.c.create({ data: { a: 1, b: 0, c: 1 } })).toBeRejectedByPolicy(); + await expect(db.c.create({ data: { a: 1, b: 1, c: 0 } })).toBeRejectedByPolicy(); + await expect(db.c.create({ data: { a: 1, b: 1, c: 1 } })).toResolveTruthy(); + + // clean up + await db.c.deleteMany(); + + await db.$unuseAll().c.create({ data: { id: 2, a: 0, b: 0, c: 1 } }); + await expect(db.a.findUnique({ where: { id: 2 } })).toResolveNull(); + await expect(db.b.findUnique({ where: { id: 2 } })).toResolveNull(); + await expect(db.c.findUnique({ where: { id: 2 } })).toResolveNull(); + + await db.$unuseAll().c.update({ where: { id: 2 }, data: { a: 1, b: 1, c: 1 } }); + await expect(db.a.findUnique({ where: { id: 2 } })).toResolveTruthy(); + await expect(db.b.findUnique({ where: { id: 2 } })).toResolveTruthy(); + await expect(db.c.findUnique({ where: { id: 2 } })).toResolveTruthy(); + }); + + it('works with policies referencing base model fields', async () => { + const db = await createPolicyTestClient( + ` +model A { + id Int @id @default(autoincrement()) + a Int + aType String + @@delegate(aType) + @@allow('all', a > 0) +} + +model B extends A { + b Int + c C @relation(fields: [cId], references: [id]) + cId Int +} + +model C { + id Int @id @default(autoincrement()) + bs B[] + @@allow('all', true) +} +`, + ); + + await expect( + db.c.create({ + data: { + bs: { + create: [ + { a: 0, b: 0 }, + { a: 1, b: 1 }, + ], + }, + }, + }), + ).toBeRejectedByPolicy(); + await expect(db.$unuseAll().b.count()).resolves.toBe(0); + + await db.$unuseAll().c.create({ + data: { + bs: { + create: [ + { id: 1, a: 0, b: 0 }, + { id: 2, a: 1, b: 1 }, + ], + }, + }, + }); + + await expect(db.c.findFirst({ include: { bs: true } })).resolves.toMatchObject({ + bs: [{ a: 1 }], + }); + await expect(db.b.update({ where: { id: 1 }, data: { b: 2 } })).toBeRejectedNotFound(); + await expect(db.b.update({ where: { id: 2 }, data: { b: 2 } })).toResolveTruthy(); + }); + + it('works with policies referencing base model relations', async () => { + const db = await createPolicyTestClient( + ` +model A { + id Int @id @default(autoincrement()) + aType String + c C @relation(fields: [cId], references: [id]) + cId Int + @@delegate(aType) + @@allow('all', true) +} + +model C { + id Int @id @default(autoincrement()) + c Int + as A[] + @@allow('all', true) +} + +model B extends A { + b Int + @@deny('update', c.c <= 0) +} +`, + ); + + await db.b.create({ + data: { id: 1, b: 0, c: { create: { c: 0 } } }, + }); + await expect(db.b.update({ where: { id: 1 }, data: { b: 1 } })).toBeRejectedNotFound(); + + await db.b.create({ + data: { id: 2, b: 0, c: { create: { c: 1 } } }, + }); + await expect(db.b.update({ where: { id: 2 }, data: { b: 1 } })).toResolveTruthy(); + }); + + it('works with policies using check on relation fields on delegate base models', async () => { + const db = await createPolicyTestClient( + ` +model A { + id Int @id @default(autoincrement()) + aType String + c C? + @@delegate(aType) + @@allow('all', true) +} + +model B extends A { + b Int + @@deny('read', !check(c)) +} + +model C { + id Int @id @default(autoincrement()) + c Int + a A @relation(fields: [aId], references: [id]) + aId Int @unique + @@allow('read', c > 0) + @@allow('create', true) +} + `, + ); + + await db.$unuseAll().b.create({ data: { id: 1, b: 1, c: { create: { c: 0 } } } }); + await expect(db.b.findUnique({ where: { id: 1 } })).resolves.toBeNull(); + await db.$unuseAll().b.create({ data: { id: 2, b: 2, c: { create: { c: 1 } } } }); + await expect(db.b.findUnique({ where: { id: 2 } })).toResolveTruthy(); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1930.test.ts b/tests/regression/test/v2-migrated/issue-1930.test.ts new file mode 100644 index 00000000..08b64e24 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1930.test.ts @@ -0,0 +1,76 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1930', async () => { + const db = await createPolicyTestClient( + ` +model Organization { + id String @id @default(cuid()) + entities Entity[] + + @@allow('all', true) +} + +model Entity { + id String @id @default(cuid()) + org Organization? @relation(fields: [orgId], references: [id]) + orgId String? + contents EntityContent[] + entityType String + isDeleted Boolean @default(false) + + @@delegate(entityType) + + @@allow('all', !isDeleted) +} + +model EntityContent { + id String @id @default(cuid()) + entity Entity @relation(fields: [entityId], references: [id]) + entityId String + + entityContentType String + + @@delegate(entityContentType) + + @@allow('create', true) + @@allow('read', check(entity)) +} + +model Article extends Entity { +} + +model ArticleContent extends EntityContent { + body String? +} + +model OtherContent extends EntityContent { + data Int +} + `, + ); + + const org = await db.$unuseAll().organization.create({ data: {} }); + const article = await db.$unuseAll().article.create({ + data: { org: { connect: { id: org.id } } }, + }); + + // normal create/read + await expect( + db.articleContent.create({ + data: { body: 'abc', entity: { connect: { id: article.id } } }, + }), + ).toResolveTruthy(); + await expect(db.article.findFirst({ include: { contents: true } })).resolves.toMatchObject({ + contents: expect.arrayContaining([expect.objectContaining({ body: 'abc' })]), + }); + + // deleted article's contents are not readable + const deletedArticle = await db.$unuseAll().article.create({ + data: { org: { connect: { id: org.id } }, isDeleted: true }, + }); + const content1 = await db.$unuseAll().articleContent.create({ + data: { body: 'bcd', entity: { connect: { id: deletedArticle.id } } }, + }); + await expect(db.articleContent.findUnique({ where: { id: content1.id } })).toResolveNull(); +}); From cad7098c32e1d835c260395046f26d10c932e3db Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 15 Oct 2025 12:14:33 -0700 Subject: [PATCH 12/17] fix: validating currentModel and currentOperation properly (#300) * fix: validating currentModel and currentOperation properly * update --- .../function-invocation-validator.ts | 25 +++- .../test/v2-migrated/issue-1955.test.ts | 100 ++++++++++++++ .../test/v2-migrated/issue-1964.test.ts | 123 +++++++++++++++++ .../test/v2-migrated/issue-1978.test.ts | 39 ++++++ .../test/v2-migrated/issue-1984.test.ts | 56 ++++++++ .../test/v2-migrated/issue-1991.test.ts | 41 ++++++ .../test/v2-migrated/issue-1992.test.ts | 60 ++++++++ .../test/v2-migrated/issue-1993.test.ts | 62 +++++++++ .../test/v2-migrated/issue-1994.test.ts | 102 ++++++++++++++ .../test/v2-migrated/issue-1997.test.ts | 129 ++++++++++++++++++ .../test/v2-migrated/issue-1998.test.ts | 56 ++++++++ 11 files changed, 787 insertions(+), 6 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-1955.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1964.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1978.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1984.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1991.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1992.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1993.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1994.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1997.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-1998.test.ts diff --git a/packages/language/src/validators/function-invocation-validator.ts b/packages/language/src/validators/function-invocation-validator.ts index e75c8e3d..ae759904 100644 --- a/packages/language/src/validators/function-invocation-validator.ts +++ b/packages/language/src/validators/function-invocation-validator.ts @@ -66,12 +66,7 @@ export default class FunctionInvocationValidator implements AstValidator ExpressionContext.DefaultValue) - .with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy) - .with('@@validate', () => ExpressionContext.ValidationRule) - .with('@@index', () => ExpressionContext.Index) - .otherwise(() => undefined); + const exprContext = this.getExpressionContext(containerAttribute); // get the context allowed for the function const funcAllowedContext = getFunctionExpressionContext(funcDecl); @@ -103,6 +98,24 @@ export default class FunctionInvocationValidator implements AstValidator ExpressionContext.DefaultValue) + .with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy) + .with('@@index', () => ExpressionContext.Index) + .otherwise(() => undefined); + } + + private isValidationAttribute(attr: DataModelAttribute | DataFieldAttribute) { + return !!attr.decl.ref?.attributes.some((attr) => attr.decl.$refText === '@@@validation'); + } + private validateArgs(funcDecl: FunctionDecl, args: Argument[], accept: ValidationAcceptor) { let success = true; for (let i = 0; i < funcDecl.params.length; i++) { diff --git a/tests/regression/test/v2-migrated/issue-1955.test.ts b/tests/regression/test/v2-migrated/issue-1955.test.ts new file mode 100644 index 00000000..839c1103 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1955.test.ts @@ -0,0 +1,100 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1955', () => { + it('simple policy', async () => { + const db = await createPolicyTestClient( + ` + model Post { + id Int @id @default(autoincrement()) + name String + expections String[] + @@allow('all', true) + } + `, + { provider: 'postgresql' }, + ); + + await expect( + db.post.createManyAndReturn({ + data: [ + { + name: 'bla', + }, + { + name: 'blu', + }, + ], + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'bla' }), + expect.objectContaining({ name: 'blu' }), + ]), + ); + + await expect( + db.post.updateManyAndReturn({ + data: { name: 'foo' }, + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'foo' }), + expect.objectContaining({ name: 'foo' }), + ]), + ); + }); + + it('complex policy', async () => { + const db = await createPolicyTestClient( + ` + model Post { + id Int @id @default(autoincrement()) + name String + expections String[] + comments Comment[] + + @@allow('create', true) + @@allow('read,update', comments^[private]) + } + + model Comment { + id Int @id @default(autoincrement()) + private Boolean @default(false) + postId Int + post Post @relation(fields: [postId], references: [id]) + } + `, + { provider: 'postgresql' }, + ); + + await expect( + db.post.createManyAndReturn({ + data: [ + { + name: 'bla', + }, + { + name: 'blu', + }, + ], + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'bla' }), + expect.objectContaining({ name: 'blu' }), + ]), + ); + + await expect( + db.post.updateManyAndReturn({ + data: { name: 'foo' }, + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'foo' }), + expect.objectContaining({ name: 'foo' }), + ]), + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1964.test.ts b/tests/regression/test/v2-migrated/issue-1964.test.ts new file mode 100644 index 00000000..b37835dd --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1964.test.ts @@ -0,0 +1,123 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1964', () => { + it('regression1', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id + orgId String +} + +model Author { + id Int @id @default(autoincrement()) + orgId String + name String + posts Post[] + + @@unique([orgId, name]) + @@allow('all', auth().orgId == orgId) +} + +model Post { + id Int @id @default(autoincrement()) + orgId String + title String + author Author @relation(fields: [authorId], references: [id]) + authorId Int + + @@allow('all', auth().orgId == orgId) +} + `, + ); + + const authDb = db.$setAuth({ id: 1, orgId: 'org' }); + + const newauthor = await authDb.author.create({ + data: { + name: `Foo ${Date.now()}`, + orgId: 'org', + posts: { + createMany: { data: [{ title: 'Hello', orgId: 'org' }] }, + }, + }, + include: { posts: true }, + }); + + await expect( + authDb.author.update({ + where: { orgId_name: { orgId: 'org', name: newauthor.name } }, + data: { + name: `Bar ${Date.now()}`, + posts: { deleteMany: { id: { equals: newauthor.posts[0].id } } }, + }, + }), + ).toResolveTruthy(); + }); + + it('regression2', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + slug String @unique + profile Profile? + @@allow('all', true) +} + +model Profile { + id Int @id @default(autoincrement()) + slug String @unique + name String + addresses Address[] + userId Int? @unique + user User? @relation(fields: [userId], references: [id]) + @@allow('all', true) +} + +model Address { + id Int @id @default(autoincrement()) + profileId Int @unique + profile Profile @relation(fields: [profileId], references: [id]) + city String + @@allow('all', true) +} + `, + ); + + const authDb = db.$setAuth({ id: 1, orgId: 'org' }); + + await authDb.user.create({ + data: { + slug: `user1`, + profile: { + create: { + name: `My Profile`, + slug: 'profile1', + addresses: { + create: { id: 1, city: 'City' }, + }, + }, + }, + }, + }); + + await expect( + authDb.user.update({ + where: { slug: 'user1' }, + data: { + profile: { + update: { + addresses: { + deleteMany: { id: { equals: 1 } }, + }, + }, + }, + }, + }), + ).toResolveTruthy(); + + await expect(authDb.address.count()).resolves.toEqual(0); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1978.test.ts b/tests/regression/test/v2-migrated/issue-1978.test.ts new file mode 100644 index 00000000..2a67c670 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1978.test.ts @@ -0,0 +1,39 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('regression', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id + posts Post[] + secret String @allow('read', posts?[published]) + @@allow('all', true) +} + +model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int + published Boolean @default(false) + @@allow('all', true) +} + `, + ); + + await db.$unuseAll().user.create({ + data: { id: 1, secret: 'secret', posts: { create: { id: 1, published: true } } }, + }); + await db.$unuseAll().user.create({ + data: { id: 2, secret: 'secret' }, + }); + + await expect(db.user.findFirst({ where: { id: 1 } })).resolves.toMatchObject({ secret: 'secret' }); + await expect(db.user.findFirst({ where: { id: 1 }, select: { id: true } })).resolves.toEqual({ id: 1 }); + + let r = await db.user.findFirst({ where: { id: 2 } }); + expect(r.secret).toBeUndefined(); + r = await db.user.findFirst({ where: { id: 2 }, select: { id: true } }); + expect(r.secret).toBeUndefined(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1984.test.ts b/tests/regression/test/v2-migrated/issue-1984.test.ts new file mode 100644 index 00000000..ac1a3d9b --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1984.test.ts @@ -0,0 +1,56 @@ +import { createPolicyTestClient, loadSchemaWithError } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 1984', () => { + it('regression1', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id @default(autoincrement()) + access String + + @@allow('all', + contains(auth().access, currentModel()) || + contains(auth().access, currentOperation())) +} + `, + ); + + const db1 = db; + await expect(db1.user.create({ data: { access: 'foo' } })).toBeRejectedByPolicy(); + + const db2 = db.$setAuth({ id: 1, access: 'aUser' }); + await expect(db2.user.create({ data: { access: 'aUser' } })).toResolveTruthy(); + + const db3 = db.$setAuth({ id: 1, access: 'do-create-read' }); + await expect(db3.user.create({ data: { access: 'do-create-read' } })).toResolveTruthy(); + + const db4 = db.$setAuth({ id: 1, access: 'do-read' }); + await expect(db4.user.create({ data: { access: 'do-read' } })).toBeRejectedByPolicy(); + }); + + it('regression2', async () => { + await loadSchemaWithError( + ` +model User { + id Int @id @default(autoincrement()) + modelName String + @@validate(contains(modelName, currentModel())) +} + `, + 'function "currentModel" is not allowed in the current context: ValidationRule', + ); + }); + + it('regression3', async () => { + await loadSchemaWithError( + ` +model User { + id Int @id @default(autoincrement()) + modelName String @contains(currentModel()) +} + `, + 'function "currentModel" is not allowed in the current context: ValidationRule', + ); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1991.test.ts b/tests/regression/test/v2-migrated/issue-1991.test.ts new file mode 100644 index 00000000..dd92a16e --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1991.test.ts @@ -0,0 +1,41 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1991', async () => { + await createPolicyTestClient( + ` +type FooMetadata { + isLocked Boolean +} + +type FooOptionMetadata { + color String +} + +model Foo { + id String @id @db.Uuid @default(uuid()) + meta FooMetadata @json +} + +model FooOption { + id String @id @db.Uuid @default(uuid()) + meta FooOptionMetadata @json +} + `, + { + provider: 'postgresql', + extraSourceFiles: { + main: ` + import { ZenStackClient } from '@zenstackhq/runtime'; + import { schema } from './schema'; + + const db = new ZenStackClient(schema, {} as any); + + db.fooOption.create({ + data: { meta: { color: 'red' } } + }) + `, + }, + }, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1992.test.ts b/tests/regression/test/v2-migrated/issue-1992.test.ts new file mode 100644 index 00000000..4ca6b7d8 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1992.test.ts @@ -0,0 +1,60 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { it } from 'vitest'; + +it('verifies issue 1992', async () => { + await loadSchema( + ` +enum MyAppUserType { + Local + Google + Microsoft +} + +model MyAppCompany { + id String @id @default(cuid()) + name String + users MyAppUser[] + + userFolders MyAppUserFolder[] +} + +model MyAppUser { + id String @id @default(cuid()) + companyId String + type MyAppUserType + + @@delegate(type) + + company MyAppCompany @relation(fields: [companyId], references: [id]) + userFolders MyAppUserFolder[] +} + +model MyAppUserLocal extends MyAppUser { + email String + password String +} + +model MyAppUserGoogle extends MyAppUser { + googleId String +} + +model MyAppUserMicrosoft extends MyAppUser { + microsoftId String +} + +model MyAppUserFolder { + id String @id @default(cuid()) + companyId String + userId String + path String + name String + + @@unique([companyId, userId, name]) + @@unique([companyId, userId, path]) + + company MyAppCompany @relation(fields: [companyId], references: [id]) + user MyAppUser @relation(fields: [userId], references: [id]) +} + `, + ); +}); diff --git a/tests/regression/test/v2-migrated/issue-1993.test.ts b/tests/regression/test/v2-migrated/issue-1993.test.ts new file mode 100644 index 00000000..3445e465 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1993.test.ts @@ -0,0 +1,62 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: zod support +it.skip('verifies issue 1993', async () => { + const { zodSchemas } = await createTestClient( + ` +enum UserType { + UserLocal + UserGoogle +} + +model User { + id String @id @default(cuid()) + companyId String? + type UserType + + @@delegate(type) + + userFolders UserFolder[] + + @@allow('all', true) +} + +model UserLocal extends User { + email String + password String +} + +model UserGoogle extends User { + googleId String +} + +model UserFolder { + id String @id @default(cuid()) + userId String + path String + + user User @relation(fields: [userId], references: [id]) + + @@allow('all', true) +} `, + ); + + expect( + zodSchemas.input.UserLocalInputSchema.create.safeParse({ + data: { + email: 'test@example.com', + password: 'password', + }, + }), + ).toMatchObject({ success: true }); + + expect( + zodSchemas.input.UserFolderInputSchema.create.safeParse({ + data: { + path: '/', + userId: '1', + }, + }), + ).toMatchObject({ success: true }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1994.test.ts b/tests/regression/test/v2-migrated/issue-1994.test.ts new file mode 100644 index 00000000..f072bdb3 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1994.test.ts @@ -0,0 +1,102 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1994', async () => { + const db = await createTestClient( + ` +model OrganizationRole { + id Int @id @default(autoincrement()) + rolePrivileges OrganizationRolePrivilege[] + type String + @@delegate(type) +} + +model Organization { + id Int @id @default(autoincrement()) + customRoles CustomOrganizationRole[] +} + +// roles common to all orgs, defined once +model SystemDefinedRole extends OrganizationRole { + name String @unique +} + +// roles specific to each org +model CustomOrganizationRole extends OrganizationRole { + name String + organizationId Int + organization Organization @relation(fields: [organizationId], references: [id]) + + @@unique([organizationId, name]) + @@index([organizationId]) +} + +model OrganizationRolePrivilege { + organizationRoleId Int + privilegeId Int + + organizationRole OrganizationRole @relation(fields: [organizationRoleId], references: [id]) + privilege Privilege @relation(fields: [privilegeId], references: [id]) + + @@id([organizationRoleId, privilegeId]) +} + +model Privilege { + id Int @id @default(autoincrement()) + name String // e.g. "org:manage" + + orgRolePrivileges OrganizationRolePrivilege[] + @@unique([name]) +} + `, + { + extraSourceFiles: { + main: ` + import { ZenStackClient } from '@zenstackhq/runtime'; + import { schema } from './schema'; + + const db = new ZenStackClient(schema, {} as any); + + async function main() { + const privilege = await db.privilege.create({ + data: { name: 'org:manage' }, + }); + + await db.systemDefinedRole.create({ + data: { + name: 'Admin', + rolePrivileges: { + create: [ + { + privilegeId: privilege.id, + }, + ], + }, + }, + }); + } + main() + `, + }, + }, + ); + + const privilege = await db.privilege.create({ + data: { name: 'org:manage' }, + }); + + await expect( + db.systemDefinedRole.create({ + data: { + name: 'Admin', + rolePrivileges: { + create: [ + { + privilegeId: privilege.id, + }, + ], + }, + }, + }), + ).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-1997.test.ts b/tests/regression/test/v2-migrated/issue-1997.test.ts new file mode 100644 index 00000000..591694bd --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1997.test.ts @@ -0,0 +1,129 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1997', async () => { + const db = await createPolicyTestClient( + ` + model Tenant { + id String @id @default(uuid()) + + users User[] + posts Post[] + comments Comment[] + postUserLikes PostUserLikes[] + } + + model User { + id String @id @default(uuid()) + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + posts Post[] + likes PostUserLikes[] + + @@allow('all', true) + } + + model Post { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + comments Comment[] + likes PostUserLikes[] + + @@id([tenantId, id]) + + @@allow('all', true) + } + + model PostUserLikes { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + + userId String + user User @relation(fields: [userId], references: [id]) + + postId String + post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) + + @@id([tenantId, id]) + @@unique([tenantId, userId, postId]) + + @@allow('all', true) + } + + model Comment { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + postId String + post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) + + @@id([tenantId, id]) + + @@allow('all', true) + } + `, + ); + + const tenant = await db.$unuseAll().tenant.create({ + data: {}, + }); + const user = await db.$unuseAll().user.create({ + data: { tenantId: tenant.id }, + }); + + const authDb = db.$setAuth({ id: user.id, tenantId: tenant.id }); + + await expect( + authDb.post.create({ + data: { + likes: { + createMany: { + data: [ + { + userId: user.id, + }, + ], + }, + }, + }, + include: { + likes: true, + }, + }), + ).resolves.toMatchObject({ + authorId: user.id, + likes: [ + { + tenantId: tenant.id, + userId: user.id, + }, + ], + }); + + await expect( + authDb.post.create({ + data: { + comments: { + createMany: { + data: [{}], + }, + }, + }, + include: { + comments: true, + }, + }), + ).resolves.toMatchObject({ + authorId: user.id, + comments: [ + { + tenantId: tenant.id, + }, + ], + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-1998.test.ts b/tests/regression/test/v2-migrated/issue-1998.test.ts new file mode 100644 index 00000000..038278b0 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-1998.test.ts @@ -0,0 +1,56 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 1998', async () => { + const db = await createPolicyTestClient( + ` +model Entity { + id String @id + type String + updatable Boolean + children Relation[] @relation("children") + parents Relation[] @relation("parents") + + @@delegate(type) + @@allow('create,read', true) + @@allow('update', updatable) +} + +model A extends Entity {} + +model B extends Entity {} + +model Relation { + parent Entity @relation("children", fields: [parentId], references: [id]) + parentId String + child Entity @relation("parents", fields: [childId], references: [id]) + childId String + + @@allow('create', true) + @@allow('read', check(parent, 'read') && check(child, 'read')) + @@allow('delete', check(parent, 'update') && check(child, 'update')) + + @@id([parentId, childId]) +} + `, + ); + + await db.a.create({ data: { id: '1', updatable: true } }); + await db.b.create({ data: { id: '2', updatable: true } }); + await db.relation.create({ data: { parentId: '1', childId: '2' } }); + + await expect( + db.relation.deleteMany({ + where: { parentId: '1', childId: '2' }, + }), + ).resolves.toEqual({ count: 1 }); + + await db.a.create({ data: { id: '3', updatable: false } }); + await db.b.create({ data: { id: '4', updatable: false } }); + await db.relation.create({ data: { parentId: '3', childId: '4' } }); + await expect( + db.relation.deleteMany({ + where: { parentId: '3', childId: '4' }, + }), + ).resolves.toEqual({ count: 0 }); +}); From 6c6890eccc6e073368c13cddb3b72699c6d00d6a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 15 Oct 2025 14:06:58 -0700 Subject: [PATCH 13/17] feat(validation): add API to suppress validation (#301) * feat(validation): add API to suppress validation * fix "@@validate" * fix * update --- packages/runtime/src/client/client-impl.ts | 10 +- packages/runtime/src/client/contract.ts | 6 ++ .../src/client/crud/validator/index.ts | 59 ++++++++---- .../src/client/crud/validator/utils.ts | 3 +- packages/runtime/src/client/options.ts | 6 ++ .../orm/validation/custom-validation.test.ts | 62 +++++++++++++ .../test/v2-migrated/issue-2000.test.ts | 66 ++++++++++++++ .../test/v2-migrated/issue-2007.test.ts | 91 +++++++++++++++++++ .../test/v2-migrated/issue-2019.test.ts | 85 +++++++++++++++++ .../test/v2-migrated/issue-2025.test.ts | 38 ++++++++ 10 files changed, 407 insertions(+), 19 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-2000.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2007.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2019.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2025.test.ts diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index c0585455..bcb753a3 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -288,6 +288,14 @@ export class ClientImpl { return this.auth; } + $setInputValidation(enable: boolean) { + const newOptions: ClientOptions = { + ...this.options, + validateInput: enable, + }; + return new ClientImpl(this.schema, newOptions, this); + } + $executeRaw(query: TemplateStringsArray, ...values: any[]) { return createZenStackPromise(async () => { const result = await sql(query, ...values).execute(this.kysely); @@ -325,7 +333,7 @@ export class ClientImpl { } function createClientProxy(client: ClientImpl): ClientImpl { - const inputValidator = new InputValidator(client.$schema); + const inputValidator = new InputValidator(client as unknown as ClientContract); const resultProcessor = new ResultProcessor(client.$schema, client.$options); return new Proxy(client, { diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index 2374bc6e..d2c19cb1 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -103,6 +103,12 @@ export type ClientContract = { */ $setAuth(auth: AuthType | undefined): ClientContract; + /** + * Returns a new client enabling/disabling input validations expressed with attributes like + * `@email`, `@regex`, `@@validate`, etc. + */ + $setInputValidation(enable: boolean): ClientContract; + /** * The Kysely query builder instance. */ diff --git a/packages/runtime/src/client/crud/validator/index.ts b/packages/runtime/src/client/crud/validator/index.ts index fd3be7ac..ffdd191a 100644 --- a/packages/runtime/src/client/crud/validator/index.ts +++ b/packages/runtime/src/client/crud/validator/index.ts @@ -16,6 +16,7 @@ import { enumerate } from '../../../utils/enumerate'; import { extractFields } from '../../../utils/object-utils'; import { formatError } from '../../../utils/zod-utils'; import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../../constants'; +import type { ClientContract } from '../../contract'; import { type AggregateArgs, type CountArgs, @@ -53,7 +54,15 @@ type GetSchemaFunc = (model: GetModels { private schemaCache = new Map(); - constructor(private readonly schema: Schema) {} + constructor(private readonly client: ClientContract) {} + + private get schema() { + return this.client.$schema; + } + + private get extraValidationsEnabled() { + return this.client.$options.validateInput !== false; + } validateFindArgs(model: GetModels, args: unknown, options: { unique: boolean; findOne: boolean }) { return this.validate< @@ -251,23 +260,31 @@ export class InputValidator { return this.makeTypeDefSchema(type); } else { return match(type) - .with('String', () => addStringValidation(z.string(), attributes)) - .with('Int', () => addNumberValidation(z.number().int(), attributes)) - .with('Float', () => addNumberValidation(z.number(), attributes)) + .with('String', () => + this.extraValidationsEnabled ? addStringValidation(z.string(), attributes) : z.string(), + ) + .with('Int', () => + this.extraValidationsEnabled ? addNumberValidation(z.number().int(), attributes) : z.number().int(), + ) + .with('Float', () => + this.extraValidationsEnabled ? addNumberValidation(z.number(), attributes) : z.number(), + ) .with('Boolean', () => z.boolean()) .with('BigInt', () => z.union([ - addNumberValidation(z.number().int(), attributes), - addBigIntValidation(z.bigint(), attributes), - ]), - ) - .with('Decimal', () => - z.union([ - addNumberValidation(z.number(), attributes), - addDecimalValidation(z.instanceof(Decimal), attributes), - addDecimalValidation(z.string(), attributes), + this.extraValidationsEnabled + ? addNumberValidation(z.number().int(), attributes) + : z.number().int(), + this.extraValidationsEnabled ? addBigIntValidation(z.bigint(), attributes) : z.bigint(), ]), ) + .with('Decimal', () => { + return z.union([ + this.extraValidationsEnabled ? addNumberValidation(z.number(), attributes) : z.number(), + addDecimalValidation(z.instanceof(Decimal), attributes, this.extraValidationsEnabled), + addDecimalValidation(z.string(), attributes, this.extraValidationsEnabled), + ]); + }) .with('DateTime', () => z.union([z.date(), z.string().datetime()])) .with('Bytes', () => z.instanceof(Uint8Array)) .otherwise(() => z.unknown()); @@ -913,8 +930,12 @@ export class InputValidator { } }); - const uncheckedCreateSchema = addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes); - const checkedCreateSchema = addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes); + const uncheckedCreateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) + : z.strictObject(uncheckedVariantFields); + const checkedCreateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) + : z.strictObject(checkedVariantFields); if (!hasRelation) { return this.orArray(uncheckedCreateSchema, canBeArray); @@ -1193,8 +1214,12 @@ export class InputValidator { } }); - const uncheckedUpdateSchema = addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes); - const checkedUpdateSchema = addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes); + const uncheckedUpdateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) + : z.strictObject(uncheckedVariantFields); + const checkedUpdateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) + : z.strictObject(checkedVariantFields); if (!hasRelation) { return uncheckedUpdateSchema; } else { diff --git a/packages/runtime/src/client/crud/validator/utils.ts b/packages/runtime/src/client/crud/validator/utils.ts index 6b0a17d5..1fdecb25 100644 --- a/packages/runtime/src/client/crud/validator/utils.ts +++ b/packages/runtime/src/client/crud/validator/utils.ts @@ -145,6 +145,7 @@ export function addBigIntValidation(schema: z.ZodBigInt, attributes: AttributeAp export function addDecimalValidation( schema: z.ZodType | z.ZodString, attributes: AttributeApplication[] | undefined, + addExtraValidation: boolean, ): z.ZodSchema { let result: z.ZodSchema = schema; @@ -176,7 +177,7 @@ export function addDecimalValidation( }); } - if (attributes) { + if (attributes && addExtraValidation) { for (const attr of attributes) { const val = getArgValue(attr.args?.[0]?.value); if (val === undefined) { diff --git a/packages/runtime/src/client/options.ts b/packages/runtime/src/client/options.ts index f09f44d6..2f6f7af7 100644 --- a/packages/runtime/src/client/options.ts +++ b/packages/runtime/src/client/options.ts @@ -72,6 +72,12 @@ export type ClientOptions = { * @see https://github.com/brianc/node-postgres/issues/429 */ fixPostgresTimezone?: boolean; + + /** + * Whether to enable input validations expressed with attributes like `@email`, `@regex`, + * `@@validate`, etc. Defaults to `true`. + */ + validateInput?: boolean; } & (HasComputedFields extends true ? { /** diff --git a/tests/e2e/orm/validation/custom-validation.test.ts b/tests/e2e/orm/validation/custom-validation.test.ts index 35667e4c..edd0c00e 100644 --- a/tests/e2e/orm/validation/custom-validation.test.ts +++ b/tests/e2e/orm/validation/custom-validation.test.ts @@ -108,4 +108,66 @@ describe('Custom validation tests', () => { ).toResolveTruthy(); } }); + + it('allows disabling validation', async () => { + const db = await createTestClient( + ` + model User { + id Int @id @default(autoincrement()) + email String @unique @email + @@validate(length(email, 8)) + @@allow('all', true) + } + `, + ); + + await expect( + db.user.create({ + data: { + email: 'xyz', + }, + }), + ).toBeRejectedByValidation(); + await expect( + db.user.create({ + data: { + email: 'a@b.com', + }, + }), + ).toBeRejectedByValidation(); + + await expect( + db.$setInputValidation(false).user.create({ + data: { + id: 1, + email: 'xyz', + }, + }), + ).toResolveTruthy(); + + await expect( + db.$setInputValidation(false).user.update({ + where: { id: 1 }, + data: { + email: 'a@b.com', + }, + }), + ).toResolveTruthy(); + + // original client not affected + await expect( + db.user.create({ + data: { + email: 'xyz', + }, + }), + ).toBeRejectedByValidation(); + await expect( + db.user.create({ + data: { + email: 'a@b.com', + }, + }), + ).toBeRejectedByValidation(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-2000.test.ts b/tests/regression/test/v2-migrated/issue-2000.test.ts new file mode 100644 index 00000000..009657a1 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2000.test.ts @@ -0,0 +1,66 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +// TODO: field-level policy support +it.skip('verifies issue 2000', async () => { + const db = await createPolicyTestClient( + ` +type Base { + id String @id @default(uuid()) @deny('update', true) + createdAt DateTime @default(now()) @deny('update', true) + updatedAt DateTime @updatedAt @deny('update', true) + active Boolean @default(false) + published Boolean @default(true) + deleted Boolean @default(false) + startDate DateTime? + endDate DateTime? + + @@allow('create', true) + @@allow('read', true) + @@allow('update', true) +} + +enum EntityType { + User + Alias + Group + Service + Device + Organization + Guest +} + +model Entity with Base { + entityType EntityType + name String? @unique + members Entity[] @relation("members") + memberOf Entity[] @relation("members") + @@delegate(entityType) + + + @@allow('create', true) + @@allow('read', true) + @@allow('update', true) + @@validate(!active || (active && name != null), "Active Entities Must Have A Name") +} + +model User extends Entity { + profile Json? + username String @unique + password String @password + + @@allow('create', true) + @@allow('read', true) + @@allow('update', true) +} + `, + ); + + await expect(db.user.create({ data: { username: 'admin', password: 'abc12345' } })).toResolveTruthy(); + await expect( + db.user.update({ where: { username: 'admin' }, data: { password: 'abc123456789123' } }), + ).toResolveTruthy(); + + // violating validation rules + await expect(db.user.update({ where: { username: 'admin' }, data: { active: true } })).toBeRejectedByPolicy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-2007.test.ts b/tests/regression/test/v2-migrated/issue-2007.test.ts new file mode 100644 index 00000000..c67d12de --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2007.test.ts @@ -0,0 +1,91 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// TODO: field-level policy support +describe.skip('Regression for issue 2007', () => { + it('regression1', async () => { + const db = await createPolicyTestClient( + ` +model Page { + id String @id @default(cuid()) + title String + + images Image[] + + @@allow('all', true) +} + +model Image { + id String @id @default(cuid()) @deny('update', true) + url String + pageId String? + page Page? @relation(fields: [pageId], references: [id]) + + @@allow('all', true) +} + `, + ); + + const image = await db.image.create({ + data: { + url: 'https://example.com/image.png', + }, + }); + + await expect( + db.image.update({ + where: { id: image.id }, + data: { + page: { + create: { + title: 'Page 1', + }, + }, + }, + }), + ).toResolveTruthy(); + }); + + it('regression2', async () => { + const db = await createPolicyTestClient( + ` + model Page { + id String @id @default(cuid()) + title String + + images Image[] + + @@allow('all', true) + } + + model Image { + id String @id @default(cuid()) + url String + pageId String? @deny('update', true) + page Page? @relation(fields: [pageId], references: [id]) + + @@allow('all', true) + } + `, + ); + + const image = await db.image.create({ + data: { + url: 'https://example.com/image.png', + }, + }); + + await expect( + db.image.update({ + where: { id: image.id }, + data: { + page: { + create: { + title: 'Page 1', + }, + }, + }, + }), + ).toBeRejectedByPolicy(); + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-2019.test.ts b/tests/regression/test/v2-migrated/issue-2019.test.ts new file mode 100644 index 00000000..d25ab518 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2019.test.ts @@ -0,0 +1,85 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2019', async () => { + const db = await createPolicyTestClient( + ` +model Tenant { + id String @id @default(uuid()) + + users User[] + content Content[] +} + +model User { + id String @id @default(uuid()) + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + posts Post[] + likes PostUserLikes[] + + @@allow('all', true) +} + +model Content { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @id @default(uuid()) + contentType String + + @@delegate(contentType) + @@allow('all', true) +} + +model Post extends Content { + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + comments Comment[] + likes PostUserLikes[] + + @@allow('all', true) +} + +model PostUserLikes extends Content { + userId String + user User @relation(fields: [userId], references: [id]) + + postId String + post Post @relation(fields: [postId], references: [id]) + + @@unique([userId, postId]) + + @@allow('all', true) +} + +model Comment extends Content { + postId String + post Post @relation(fields: [postId], references: [id]) + + @@allow('all', true) +} + `, + ); + + const tenant = await db.$unuseAll().tenant.create({ data: {} }); + const user = await db.$unuseAll().user.create({ data: { tenantId: tenant.id } }); + const authDb = db.$setAuth({ id: user.id, tenantId: tenant.id }); + const result = await authDb.post.create({ + data: { + likes: { + createMany: { + data: [ + { + userId: user.id, + }, + ], + }, + }, + }, + include: { + likes: true, + }, + }); + expect(result.likes[0].tenantId).toBe(tenant.id); +}); diff --git a/tests/regression/test/v2-migrated/issue-2025.test.ts b/tests/regression/test/v2-migrated/issue-2025.test.ts new file mode 100644 index 00000000..42da8fc4 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2025.test.ts @@ -0,0 +1,38 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2025', async () => { + const db = await createTestClient( + ` + model User { + id String @id @default(cuid()) + email String @unique @email + termsAndConditions Int? + @@allow('all', true) + } + `, + ); + + await expect( + db.user.create({ + data: { + email: 'xyz', + }, + }), + ).toBeRejectedByValidation(); + + const user = await db.$setInputValidation(false).user.create({ + data: { + email: 'xyz', + }, + }); + + await expect( + db.user.update({ + where: { id: user.id }, + data: { + termsAndConditions: 1, + }, + }), + ).toResolveTruthy(); +}); From d3a560681bcb545db98b6d922416fdbf7e2c63d4 Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:13:54 -0700 Subject: [PATCH 14/17] fix: prisma plugin not respecting `zenstack.output` (#298) * fix: prisma plugin not respecting `zenstack.output` * chore: add test for #295 --------- Co-authored-by: = <=> --- packages/cli/src/plugins/prisma.ts | 4 ++-- .../cli/test/plugins/prisma-plugin.test.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/plugins/prisma.ts b/packages/cli/src/plugins/prisma.ts index 1ff60780..14227565 100644 --- a/packages/cli/src/plugins/prisma.ts +++ b/packages/cli/src/plugins/prisma.ts @@ -5,10 +5,10 @@ import path from 'node:path'; const plugin: CliPlugin = { name: 'Prisma Schema Generator', statusText: 'Generating Prisma schema', - async generate({ model, schemaFile, defaultOutputPath, pluginOptions }) { + async generate({ model, defaultOutputPath, pluginOptions }) { let outFile = path.join(defaultOutputPath, 'schema.prisma'); if (typeof pluginOptions['output'] === 'string') { - outFile = path.resolve(path.dirname(schemaFile), pluginOptions['output']); + outFile = path.resolve(defaultOutputPath, pluginOptions['output']); if (!fs.existsSync(path.dirname(outFile))) { fs.mkdirSync(path.dirname(outFile), { recursive: true }); } diff --git a/packages/cli/test/plugins/prisma-plugin.test.ts b/packages/cli/test/plugins/prisma-plugin.test.ts index 23cb59f3..739252e2 100644 --- a/packages/cli/test/plugins/prisma-plugin.test.ts +++ b/packages/cli/test/plugins/prisma-plugin.test.ts @@ -57,4 +57,25 @@ model User { runCli('generate', workDir); expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true); }); + + it('can generate a Prisma schema with custom output relative to zenstack.output', () => { + const workDir = createProject(` +plugin prisma { + provider = '@core/prisma' + output = './schema.prisma' +} + +model User { + id String @id @default(cuid()) +} +`); + + const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8')); + pkgJson.zenstack = { + output: './relative', + }; + fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2)); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'relative/schema.prisma'))).toBe(true); + }); }); From f896eaf8f5ebde1f917be6fccb43f63f6c0107bf Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 15 Oct 2025 18:28:30 -0700 Subject: [PATCH 15/17] fix: delegate count relation issue, default boolean value issue (#302) * fix: delegate count relation issue, default boolean value issue * address pr comments --- .../attribute-application-validator.ts | 6 ++ .../src/client/crud/dialects/base-dialect.ts | 21 ++--- .../src/client/crud/operations/base.ts | 20 +++-- .../src/client/helpers/schema-db-pusher.ts | 14 ++- .../test/v2-migrated/issue-2028.test.ts | 90 +++++++++++++++++++ .../test/v2-migrated/issue-2038.test.ts | 25 ++++++ .../test/v2-migrated/issue-2039.test.ts | 27 ++++++ .../test/v2-migrated/issue-2106.test.ts | 16 ++++ .../test/v2-migrated/issue-2246.test.ts | 79 ++++++++++++++++ .../test/v2-migrated/issue-2247.test.ts | 61 +++++++++++++ 10 files changed, 334 insertions(+), 25 deletions(-) create mode 100644 tests/regression/test/v2-migrated/issue-2028.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2038.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2039.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2106.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2246.test.ts create mode 100644 tests/regression/test/v2-migrated/issue-2247.test.ts diff --git a/packages/language/src/validators/attribute-application-validator.ts b/packages/language/src/validators/attribute-application-validator.ts index b5384196..d1319cf0 100644 --- a/packages/language/src/validators/attribute-application-validator.ts +++ b/packages/language/src/validators/attribute-application-validator.ts @@ -22,6 +22,7 @@ import { import { getAllAttributes, getStringLiteral, + hasAttribute, isAuthOrAuthMemberAccess, isBeforeInvocation, isCollectionPredicate, @@ -364,6 +365,11 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at if (dstType === 'ContextType') { // ContextType is inferred from the attribute's container's type if (isDataField(attr.$container)) { + // If the field is Typed JSON, and the attribute is @default, the argument must be a string + const dstIsTypedJson = hasAttribute(attr.$container, '@json'); + if (dstIsTypedJson && attr.decl.ref?.name === '@default') { + return argResolvedType.decl === 'String'; + } dstIsArray = attr.$container.type.array; } } diff --git a/packages/runtime/src/client/crud/dialects/base-dialect.ts b/packages/runtime/src/client/crud/dialects/base-dialect.ts index 17d300ac..1b8b1e1c 100644 --- a/packages/runtime/src/client/crud/dialects/base-dialect.ts +++ b/packages/runtime/src/client/crud/dialects/base-dialect.ts @@ -991,15 +991,14 @@ export abstract class BaseCrudDialect { for (const [field, value] of Object.entries(selections.select)) { const fieldDef = requireField(this.schema, model, field); - const fieldModel = fieldDef.type; + const fieldModel = fieldDef.type as GetModels; let fieldCountQuery: SelectQueryBuilder; // join conditions const m2m = getManyToManyRelation(this.schema, model, field); if (m2m) { // many-to-many relation, count the join table - fieldCountQuery = eb - .selectFrom(fieldModel) + fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false) .innerJoin(m2m.joinTable, (join) => join .onRef(`${m2m.joinTable}.${m2m.otherFkName}`, '=', `${fieldModel}.${m2m.otherPKName}`) @@ -1008,7 +1007,9 @@ export abstract class BaseCrudDialect { .select(eb.fn.countAll().as(`_count$${field}`)); } else { // build a nested query to count the number of records in the relation - fieldCountQuery = eb.selectFrom(fieldModel).select(eb.fn.countAll().as(`_count$${field}`)); + fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false).select( + eb.fn.countAll().as(`_count$${field}`), + ); // join conditions const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, fieldModel); @@ -1017,18 +1018,6 @@ export abstract class BaseCrudDialect { } } - // merge _count filter - if ( - value && - typeof value === 'object' && - 'where' in value && - value.where && - typeof value.where === 'object' - ) { - const filter = this.buildFilter(fieldModel, fieldModel, value.where); - fieldCountQuery = fieldCountQuery.where(filter); - } - jsonObject[field] = fieldCountQuery; } diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index a33913d3..4193fdfa 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -822,16 +822,20 @@ export abstract class BaseOperationHandler { continue; } if (!(field in data)) { - if (typeof fields[field]?.default === 'object' && 'kind' in fields[field].default) { - const generated = this.evalGenerator(fields[field].default); + if (typeof fieldDef?.default === 'object' && 'kind' in fieldDef.default) { + const generated = this.evalGenerator(fieldDef.default); if (generated !== undefined) { - values[field] = generated; + values[field] = this.dialect.transformPrimitive( + generated, + fieldDef.type as BuiltinType, + !!fieldDef.array, + ); } - } else if (fields[field]?.updatedAt) { + } else if (fieldDef?.updatedAt) { // TODO: should this work at kysely level instead? values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); - } else if (fields[field]?.default !== undefined) { - let value = fields[field].default; + } else if (fieldDef?.default !== undefined) { + let value = fieldDef.default; if (fieldDef.type === 'Json') { // Schema uses JSON string for default value of Json fields if (fieldDef.array && Array.isArray(value)) { @@ -842,8 +846,8 @@ export abstract class BaseOperationHandler { } values[field] = this.dialect.transformPrimitive( value, - fields[field].type as BuiltinType, - !!fields[field].array, + fieldDef.type as BuiltinType, + !!fieldDef.array, ); } } diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index 9e855398..a666b4d8 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -162,9 +162,21 @@ export class SchemaDbPusher { if (fieldDef.unique) { continue; } + if (fieldDef.originModel && fieldDef.originModel !== modelDef.name) { + // field is inherited from a base model, skip + continue; + } table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, [this.getColumnName(fieldDef)]); } else { - // multi-field constraint + // multi-field constraint, if any field is inherited from base model, skip + if ( + Object.keys(value).some((f) => { + const fDef = modelDef.fields[f]!; + return fDef.originModel && fDef.originModel !== modelDef.name; + }) + ) { + continue; + } table = table.addUniqueConstraint( `unique_${modelDef.name}_${key}`, Object.keys(value).map((f) => this.getColumnName(modelDef.fields[f]!)), diff --git a/tests/regression/test/v2-migrated/issue-2028.test.ts b/tests/regression/test/v2-migrated/issue-2028.test.ts new file mode 100644 index 00000000..cff51a9f --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2028.test.ts @@ -0,0 +1,90 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2028', async () => { + const db = await createTestClient( + ` +enum FooType { + Bar + Baz +} + +model User { + id String @id @default(cuid()) + userFolders UserFolder[] + @@allow('all', true) +} + +model Foo { + id String @id @default(cuid()) + type FooType + + userFolders UserFolder[] + + @@delegate(type) + @@allow('all', true) +} + +model Bar extends Foo { + name String +} + +model Baz extends Foo { + age Int +} + +model UserFolder { + id String @id @default(cuid()) + userId String + fooId String + + user User @relation(fields: [userId], references: [id]) + foo Foo @relation(fields: [fooId], references: [id]) + + @@unique([userId, fooId]) + @@allow('all', true) +} + `, + ); + + // Ensure we can query by the CompoundUniqueInput + const user = await db.user.create({ data: {} }); + const bar = await db.bar.create({ data: { name: 'bar' } }); + const baz = await db.baz.create({ data: { age: 1 } }); + + const userFolderA = await db.userFolder.create({ + data: { + userId: user.id, + fooId: bar.id, + }, + }); + + const userFolderB = await db.userFolder.create({ + data: { + userId: user.id, + fooId: baz.id, + }, + }); + + await expect( + db.userFolder.findUnique({ + where: { + userId_fooId: { + userId: user.id, + fooId: bar.id, + }, + }, + }), + ).resolves.toMatchObject(userFolderA); + + await expect( + db.userFolder.findUnique({ + where: { + userId_fooId: { + userId: user.id, + fooId: baz.id, + }, + }, + }), + ).resolves.toMatchObject(userFolderB); +}); diff --git a/tests/regression/test/v2-migrated/issue-2038.test.ts b/tests/regression/test/v2-migrated/issue-2038.test.ts new file mode 100644 index 00000000..61fb429a --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2038.test.ts @@ -0,0 +1,25 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2038', async () => { + const db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + flag Boolean + @@allow('all', true) +} + +model Post { + id Int @id @default(autoincrement()) + published Boolean @default(auth().flag) + @@allow('all', true) +} + `, + ); + + const authDb = db.$setAuth({ id: 1, flag: true }); + await expect(authDb.post.create({ data: {} })).resolves.toMatchObject({ + published: true, + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-2039.test.ts b/tests/regression/test/v2-migrated/issue-2039.test.ts new file mode 100644 index 00000000..ba62278d --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2039.test.ts @@ -0,0 +1,27 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2039', async () => { + const db = await createTestClient( + ` +type Foo { + a String +} + +model Bar { + id String @id @default(cuid()) + foo Foo @json @default("{ \\"a\\": \\"a\\" }") + fooList Foo[] @json @default("[{ \\"a\\": \\"b\\" }]") + @@allow('all', true) +} + `, + { provider: 'postgresql' }, + ); + + // Ensure default values are correctly set + await expect(db.bar.create({ data: {} })).resolves.toMatchObject({ + id: expect.any(String), + foo: { a: 'a' }, + fooList: [{ a: 'b' }], + }); +}); diff --git a/tests/regression/test/v2-migrated/issue-2106.test.ts b/tests/regression/test/v2-migrated/issue-2106.test.ts new file mode 100644 index 00000000..af9afe96 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2106.test.ts @@ -0,0 +1,16 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2106', async () => { + const db = await createTestClient( + ` +model User { + id Int @id + age BigInt + @@allow('all', true) +} + `, + ); + + await expect(db.user.create({ data: { id: 1, age: 1n } })).toResolveTruthy(); +}); diff --git a/tests/regression/test/v2-migrated/issue-2246.test.ts b/tests/regression/test/v2-migrated/issue-2246.test.ts new file mode 100644 index 00000000..d6ff576c --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2246.test.ts @@ -0,0 +1,79 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2246', async () => { + const db = await createTestClient( + ` +model Media { + id Int @id @default(autoincrement()) + title String + mediaType String + + @@delegate(mediaType) + @@allow('all', true) +} + +model Movie extends Media { + director Director @relation(fields: [directorId], references: [id]) + directorId Int + duration Int + rating String +} + +model Director { + id Int @id @default(autoincrement()) + name String + email String + movies Movie[] + + @@allow('all', true) +} + `, + ); + + await db.director.create({ + data: { + name: 'Christopher Nolan', + email: 'christopher.nolan@example.com', + movies: { + create: { + title: 'Inception', + duration: 148, + rating: 'PG-13', + }, + }, + }, + }); + + await expect( + db.director.findMany({ + include: { + movies: { + where: { title: 'Inception' }, + }, + }, + }), + ).resolves.toHaveLength(1); + + await expect( + db.director.findFirst({ + include: { + _count: { select: { movies: { where: { title: 'Inception' } } } }, + }, + }), + ).resolves.toMatchObject({ _count: { movies: 1 } }); + + await expect( + db.movie.findMany({ + where: { title: 'Interstellar' }, + }), + ).resolves.toHaveLength(0); + + await expect( + db.director.findFirst({ + include: { + _count: { select: { movies: { where: { title: 'Interstellar' } } } }, + }, + }), + ).resolves.toMatchObject({ _count: { movies: 0 } }); +}); diff --git a/tests/regression/test/v2-migrated/issue-2247.test.ts b/tests/regression/test/v2-migrated/issue-2247.test.ts new file mode 100644 index 00000000..6caf02a4 --- /dev/null +++ b/tests/regression/test/v2-migrated/issue-2247.test.ts @@ -0,0 +1,61 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { expect, it } from 'vitest'; + +it('verifies issue 2247', async () => { + const db = await createTestClient( + ` +model User { + id String @id @default(cuid()) + employerId String? +} + +model Member { + id String @id @default(cuid()) + placeId String + place Place @relation(fields: [placeId], references: [id]) +} + +model Place { + id String @id @default(cuid()) + name String + placeType String @map("owner_type") + members Member[] + + @@delegate(placeType) + @@unique([name, placeType]) +} + +model Country extends Place { + regions Region[] + things Thing[] +} + +model Region extends Place { + countryId String + country Country @relation(fields: [countryId], references: [id]) + cities City[] +} + +model City extends Place { + regionId String + region Region @relation(fields: [regionId], references: [id]) +} + + +model Thing { + id String @id @default(cuid()) + countryId String + country Country @relation(fields: [countryId], references: [id]) + + @@allow('read', + country.members?[id == auth().employerId] + || country.regions?[members?[id == auth().employerId]] + || country.regions?[cities?[members?[id == auth().employerId]]] + ) +} + `, + ); + + const authDb = db.$setAuth({ id: '1', employerId: '1' }); + await expect(authDb.thing.findMany()).toResolveTruthy(); +}); From e0040cb974240e211dab91b5cd8cfc3e73e26065 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 15 Oct 2025 18:57:56 -0700 Subject: [PATCH 16/17] chore: test refactor (#303) --- packages/cli/test/ts-schema-gen.test.ts | 60 ++- packages/sdk/src/ts-schema-generator.ts | 8 +- packages/testtools/src/client.ts | 5 +- .../test/v2-migrated/issue-1058.test.ts | 12 +- .../test/v2-migrated/issue-1080.test.ts | 194 ++++----- .../test/v2-migrated/issue-1123.test.ts | 32 +- .../test/v2-migrated/issue-1135.test.ts | 66 +-- .../test/v2-migrated/issue-1149.test.ts | 102 ++--- .../test/v2-migrated/issue-1167.test.ts | 12 +- .../test/v2-migrated/issue-1179.test.ts | 12 +- .../test/v2-migrated/issue-1241.test.ts | 82 ++-- .../test/v2-migrated/issue-1257.test.ts | 22 +- .../test/v2-migrated/issue-1271.test.ts | 260 ++++++------ .../test/v2-migrated/issue-1381.test.ts | 14 +- .../test/v2-migrated/issue-1388.test.ts | 22 +- .../test/v2-migrated/issue-1410.test.ts | 34 +- .../test/v2-migrated/issue-1415.test.ts | 12 +- .../test/v2-migrated/issue-1416.test.ts | 12 +- .../test/v2-migrated/issue-1427.test.ts | 40 +- .../test/v2-migrated/issue-1467.test.ts | 30 +- .../test/v2-migrated/issue-1483.test.ts | 54 +-- .../test/v2-migrated/issue-1487.test.ts | 50 +-- .../test/v2-migrated/issue-1506.test.ts | 12 +- .../test/v2-migrated/issue-1507.test.ts | 20 +- .../test/v2-migrated/issue-1518.test.ts | 24 +- .../test/v2-migrated/issue-1520.test.ts | 66 +-- .../test/v2-migrated/issue-1522.test.ts | 150 +++---- .../test/v2-migrated/issue-1530.test.ts | 54 +-- .../test/v2-migrated/issue-1551.test.ts | 14 +- .../test/v2-migrated/issue-1562.test.ts | 38 +- .../test/v2-migrated/issue-1563.test.ts | 40 +- .../test/v2-migrated/issue-1575.test.ts | 12 +- .../test/v2-migrated/issue-1576.test.ts | 108 ++--- .../test/v2-migrated/issue-1585.test.ts | 48 ++- .../test/v2-migrated/issue-1627.test.ts | 90 ++-- .../test/v2-migrated/issue-1642.test.ts | 64 +-- .../test/v2-migrated/issue-1645.test.ts | 398 +++++++++--------- .../test/v2-migrated/issue-1648.test.ts | 82 ++-- .../test/v2-migrated/issue-1674.test.ts | 124 +++--- .../test/v2-migrated/issue-1681.test.ts | 48 ++- .../test/v2-migrated/issue-1693.test.ts | 12 +- .../test/v2-migrated/issue-1695.test.ts | 12 +- .../test/v2-migrated/issue-1698.test.ts | 116 ++--- .../test/v2-migrated/issue-1745.test.ts | 12 +- .../test/v2-migrated/issue-1755.test.ts | 112 ++--- .../test/v2-migrated/issue-1758.test.ts | 14 +- .../test/v2-migrated/issue-1763.test.ts | 20 +- .../test/v2-migrated/issue-177.test.ts | 20 +- .../test/v2-migrated/issue-1786.test.ts | 14 +- .../test/v2-migrated/issue-1835.test.ts | 12 +- .../test/v2-migrated/issue-1849.test.ts | 22 +- .../test/v2-migrated/issue-1857.test.ts | 26 +- .../test/v2-migrated/issue-1870.test.ts | 12 +- .../test/v2-migrated/issue-1894.test.ts | 86 ++-- .../test/v2-migrated/issue-1930.test.ts | 118 +++--- .../test/v2-migrated/issue-1991.test.ts | 22 +- .../test/v2-migrated/issue-1992.test.ts | 12 +- .../test/v2-migrated/issue-1994.test.ts | 166 ++++---- .../test/v2-migrated/issue-1997.test.ts | 206 ++++----- .../test/v2-migrated/issue-1998.test.ts | 110 ++--- .../test/v2-migrated/issue-2019.test.ts | 162 +++---- .../test/v2-migrated/issue-2025.test.ts | 60 +-- .../test/v2-migrated/issue-2028.test.ts | 144 +++---- .../test/v2-migrated/issue-2038.test.ts | 40 +- .../test/v2-migrated/issue-2039.test.ts | 44 +- .../test/v2-migrated/issue-2106.test.ts | 26 +- .../test/v2-migrated/issue-2246.test.ts | 130 +++--- .../test/v2-migrated/issue-2247.test.ts | 120 +++--- .../test/v2-migrated/issue-389.test.ts | 26 +- .../test/v2-migrated/issue-392.test.ts | 46 +- .../test/v2-migrated/issue-416.test.ts | 12 +- .../test/v2-migrated/issue-509.test.ts | 18 +- .../test/v2-migrated/issue-609.test.ts | 96 ++--- .../test/v2-migrated/issue-632.test.ts | 16 +- .../test/v2-migrated/issue-646.test.ts | 8 +- .../test/v2-migrated/issue-674.test.ts | 12 +- .../test/v2-migrated/issue-689.test.ts | 112 ++--- .../test/v2-migrated/issue-714.test.ts | 104 ++--- .../test/v2-migrated/issue-735.test.ts | 12 +- .../test/v2-migrated/issue-756.test.ts | 20 +- .../test/v2-migrated/issue-764.test.ts | 48 ++- .../test/v2-migrated/issue-765.test.ts | 54 +-- .../test/v2-migrated/issue-804.test.ts | 20 +- .../test/v2-migrated/issue-811.test.ts | 118 +++--- .../test/v2-migrated/issue-825.test.ts | 62 +-- .../test/v2-migrated/issue-947.test.ts | 14 +- .../test/v2-migrated/issue-971.test.ts | 14 +- 87 files changed, 2681 insertions(+), 2508 deletions(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index d29a0c5c..4ac6f94f 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -370,56 +370,54 @@ model User { expect(schema.models).toMatchObject({ User: { - name: "User", + name: 'User', fields: { id: { - name: "id", - type: "String", + name: 'id', + type: 'String', id: true, attributes: [ { - name: "@id" + name: '@id', }, { - name: "@default", + name: '@default', args: [ { - name: "value", + name: 'value', value: { - kind: "call", - function: "uuid", + kind: 'call', + function: 'uuid', args: [ { - kind: "literal", - value: 7 - } - ] - } - } - ] - } + kind: 'literal', + value: 7, + }, + ], + }, + }, + ], + }, ], default: { - kind: "call", - function: "uuid", + kind: 'call', + function: 'uuid', args: [ { - kind: "literal", - value: 7 - } - ] - } - } + kind: 'literal', + value: 7, + }, + ], + }, + }, }, - idFields: [ - "id" - ], + idFields: ['id'], uniqueFields: { id: { - type: "String" - } - } - } + type: 'String', + }, + }, + }, }); }); }); diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 78ba40e0..9c393bd5 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -507,9 +507,11 @@ export class TsSchemaGenerator { ...(defaultValue.args.length > 0 ? [ ts.factory.createArrayLiteralExpression( - defaultValue.args.map((arg) => this.createExpressionUtilsCall('literal', [ - this.createLiteralNode(arg) - ])), + defaultValue.args.map((arg) => + this.createExpressionUtilsCall('literal', [ + this.createLiteralNode(arg), + ]), + ), ), ] : []), diff --git a/packages/testtools/src/client.ts b/packages/testtools/src/client.ts index cb3fded5..4a23280e 100644 --- a/packages/testtools/src/client.ts +++ b/packages/testtools/src/client.ts @@ -37,6 +37,7 @@ export type CreateTestClientOptions = Omit; workDir?: string; + debug?: boolean; }; export async function createTestClient( @@ -103,7 +104,9 @@ export async function createTestClient( } invariant(workDir); - console.log(`Work directory: ${workDir}`); + if (options?.debug) { + console.log(`Work directory: ${workDir}`); + } const { plugins, ...rest } = options ?? {}; const _options: ClientOptions = { diff --git a/tests/regression/test/v2-migrated/issue-1058.test.ts b/tests/regression/test/v2-migrated/issue-1058.test.ts index fed09565..e89514ab 100644 --- a/tests/regression/test/v2-migrated/issue-1058.test.ts +++ b/tests/regression/test/v2-migrated/issue-1058.test.ts @@ -1,8 +1,9 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1058', async () => { - const schema = ` +describe('Regression for issue #1058', () => { + it('verifies issue 1058', async () => { + const schema = ` model User { id String @id @default(cuid()) name String @@ -45,8 +46,9 @@ it('verifies issue 1058', async () => { entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - } + } `; - await createTestClient(schema, { provider: 'postgresql' }); + await createTestClient(schema, { provider: 'postgresql' }); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1080.test.ts b/tests/regression/test/v2-migrated/issue-1080.test.ts index 0f46beca..aacef5cd 100644 --- a/tests/regression/test/v2-migrated/issue-1080.test.ts +++ b/tests/regression/test/v2-migrated/issue-1080.test.ts @@ -1,9 +1,10 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1080', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #1080', () => { + it('verifies issue 1080', async () => { + const db = await createPolicyTestClient( + ` model Project { id String @id @unique @default(uuid()) Fields Field[] @@ -20,110 +21,111 @@ model Field { @@allow('all', true) } `, - ); + ); - const project = await db.project.create({ - include: { Fields: true }, - data: { - Fields: { - create: [{ name: 'first' }, { name: 'second' }], + const project = await db.project.create({ + include: { Fields: true }, + data: { + Fields: { + create: [{ name: 'first' }, { name: 'second' }], + }, }, - }, - }); + }); - let updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: [ - { - where: { id: project.Fields[0].id }, - create: { name: 'first1' }, - update: { name: 'first1' }, - }, - { - where: { id: project.Fields[1].id }, - create: { name: 'second1' }, - update: { name: 'second1' }, - }, - ], + let updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: [ + { + where: { id: project.Fields[0].id }, + create: { name: 'first1' }, + update: { name: 'first1' }, + }, + { + where: { id: project.Fields[1].id }, + create: { name: 'second1' }, + update: { name: 'second1' }, + }, + ], + }, }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first1' }), - expect.objectContaining({ name: 'second1' }), - ]), - }); + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first1' }), + expect.objectContaining({ name: 'second1' }), + ]), + }); - updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: { - where: { id: project.Fields[0].id }, - create: { name: 'first2' }, - update: { name: 'first2' }, + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: project.Fields[0].id }, + create: { name: 'first2' }, + update: { name: 'first2' }, + }, }, }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first2' }), - expect.objectContaining({ name: 'second1' }), - ]), - }); + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first2' }), + expect.objectContaining({ name: 'second1' }), + ]), + }); - updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: { - where: { id: project.Fields[0].id }, - create: { name: 'first3' }, - update: { name: 'first3' }, - }, - update: { - where: { id: project.Fields[1].id }, - data: { name: 'second3' }, + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: project.Fields[0].id }, + create: { name: 'first3' }, + update: { name: 'first3' }, + }, + update: { + where: { id: project.Fields[1].id }, + data: { name: 'second3' }, + }, }, }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first3' }), - expect.objectContaining({ name: 'second3' }), - ]), - }); + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first3' }), + expect.objectContaining({ name: 'second3' }), + ]), + }); - updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: { - where: { id: 'non-exist' }, - create: { name: 'third1' }, - update: { name: 'third1' }, - }, - update: { - where: { id: project.Fields[1].id }, - data: { name: 'second4' }, + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: 'non-exist' }, + create: { name: 'third1' }, + update: { name: 'third1' }, + }, + update: { + where: { id: project.Fields[1].id }, + data: { name: 'second4' }, + }, }, }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first3' }), - expect.objectContaining({ name: 'second4' }), - expect.objectContaining({ name: 'third1' }), - ]), + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first3' }), + expect.objectContaining({ name: 'second4' }), + expect.objectContaining({ name: 'third1' }), + ]), + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1123.test.ts b/tests/regression/test/v2-migrated/issue-1123.test.ts index 3c1cb4d0..4f2b5a5f 100644 --- a/tests/regression/test/v2-migrated/issue-1123.test.ts +++ b/tests/regression/test/v2-migrated/issue-1123.test.ts @@ -1,9 +1,10 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1123', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #1123', () => { + it('verifies issue 1123', async () => { + const db = await createPolicyTestClient( + ` model Content { id String @id @default(cuid()) published Boolean @default(false) @@ -12,7 +13,7 @@ model Content { @@delegate(contentType) @@allow('all', true) } - + model Post extends Content { title String } @@ -28,16 +29,19 @@ model Like { @@allow('all', true) } `, - ); + ); - await db.post.create({ - data: { - title: 'a post', - likes: { create: {} }, - }, - }); + await db.post.create({ + data: { + title: 'a post', + likes: { create: {} }, + }, + }); - await expect(db.content.findFirst({ include: { _count: { select: { likes: true } } } })).resolves.toMatchObject({ - _count: { likes: 1 }, + await expect(db.content.findFirst({ include: { _count: { select: { likes: true } } } })).resolves.toMatchObject( + { + _count: { likes: 1 }, + }, + ); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1135.test.ts b/tests/regression/test/v2-migrated/issue-1135.test.ts index 41df934f..521dd133 100644 --- a/tests/regression/test/v2-migrated/issue-1135.test.ts +++ b/tests/regression/test/v2-migrated/issue-1135.test.ts @@ -1,22 +1,23 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1135', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1135', () => { + it('verifies issue 1135', async () => { + const db = await createTestClient( + ` model Attachment { id String @id @default(cuid()) url String myEntityId String myEntity Entity @relation(fields: [myEntityId], references: [id], onUpdate: NoAction) } - + model Entity { - id String @id @default(cuid()) + id String @id @default(cuid()) name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @default(now()) - + attachments Attachment[] type String @@ -27,9 +28,9 @@ model Person extends Entity { age Int? } `, - { - extraSourceFiles: { - 'main.ts': ` + { + extraSourceFiles: { + 'main.ts': ` import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './schema'; @@ -46,31 +47,32 @@ db.person.create({ }, }); `, + }, }, - }, - ); + ); - await expect( - db.person.create({ - data: { - name: 'test', - attachments: { - create: { - url: 'https://...', + await expect( + db.person.create({ + data: { + name: 'test', + attachments: { + create: { + url: 'https://...', + }, }, }, - }, - include: { attachments: true }, - }), - ).resolves.toMatchObject({ - id: expect.any(String), - name: 'test', - attachments: [ - { - id: expect.any(String), - url: 'https://...', - myEntityId: expect.any(String), - }, - ], + include: { attachments: true }, + }), + ).resolves.toMatchObject({ + id: expect.any(String), + name: 'test', + attachments: [ + { + id: expect.any(String), + url: 'https://...', + myEntityId: expect.any(String), + }, + ], + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1149.test.ts b/tests/regression/test/v2-migrated/issue-1149.test.ts index 404c3969..ff18a4b5 100644 --- a/tests/regression/test/v2-migrated/issue-1149.test.ts +++ b/tests/regression/test/v2-migrated/issue-1149.test.ts @@ -1,90 +1,92 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1149', async () => { - const schema = ` +describe('Regression for issue #1149', () => { + it('verifies issue 1149', async () => { + const schema = ` model User { id String @id @default(cuid()) name String - + userRankings UserRanking[] userFavorites UserFavorite[] } - + model Entity { id String @id @default(cuid()) name String type String userRankings UserRanking[] userFavorites UserFavorite[] - + @@delegate(type) } - + model Person extends Entity { } - + model Studio extends Entity { } - - + + model UserRanking { id String @id @default(cuid()) rank Int - + entityId String entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) } - + model UserFavorite { id String @id @default(cuid()) - + entityId String entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - } + } `; - const db = await createTestClient(schema); - - const user = await db.user.create({ data: { name: 'user' } }); - const person = await db.person.create({ data: { name: 'person' } }); - - await expect( - db.userRanking.createMany({ - data: { - rank: 1, - entityId: person.id, - userId: user.id, - }, - }), - ).resolves.toMatchObject({ count: 1 }); - - await expect( - db.userRanking.createMany({ - data: [ - { - rank: 2, - entityId: person.id, - userId: user.id, - }, - { - rank: 3, + const db = await createTestClient(schema); + + const user = await db.user.create({ data: { name: 'user' } }); + const person = await db.person.create({ data: { name: 'person' } }); + + await expect( + db.userRanking.createMany({ + data: { + rank: 1, entityId: person.id, userId: user.id, }, - ], - }), - ).resolves.toMatchObject({ count: 2 }); - - await expect(db.userRanking.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ rank: 1 }), - expect.objectContaining({ rank: 2 }), - expect.objectContaining({ rank: 3 }), - ]), - ); + }), + ).resolves.toMatchObject({ count: 1 }); + + await expect( + db.userRanking.createMany({ + data: [ + { + rank: 2, + entityId: person.id, + userId: user.id, + }, + { + rank: 3, + entityId: person.id, + userId: user.id, + }, + ], + }), + ).resolves.toMatchObject({ count: 2 }); + + await expect(db.userRanking.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ rank: 1 }), + expect.objectContaining({ rank: 2 }), + expect.objectContaining({ rank: 3 }), + ]), + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1167.test.ts b/tests/regression/test/v2-migrated/issue-1167.test.ts index 9a18c374..5b81fa67 100644 --- a/tests/regression/test/v2-migrated/issue-1167.test.ts +++ b/tests/regression/test/v2-migrated/issue-1167.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1167', async () => { - await loadSchema( - ` +describe('Regression for issue #1167', () => { + it('verifies issue 1167', async () => { + await loadSchema( + ` model FileAsset { id String @id @default(cuid()) delegate_type String @@ -15,5 +16,6 @@ model ImageAsset extends FileAsset { @@map("image_assets") } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1179.test.ts b/tests/regression/test/v2-migrated/issue-1179.test.ts index b6a21879..0f2611ae 100644 --- a/tests/regression/test/v2-migrated/issue-1179.test.ts +++ b/tests/regression/test/v2-migrated/issue-1179.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('regression', async () => { - await loadSchema( - ` +describe('Regression for issue #1179', () => { + it('regression', async () => { + await loadSchema( + ` type Base { id String @id @default(uuid()) } @@ -22,5 +23,6 @@ model Post { @@allow('all', auth().id == userId) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1241.test.ts b/tests/regression/test/v2-migrated/issue-1241.test.ts index bddfa4e8..f0f53fab 100644 --- a/tests/regression/test/v2-migrated/issue-1241.test.ts +++ b/tests/regression/test/v2-migrated/issue-1241.test.ts @@ -1,10 +1,11 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; import { randomBytes } from 'crypto'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1241', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #1241', () => { + it('verifies issue 1241', async () => { + const db = await createPolicyTestClient( + ` model User { id String @id @default(uuid()) todos Todo[] @@ -39,46 +40,47 @@ model File { @@allow('all', true) } `, - ); + ); - const user = await db.$unuseAll().user.create({ - data: {}, - }); - await db.$unuseAll().todo.create({ - data: { - user_id: user.id, - - images: { - create: new Array(3).fill(null).map((_, i) => ({ - s3_key: randomBytes(8).toString('hex'), - label: `img-label-${i + 1}`, - })), - }, + const user = await db.$unuseAll().user.create({ + data: {}, + }); + await db.$unuseAll().todo.create({ + data: { + user_id: user.id, - documents: { - create: new Array(3).fill(null).map((_, i) => ({ - s3_key: randomBytes(8).toString('hex'), - label: `doc-label-${i + 1}`, - })), - }, - }, - }); + images: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `img-label-${i + 1}`, + })), + }, - const todo = await db.todo.findFirst({ where: {}, include: { documents: true } }); - await expect( - db.todo.update({ - where: { id: todo.id }, - data: { documents: { - update: todo.documents.map((doc: any) => { - return { - where: { s3_key: doc.s3_key }, - data: { label: 'updated' }, - }; - }), + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `doc-label-${i + 1}`, + })), }, }, - include: { documents: true }, - }), - ).toResolveTruthy(); + }); + + const todo = await db.todo.findFirst({ where: {}, include: { documents: true } }); + await expect( + db.todo.update({ + where: { id: todo.id }, + data: { + documents: { + update: todo.documents.map((doc: any) => { + return { + where: { s3_key: doc.s3_key }, + data: { label: 'updated' }, + }; + }), + }, + }, + include: { documents: true }, + }), + ).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1257.test.ts b/tests/regression/test/v2-migrated/issue-1257.test.ts index 38fc799e..acd1a258 100644 --- a/tests/regression/test/v2-migrated/issue-1257.test.ts +++ b/tests/regression/test/v2-migrated/issue-1257.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1257', async () => { - await loadSchema( - ` +describe('Regression for issue #1257', () => { + it('verifies issue 1257', async () => { + await loadSchema( + ` import "./user" import "./image" @@ -11,13 +12,13 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") }`, - { - base: ` + { + base: ` type Base { id Int @id @default(autoincrement()) } `, - user: ` + user: ` import "./base" import "./image" @@ -31,7 +32,7 @@ model User with Base { @@auth } `, - image: ` + image: ` import "./user" import "./base" @@ -43,6 +44,7 @@ model Image with Base { @@allow('all', auth().role == Admin) } `, - }, - ); + }, + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1271.test.ts b/tests/regression/test/v2-migrated/issue-1271.test.ts index d6acb7e6..8c264a36 100644 --- a/tests/regression/test/v2-migrated/issue-1271.test.ts +++ b/tests/regression/test/v2-migrated/issue-1271.test.ts @@ -1,188 +1,190 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1271', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #1271', () => { + it('verifies issue 1271', async () => { + const db = await createPolicyTestClient( + ` model User { id String @id @default(uuid()) - + @@auth @@allow('all', true) } - + model Test { id String @id @default(uuid()) linkingTable LinkingTable[] key String @default('test') locale String @default('EN') - + @@unique([key, locale]) @@allow("all", true) } - + model LinkingTable { test_id String test Test @relation(fields: [test_id], references: [id]) - + another_test_id String another_test AnotherTest @relation(fields: [another_test_id], references: [id]) - + @@id([test_id, another_test_id]) @@allow("all", true) } - + model AnotherTest { id String @id @default(uuid()) status String linkingTable LinkingTable[] - + @@allow("all", true) -} +} `, - ); + ); - const test = await db.test.create({ - data: { - key: 'test1', - }, - }); - const anotherTest = await db.anotherTest.create({ - data: { - status: 'available', - }, - }); + const test = await db.test.create({ + data: { + key: 'test1', + }, + }); + const anotherTest = await db.anotherTest.create({ + data: { + status: 'available', + }, + }); - const updated = await db.test.upsert({ - where: { - key_locale: { - key: test.key, - locale: test.locale, + const updated = await db.test.upsert({ + where: { + key_locale: { + key: test.key, + locale: test.locale, + }, }, - }, - create: { - linkingTable: { - create: { - another_test_id: anotherTest.id, + create: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, }, }, - }, - update: { - linkingTable: { - create: { - another_test_id: anotherTest.id, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, }, }, - }, - include: { - linkingTable: true, - }, - }); + include: { + linkingTable: true, + }, + }); - expect(updated.linkingTable).toHaveLength(1); - expect(updated.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + expect(updated.linkingTable).toHaveLength(1); + expect(updated.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); - const test2 = await db.test.upsert({ - where: { - key_locale: { + const test2 = await db.test.upsert({ + where: { + key_locale: { + key: 'test2', + locale: 'locale2', + }, + }, + create: { key: 'test2', locale: 'locale2', - }, - }, - create: { - key: 'test2', - locale: 'locale2', - linkingTable: { - create: { - another_test_id: anotherTest.id, + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, }, }, - }, - update: { - linkingTable: { - create: { - another_test_id: anotherTest.id, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, }, }, - }, - include: { - linkingTable: true, - }, - }); - expect(test2).toMatchObject({ key: 'test2', locale: 'locale2' }); - expect(test2.linkingTable).toHaveLength(1); - expect(test2.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); - - const linkingTable = test2.linkingTable[0]; - - // connectOrCreate: connect case - const test3 = await db.test.create({ - data: { - key: 'test3', - locale: 'locale3', - }, - }); - console.log('test3 created:', test3); - const updated2 = await db.linkingTable.update({ - where: { - test_id_another_test_id: { - test_id: linkingTable.test_id, - another_test_id: linkingTable.another_test_id, + include: { + linkingTable: true, }, - }, - data: { - test: { - connectOrCreate: { - where: { - key_locale: { - key: test3.key, - locale: test3.locale, + }); + expect(test2).toMatchObject({ key: 'test2', locale: 'locale2' }); + expect(test2.linkingTable).toHaveLength(1); + expect(test2.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const linkingTable = test2.linkingTable[0]; + + // connectOrCreate: connect case + const test3 = await db.test.create({ + data: { + key: 'test3', + locale: 'locale3', + }, + }); + console.log('test3 created:', test3); + const updated2 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: linkingTable.test_id, + another_test_id: linkingTable.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: test3.key, + locale: test3.locale, + }, + }, + create: { + key: 'test4', + locale: 'locale4', }, - }, - create: { - key: 'test4', - locale: 'locale4', }, }, + another_test: { connect: { id: anotherTest.id } }, }, - another_test: { connect: { id: anotherTest.id } }, - }, - include: { test: true }, - }); - expect(updated2).toMatchObject({ - test: expect.objectContaining({ key: 'test3', locale: 'locale3' }), - another_test_id: anotherTest.id, - }); + include: { test: true }, + }); + expect(updated2).toMatchObject({ + test: expect.objectContaining({ key: 'test3', locale: 'locale3' }), + another_test_id: anotherTest.id, + }); - // connectOrCreate: create case - const updated3 = await db.linkingTable.update({ - where: { - test_id_another_test_id: { - test_id: updated2.test_id, - another_test_id: updated2.another_test_id, + // connectOrCreate: create case + const updated3 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: updated2.test_id, + another_test_id: updated2.another_test_id, + }, }, - }, - data: { - test: { - connectOrCreate: { - where: { - key_locale: { + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: 'test4', + locale: 'locale4', + }, + }, + create: { key: 'test4', locale: 'locale4', }, }, - create: { - key: 'test4', - locale: 'locale4', - }, }, + another_test: { connect: { id: anotherTest.id } }, }, - another_test: { connect: { id: anotherTest.id } }, - }, - include: { test: true }, - }); - expect(updated3).toMatchObject({ - test: expect.objectContaining({ key: 'test4', locale: 'locale4' }), - another_test_id: anotherTest.id, + include: { test: true }, + }); + expect(updated3).toMatchObject({ + test: expect.objectContaining({ key: 'test4', locale: 'locale4' }), + another_test_id: anotherTest.id, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1381.test.ts b/tests/regression/test/v2-migrated/issue-1381.test.ts index 3f168270..2789d29e 100644 --- a/tests/regression/test/v2-migrated/issue-1381.test.ts +++ b/tests/regression/test/v2-migrated/issue-1381.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1381', async () => { - await loadSchema( - ` +describe('Regression for issue #1381', () => { + it('verifies issue 1381', async () => { + await loadSchema( + ` enum MemberRole { owner admin @@ -49,7 +50,8 @@ model Option { space.type in [contractor, public] && space.memberships?[space.type in [contractor, public] && auth() == user && role in [owner, admin]] ) -} +} `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1388.test.ts b/tests/regression/test/v2-migrated/issue-1388.test.ts index ab3f4701..5d617708 100644 --- a/tests/regression/test/v2-migrated/issue-1388.test.ts +++ b/tests/regression/test/v2-migrated/issue-1388.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1388', async () => { - await loadSchema( - ` +describe('Regression for issue #1388', () => { + it('verifies issue 1388', async () => { + await loadSchema( + ` import './auth' import './post' @@ -12,14 +13,14 @@ datasource db { url = env("DATABASE_URL") } `, - { - auth: ` + { + auth: ` model User { id String @id @default(cuid()) role String -} +} `, - post: ` + post: ` model Post { id String @id @default(nanoid(6)) title String @@ -27,6 +28,7 @@ model Post { @@allow('all', auth().id == 'user1') } `, - }, - ); + }, + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1410.test.ts b/tests/regression/test/v2-migrated/issue-1410.test.ts index c4f7c2db..62749fc2 100644 --- a/tests/regression/test/v2-migrated/issue-1410.test.ts +++ b/tests/regression/test/v2-migrated/issue-1410.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1410', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1410', () => { + it('verifies issue 1410', async () => { + const db = await createTestClient( + ` model Drink { id Int @id @default(autoincrement()) slug String @unique @@ -124,20 +125,21 @@ it('verifies issue 1410', async () => { @@allow('all', true) } `, - ); + ); - await db.beer.findMany({ - include: { style: true, manufacturer: true }, - where: { NOT: { gluten: true } }, - }); + await db.beer.findMany({ + include: { style: true, manufacturer: true }, + where: { NOT: { gluten: true } }, + }); - await db.beer.findMany({ - include: { style: true, manufacturer: true }, - where: { AND: [{ gluten: true }, { abv: { gt: 50 } }] }, - }); + await db.beer.findMany({ + include: { style: true, manufacturer: true }, + where: { AND: [{ gluten: true }, { abv: { gt: 50 } }] }, + }); - await db.beer.findMany({ - include: { style: true, manufacturer: true }, - where: { OR: [{ AND: [{ NOT: { gluten: true } }] }, { abv: { gt: 50 } }] }, + await db.beer.findMany({ + include: { style: true, manufacturer: true }, + where: { OR: [{ AND: [{ NOT: { gluten: true } }] }, { abv: { gt: 50 } }] }, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1415.test.ts b/tests/regression/test/v2-migrated/issue-1415.test.ts index 0ebbf7e9..2adecac0 100644 --- a/tests/regression/test/v2-migrated/issue-1415.test.ts +++ b/tests/regression/test/v2-migrated/issue-1415.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1415', async () => { - await loadSchema( - ` +describe('Regression for issue #1415', () => { + it('verifies issue 1415', async () => { + await loadSchema( + ` model User { id String @id @default(cuid()) prices Price[] @@ -17,5 +18,6 @@ model Price { @@delegate(priceType) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1416.test.ts b/tests/regression/test/v2-migrated/issue-1416.test.ts index 461ff068..d89b8280 100644 --- a/tests/regression/test/v2-migrated/issue-1416.test.ts +++ b/tests/regression/test/v2-migrated/issue-1416.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1416', async () => { - await loadSchema( - ` +describe('Regression for issue #1416', () => { + it('verifies issue 1416', async () => { + await loadSchema( + ` model User { id String @id @default(cuid()) role String @@ -32,5 +33,6 @@ model MyEntity extends Entity { foo String } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1427.test.ts b/tests/regression/test/v2-migrated/issue-1427.test.ts index f111288f..84836443 100644 --- a/tests/regression/test/v2-migrated/issue-1427.test.ts +++ b/tests/regression/test/v2-migrated/issue-1427.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1427', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1427', () => { + it('verifies issue 1427', async () => { + const db = await createTestClient( + ` model User { id String @id @default(cuid()) name String @@ -18,23 +19,24 @@ model Profile { @@allow('all', true) } `, - ); + ); - await db.$unuseAll().user.create({ - data: { - name: 'John', - profile: { - create: {}, + await db.$unuseAll().user.create({ + data: { + name: 'John', + profile: { + create: {}, + }, }, - }, - }); + }); - const found = await db.user.findFirst({ - select: { - id: true, - name: true, - profile: false, - }, + const found = await db.user.findFirst({ + select: { + id: true, + name: true, + profile: false, + }, + }); + expect(found.profile).toBeUndefined(); }); - expect(found.profile).toBeUndefined(); }); diff --git a/tests/regression/test/v2-migrated/issue-1467.test.ts b/tests/regression/test/v2-migrated/issue-1467.test.ts index 042ef8b6..e3ff1ffd 100644 --- a/tests/regression/test/v2-migrated/issue-1467.test.ts +++ b/tests/regression/test/v2-migrated/issue-1467.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1467', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1467', () => { + it('verifies issue 1467', async () => { + const db = await createTestClient( + ` model User { id Int @id @default(autoincrement()) type String @@ -27,18 +28,19 @@ it('verifies issue 1467', async () => { model Beer extends Drink { } `, - ); + ); - await db.beer.create({ - data: { id: 1, name: 'Beer1' }, - }); + await db.beer.create({ + data: { id: 1, name: 'Beer1' }, + }); - await db.container.create({ data: { drink: { connect: { id: 1 } } } }); - await db.container.create({ data: { drink: { connect: { id: 1 } } } }); + await db.container.create({ data: { drink: { connect: { id: 1 } } } }); + await db.container.create({ data: { drink: { connect: { id: 1 } } } }); - const beers = await db.beer.findFirst({ - select: { id: true, name: true, _count: { select: { containers: true } } }, - orderBy: { name: 'asc' }, + const beers = await db.beer.findFirst({ + select: { id: true, name: true, _count: { select: { containers: true } } }, + orderBy: { name: 'asc' }, + }); + expect(beers).toMatchObject({ _count: { containers: 2 } }); }); - expect(beers).toMatchObject({ _count: { containers: 2 } }); }); diff --git a/tests/regression/test/v2-migrated/issue-1483.test.ts b/tests/regression/test/v2-migrated/issue-1483.test.ts index 8802a312..fa35644c 100644 --- a/tests/regression/test/v2-migrated/issue-1483.test.ts +++ b/tests/regression/test/v2-migrated/issue-1483.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1483', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1483', () => { + it('verifies issue 1483', async () => { + const db = await createTestClient( + ` model User { @@auth id String @id @@ -38,30 +39,31 @@ model Edit { @@allow('all', true) } `, - ); + ); - await db.edit.deleteMany({}); - await db.person.deleteMany({}); - await db.user.deleteMany({}); + await db.edit.deleteMany({}); + await db.person.deleteMany({}); + await db.user.deleteMany({}); - const person = await db.person.create({ - data: { - name: 'test', - }, - }); - - await db.edit.create({ - data: { - entityId: person.id, - }, - }); + const person = await db.person.create({ + data: { + name: 'test', + }, + }); - await expect( - db.edit.findMany({ - include: { - author: true, - entity: true, + await db.edit.create({ + data: { + entityId: person.id, }, - }), - ).resolves.toHaveLength(1); + }); + + await expect( + db.edit.findMany({ + include: { + author: true, + entity: true, + }, + }), + ).resolves.toHaveLength(1); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1487.test.ts b/tests/regression/test/v2-migrated/issue-1487.test.ts index acf39ead..f75ac391 100644 --- a/tests/regression/test/v2-migrated/issue-1487.test.ts +++ b/tests/regression/test/v2-migrated/issue-1487.test.ts @@ -1,10 +1,11 @@ import { createTestClient } from '@zenstackhq/testtools'; import Decimal from 'decimal.js'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1487', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1487', () => { + it('verifies issue 1487', async () => { + const db = await createTestClient( + ` model LineItem { id Int @id @default(autoincrement()) price Decimal @@ -25,28 +26,29 @@ model BaseType { @@delegate(entityType) } `, - ); + ); - const create = await db.Order.create({ - data: { - total: new Decimal(100_100.99), - lineItems: { create: [{ price: 90_000.66 }, { price: 20_100.33 }] }, - }, - }); + const create = await db.Order.create({ + data: { + total: new Decimal(100_100.99), + lineItems: { create: [{ price: 90_000.66 }, { price: 20_100.33 }] }, + }, + }); - const order = await db.Order.findFirst({ where: { id: create.id }, include: { lineItems: true } }); - expect(Decimal.isDecimal(order.total)).toBe(true); - expect(order.createdAt instanceof Date).toBe(true); - expect(order.total.toString()).toEqual('100100.99'); - order.lineItems.forEach((item: any) => { - expect(Decimal.isDecimal(item.price)).toBe(true); - expect(item.price.toString()).not.toEqual('[object Object]'); - }); + const order = await db.Order.findFirst({ where: { id: create.id }, include: { lineItems: true } }); + expect(Decimal.isDecimal(order.total)).toBe(true); + expect(order.createdAt instanceof Date).toBe(true); + expect(order.total.toString()).toEqual('100100.99'); + order.lineItems.forEach((item: any) => { + expect(Decimal.isDecimal(item.price)).toBe(true); + expect(item.price.toString()).not.toEqual('[object Object]'); + }); - const lineItems = await db.LineItem.findMany(); - lineItems.forEach((item: any) => { - expect(item.createdAt instanceof Date).toBe(true); - expect(Decimal.isDecimal(item.price)).toBe(true); - expect(item.price.toString()).not.toEqual('[object Object]'); + const lineItems = await db.LineItem.findMany(); + lineItems.forEach((item: any) => { + expect(item.createdAt instanceof Date).toBe(true); + expect(Decimal.isDecimal(item.price)).toBe(true); + expect(item.price.toString()).not.toEqual('[object Object]'); + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1506.test.ts b/tests/regression/test/v2-migrated/issue-1506.test.ts index 759ec279..2472bc32 100644 --- a/tests/regression/test/v2-migrated/issue-1506.test.ts +++ b/tests/regression/test/v2-migrated/issue-1506.test.ts @@ -1,9 +1,10 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1506', async () => { - await createPolicyTestClient( - ` +describe('Regression for issue #1506', () => { + it('verifies issue 1506', async () => { + await createPolicyTestClient( + ` model A { id Int @id @default(autoincrement()) value Int @@ -31,5 +32,6 @@ model C { @@allow('read', true) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1507.test.ts b/tests/regression/test/v2-migrated/issue-1507.test.ts index aee6fddd..ffcd3d5a 100644 --- a/tests/regression/test/v2-migrated/issue-1507.test.ts +++ b/tests/regression/test/v2-migrated/issue-1507.test.ts @@ -1,9 +1,10 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1507', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #1507', () => { + it('verifies issue 1507', async () => { + const db = await createPolicyTestClient( + ` model User { id Int @id @default(autoincrement()) age Int @@ -16,10 +17,11 @@ model Profile { @@allow('read', auth().age == age) } `, - ); + ); - await db.$unuseAll().profile.create({ data: { age: 18 } }); - await db.$unuseAll().profile.create({ data: { age: 20 } }); - await expect(db.$setAuth({ id: 1, age: 18 }).profile.findMany()).resolves.toHaveLength(1); - await expect(db.$setAuth({ id: 1, age: 18 }).profile.count()).resolves.toBe(1); + await db.$unuseAll().profile.create({ data: { age: 18 } }); + await db.$unuseAll().profile.create({ data: { age: 20 } }); + await expect(db.$setAuth({ id: 1, age: 18 }).profile.findMany()).resolves.toHaveLength(1); + await expect(db.$setAuth({ id: 1, age: 18 }).profile.count()).resolves.toBe(1); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1518.test.ts b/tests/regression/test/v2-migrated/issue-1518.test.ts index 1dfa436e..50e30e3b 100644 --- a/tests/regression/test/v2-migrated/issue-1518.test.ts +++ b/tests/regression/test/v2-migrated/issue-1518.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1518', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1518', () => { + it('verifies issue 1518', async () => { + const db = await createTestClient( + ` model Activity { id String @id @default(uuid()) title String @@ -18,13 +19,14 @@ model TaskActivity extends Activity { @@allow('all', true) } `, - ); + ); - await db.taskActivity.create({ - data: { - id: '00000000-0000-0000-0000-111111111111', - title: 'Test Activity', - description: 'Description of task', - }, + await db.taskActivity.create({ + data: { + id: '00000000-0000-0000-0000-111111111111', + title: 'Test Activity', + description: 'Description of task', + }, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1520.test.ts b/tests/regression/test/v2-migrated/issue-1520.test.ts index 79b8fc54..baef5464 100644 --- a/tests/regression/test/v2-migrated/issue-1520.test.ts +++ b/tests/regression/test/v2-migrated/issue-1520.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1520', async () => { - const db = await createTestClient( - ` +describe('Regression for issue #1520', () => { + it('verifies issue 1520', async () => { + const db = await createTestClient( + ` model Course { id Int @id @default(autoincrement()) title String @@ -25,43 +26,44 @@ model Notification { } model AddedToGroupNotification extends Notification { - groupId Int + groupId Int group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) } model AddedToCourseNotification extends Notification { - courseId Int + courseId Int course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) } `, - ); + ); - const r = await db.course.create({ - data: { - title: 'English classes', - addedToNotifications: { - createMany: { - data: [ - { - id: 1, - receiverId: 1, - senderId: 2, - }, - ], + const r = await db.course.create({ + data: { + title: 'English classes', + addedToNotifications: { + createMany: { + data: [ + { + id: 1, + receiverId: 1, + senderId: 2, + }, + ], + }, }, }, - }, - include: { addedToNotifications: true }, - }); + include: { addedToNotifications: true }, + }); - expect(r.addedToNotifications).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 1, - courseId: 1, - receiverId: 1, - senderId: 2, - }), - ]), - ); + expect(r.addedToNotifications).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + courseId: 1, + receiverId: 1, + senderId: 2, + }), + ]), + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1522.test.ts b/tests/regression/test/v2-migrated/issue-1522.test.ts index da18046a..7ae43c7f 100644 --- a/tests/regression/test/v2-migrated/issue-1522.test.ts +++ b/tests/regression/test/v2-migrated/issue-1522.test.ts @@ -1,90 +1,92 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1522', async () => { - const db = await createTestClient( - ` -model Course { - id String @id @default(uuid()) - title String - description String - sections Section[] - activities Activity[] - @@allow('all', true) -} +describe('Regression for issue #1522', () => { + it('verifies issue 1522', async () => { + const db = await createTestClient( + ` + 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 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 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 UrlActivity extends Activity { + url String + } -model TaskActivity extends Activity { - description String -} - `, - ); + model TaskActivity extends Activity { + description String + } + `, + ); - 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, + 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, - }, - }); + 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 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' } }, + 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', + expect(found.sections[0].activities[0]).toMatchObject({ + description: 'Description of task', + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1530.test.ts b/tests/regression/test/v2-migrated/issue-1530.test.ts index 55fb1089..d8877e84 100644 --- a/tests/regression/test/v2-migrated/issue-1530.test.ts +++ b/tests/regression/test/v2-migrated/issue-1530.test.ts @@ -1,34 +1,36 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1530', async () => { - const db = await createTestClient( - ` -model Category { - id Int @id @default(autoincrement()) - name String @unique +describe('Regression for issue #1530', () => { + it('verifies issue 1530', async () => { + const db = await createTestClient( + ` + model Category { + id Int @id @default(autoincrement()) + name String @unique - parentId Int? - parent Category? @relation("ParentChildren", fields: [parentId], references: [id]) - children Category[] @relation("ParentChildren") - @@allow('all', true) -} - `, - { usePrismaPush: true }, - ); + parentId Int? + parent Category? @relation("ParentChildren", fields: [parentId], references: [id]) + children Category[] @relation("ParentChildren") + @@allow('all', true) + } + `, + { usePrismaPush: true }, + ); - await db.$unuseAll().category.create({ - data: { id: 1, name: 'C1' }, - }); + await db.$unuseAll().category.create({ + data: { id: 1, name: 'C1' }, + }); - await db.category.update({ - where: { id: 1 }, - data: { parent: { connect: { id: 1 } } }, - }); + await db.category.update({ + where: { id: 1 }, + data: { parent: { connect: { id: 1 } } }, + }); - const r = await db.category.update({ - where: { id: 1 }, - data: { parent: { disconnect: true } }, + const r = await db.category.update({ + where: { id: 1 }, + data: { parent: { disconnect: true } }, + }); + expect(r.parent).toBeUndefined(); }); - expect(r.parent).toBeUndefined(); }); diff --git a/tests/regression/test/v2-migrated/issue-1551.test.ts b/tests/regression/test/v2-migrated/issue-1551.test.ts index 441eed43..beacb870 100644 --- a/tests/regression/test/v2-migrated/issue-1551.test.ts +++ b/tests/regression/test/v2-migrated/issue-1551.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1551', async () => { - await loadSchema( - ` +describe('Regression for issue #1551', () => { + it('verifies issue 1551', async () => { + await loadSchema( + ` model User { id Int @id profile Profile? @relation(fields: [profileId], references: [id]) @@ -19,8 +20,9 @@ model Profile { } model IndividualProfile extends Profile { - name String + name String } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1562.test.ts b/tests/regression/test/v2-migrated/issue-1562.test.ts index 98e3e98a..3a7c7c05 100644 --- a/tests/regression/test/v2-migrated/issue-1562.test.ts +++ b/tests/regression/test/v2-migrated/issue-1562.test.ts @@ -1,25 +1,27 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1562', async () => { - const db = await createTestClient( - ` -type Base { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() +describe('Regression for issue #1562', () => { + it('verifies issue 1562', async () => { + const db = await createTestClient( + ` + type Base { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt() - // require login - @@allow('all', true) -} + // require login + @@allow('all', true) + } -model User with Base { - name String @unique @regex('^[a-zA-Z0-9_]{3,30}$') + model User with Base { + name String @unique @regex('^[a-zA-Z0-9_]{3,30}$') - @@allow('read', true) -} - `, - ); + @@allow('read', true) + } + `, + ); - await expect(db.user.create({ data: { name: '1 2 3 4' } })).toBeRejectedByValidation(); + await expect(db.user.create({ data: { name: '1 2 3 4' } })).toBeRejectedByValidation(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1563.test.ts b/tests/regression/test/v2-migrated/issue-1563.test.ts index 5b7c1b28..871927b3 100644 --- a/tests/regression/test/v2-migrated/issue-1563.test.ts +++ b/tests/regression/test/v2-migrated/issue-1563.test.ts @@ -1,26 +1,28 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1563', async () => { - const db = await createTestClient( - ` -model ModelA { - id String @id @default(cuid()) - ref ModelB[] -} +describe('Regression for issue #1563', () => { + it('verifies issue 1563', async () => { + const db = await createTestClient( + ` + model ModelA { + id String @id @default(cuid()) + ref ModelB[] + } -model ModelB { - id String @id @default(cuid()) - ref ModelA? @relation(fields: [refId], references: [id]) - refId String? + model ModelB { + id String @id @default(cuid()) + ref ModelA? @relation(fields: [refId], references: [id]) + refId String? - @@validate(refId != null, "refId must be set") -} - `, - ); + @@validate(refId != null, "refId must be set") + } + `, + ); - const a = await db.modelA.create({ data: {} }); - const b = await db.modelB.create({ data: { refId: a.id } }); + const a = await db.modelA.create({ data: {} }); + const b = await db.modelB.create({ data: { refId: a.id } }); - await expect(db.modelB.update({ where: { id: b.id }, data: { refId: a.id } })).toResolveTruthy(); + await expect(db.modelB.update({ where: { id: b.id }, data: { refId: a.id } })).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1575.test.ts b/tests/regression/test/v2-migrated/issue-1575.test.ts index c09ac5aa..3eb8fa1c 100644 --- a/tests/regression/test/v2-migrated/issue-1575.test.ts +++ b/tests/regression/test/v2-migrated/issue-1575.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1575', async () => { - await loadSchema( - ` +describe('Regression for issue #1575', () => { + it('verifies issue 1575', async () => { + await loadSchema( + ` model UserAssets { id String @id @default(cuid()) videoId String @@ -25,5 +26,6 @@ model Movie extends Asset { duration Int } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1576.test.ts b/tests/regression/test/v2-migrated/issue-1576.test.ts index 078fd7cc..91870b3e 100644 --- a/tests/regression/test/v2-migrated/issue-1576.test.ts +++ b/tests/regression/test/v2-migrated/issue-1576.test.ts @@ -1,61 +1,63 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1576', async () => { - const db = await createTestClient( - ` -model Profile { - id Int @id @default(autoincrement()) - name String - items Item[] - type String - @@delegate(type) - @@allow('all', true) -} +describe('Regression for issue #1576', () => { + it('verifies issue 1576', async () => { + const db = await createTestClient( + ` + model Profile { + id Int @id @default(autoincrement()) + name String + items Item[] + type String + @@delegate(type) + @@allow('all', true) + } -model GoldProfile extends Profile { - ticket Int -} + model GoldProfile extends Profile { + ticket Int + } -model Item { - id Int @id @default(autoincrement()) - profileId Int - profile Profile @relation(fields: [profileId], references: [id]) - type String - @@delegate(type) - @@allow('all', true) -} + model Item { + id Int @id @default(autoincrement()) + profileId Int + profile Profile @relation(fields: [profileId], references: [id]) + type String + @@delegate(type) + @@allow('all', true) + } -model GoldItem extends Item { - inventory Boolean -} - `, - ); + model GoldItem extends Item { + inventory Boolean + } + `, + ); - const profile = await db.goldProfile.create({ - data: { - name: 'hello', - ticket: 5, - }, - }); + const profile = await db.goldProfile.create({ + data: { + name: 'hello', + ticket: 5, + }, + }); - await expect( - db.goldItem.createManyAndReturn({ - data: [ - { - profileId: profile.id, - inventory: true, - }, - { - profileId: profile.id, - inventory: true, - }, - ], - }), - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), - expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), - ]), - ); + await expect( + db.goldItem.createManyAndReturn({ + data: [ + { + profileId: profile.id, + inventory: true, + }, + { + profileId: profile.id, + inventory: true, + }, + ], + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + ]), + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1585.test.ts b/tests/regression/test/v2-migrated/issue-1585.test.ts index 388e0cde..aa9d3484 100644 --- a/tests/regression/test/v2-migrated/issue-1585.test.ts +++ b/tests/regression/test/v2-migrated/issue-1585.test.ts @@ -1,29 +1,31 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1585', async () => { - const db = await createTestClient( - ` - model Asset { - id Int @id @default(autoincrement()) - type String - views Int +describe('Regression for issue #1585', () => { + it('verifies issue 1585', async () => { + const db = await createTestClient( + ` + model Asset { + id Int @id @default(autoincrement()) + type String + views Int - @@allow('all', true) - @@delegate(type) - } + @@allow('all', true) + @@delegate(type) + } - model Post extends Asset { - title String - } - `, - ); + model Post extends Asset { + title String + } + `, + ); - await db.post.create({ data: { title: 'Post1', views: 0 } }); - await db.post.create({ data: { title: 'Post2', views: 1 } }); - await expect( - db.post.count({ - where: { views: { gt: 0 } }, - }), - ).resolves.toBe(1); + await db.post.create({ data: { title: 'Post1', views: 0 } }); + await db.post.create({ data: { title: 'Post2', views: 1 } }); + await expect( + db.post.count({ + where: { views: { gt: 0 } }, + }), + ).resolves.toBe(1); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1627.test.ts b/tests/regression/test/v2-migrated/issue-1627.test.ts index a086b42f..5337ef57 100644 --- a/tests/regression/test/v2-migrated/issue-1627.test.ts +++ b/tests/regression/test/v2-migrated/issue-1627.test.ts @@ -1,49 +1,51 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 1627', async () => { - const db = await createPolicyTestClient( - ` -model User { - id String @id - memberships GymUser[] -} - -model Gym { - id String @id - members GymUser[] - - @@allow('all', true) -} - -model GymUser { - id String @id - userID String - user User @relation(fields: [userID], references: [id]) - gymID String? - gym Gym? @relation(fields: [gymID], references: [id]) - role String - - @@allow('read',gym.members?[user == auth() && (role == "ADMIN" || role == "TRAINER")]) - @@unique([userID, gymID]) -} - `, - ); - - await db.$unuseAll().user.create({ data: { id: '1' } }); - - await db.$unuseAll().gym.create({ - data: { - id: '1', - members: { - create: { - id: '1', - user: { connect: { id: '1' } }, - role: 'ADMIN', +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #1627', () => { + it('verifies issue 1627', async () => { + const db = await createPolicyTestClient( + ` + model User { + id String @id + memberships GymUser[] + } + + model Gym { + id String @id + members GymUser[] + + @@allow('all', true) + } + + model GymUser { + id String @id + userID String + user User @relation(fields: [userID], references: [id]) + gymID String? + gym Gym? @relation(fields: [gymID], references: [id]) + role String + + @@allow('read',gym.members?[user == auth() && (role == "ADMIN" || role == "TRAINER")]) + @@unique([userID, gymID]) + } + `, + ); + + await db.$unuseAll().user.create({ data: { id: '1' } }); + + await db.$unuseAll().gym.create({ + data: { + id: '1', + members: { + create: { + id: '1', + user: { connect: { id: '1' } }, + role: 'ADMIN', + }, }, }, - }, - }); + }); - await expect(db.gymUser.findMany()).resolves.toHaveLength(0); + await expect(db.gymUser.findMany()).resolves.toHaveLength(0); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1642.test.ts b/tests/regression/test/v2-migrated/issue-1642.test.ts index 3fc9b542..d0404af3 100644 --- a/tests/regression/test/v2-migrated/issue-1642.test.ts +++ b/tests/regression/test/v2-migrated/issue-1642.test.ts @@ -1,40 +1,42 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1642', async () => { - const db = await createPolicyTestClient( - ` -model User { - id Int @id - name String - posts Post[] +describe('Regression for issue #1642', () => { + it('verifies issue 1642', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + name String + posts Post[] - @@allow('read', true) - @@allow('all', auth().id == 1) -} + @@allow('read', true) + @@allow('all', auth().id == 1) + } -model Post { - id Int @id - title String - description String - author User @relation(fields: [authorId], references: [id]) - authorId Int + model Post { + id Int @id + title String + description String + author User @relation(fields: [authorId], references: [id]) + authorId Int - // delegate all access policies to the author: - @@allow('all', check(author)) - @@allow('update', true) - @@allow('post-update', title == 'hello') -} - `, - ); + // delegate all access policies to the author: + @@allow('all', check(author)) + @@allow('update', true) + @@allow('post-update', title == 'hello') + } + `, + ); - await db.$unuseAll().user.create({ data: { id: 1, name: 'User1' } }); - await db.$unuseAll().post.create({ data: { id: 1, title: 'hello', description: 'desc1', authorId: 1 } }); + await db.$unuseAll().user.create({ data: { id: 1, name: 'User1' } }); + await db.$unuseAll().post.create({ data: { id: 1, title: 'hello', description: 'desc1', authorId: 1 } }); - const authDb = db.$setAuth({ id: 2 }); - await expect( - authDb.post.update({ where: { id: 1 }, data: { title: 'world', description: 'desc2' } }), - ).toBeRejectedByPolicy(); + const authDb = db.$setAuth({ id: 2 }); + await expect( + authDb.post.update({ where: { id: 1 }, data: { title: 'world', description: 'desc2' } }), + ).toBeRejectedByPolicy(); - await expect(authDb.post.update({ where: { id: 1 }, data: { description: 'desc2' } })).toResolveTruthy(); + await expect(authDb.post.update({ where: { id: 1 }, data: { description: 'desc2' } })).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1645.test.ts b/tests/regression/test/v2-migrated/issue-1645.test.ts index a04fca7d..8db966d4 100644 --- a/tests/regression/test/v2-migrated/issue-1645.test.ts +++ b/tests/regression/test/v2-migrated/issue-1645.test.ts @@ -1,201 +1,203 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 1645', async () => { - const db = await createPolicyTestClient( - ` -model Product { - id String @id @default(cuid()) - name String - slug String - description String? - sku String - price Int - onSale Boolean @default(false) - salePrice Int @default(0) - saleStartDateTime DateTime? - saleEndDateTime DateTime? - scheduledAvailability Boolean @default(false) - availabilityStartDateTime DateTime? - availabilityEndDateTime DateTime? - type String @default('VARIABLE') - image String - orderItems OrderItem[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([slug]) - - @@allow('all', true) -} - -model BaseOrder { - id String @id @default(cuid()) - orderNumber String @unique @default(nanoid(16)) - lineItems OrderItem[] - status String @default('PENDING') - type String @default('PARENT') - userType String? - billingAddress BillingAddress @relation(fields: [billingAddressId], references: [id]) - billingAddressId String @map("billing_address_id") - shippingAddress ShippingAddress @relation(fields: [shippingAddressId], references: [id]) - shippingAddressId String @map("shipping_address_id") - notes String? @default('') - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@allow('all', true) - @@delegate(userType) -} - -model Order extends BaseOrder { - parentId String? @map("parent_id") - parent Order? @relation("OrderToParent", fields: [parentId], references: [id]) - groupedOrders Order[] @relation("OrderToParent") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String @map("user_id") -} - -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - orders Order[] - billingAddresses BillingAddress[] - shippingAddresses ShippingAddress[] - - @@allow('create,read', true) - @@allow('update,delete', auth().id == this.id) -} - -model GuestUser { - id String @id @default(cuid()) - name String? - email String @unique - orders GuestOrder[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@auth - @@allow('all', true) -} - -model GuestOrder extends BaseOrder { - guestUser GuestUser @relation(fields: [guestUserId], references: [id], onDelete: Cascade) - guestUserId String @map("guest_user_id") - parentId String? @map("parent_id") - parent GuestOrder? @relation("OrderToParent", fields: [parentId], references: [id]) - groupedOrders GuestOrder[] @relation("OrderToParent") -} - -model OrderItem { - id String @id @default(cuid()) - order BaseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade) - orderId String @map("order_id") - product Product @relation(fields: [productId], references: [id]) - productId String @map("product_id") - quantity Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@allow('all', true) -} - -model OrderAddress { - id String @id @default(cuid()) - firstName String - lastName String - address1 String - address2 String? - city String - state String - postalCode String - country String - email String - phone String - type String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@allow('all', true) - @@delegate(type) -} - -model BillingAddress extends OrderAddress { - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - order BaseOrder[] -} - -model ShippingAddress extends OrderAddress { - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - order BaseOrder[] -} - `, - { usePrismaPush: true }, - ); - - await db.user.create({ data: { id: '1', name: 'John', email: 'john@example.com' } }); - - const shipping = await db.shippingAddress.create({ - data: { - id: '1', - firstName: 'John', - lastName: 'Doe', - address1: '123 Main St', - city: 'Anytown', - state: 'CA', - postalCode: '12345', - country: 'US', - email: 'john@example.com', - phone: '123-456-7890', - user: { connect: { id: '1' } }, - }, +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #1645', () => { + it('verifies issue 1645', async () => { + const db = await createPolicyTestClient( + ` + model Product { + id String @id @default(cuid()) + name String + slug String + description String? + sku String + price Int + onSale Boolean @default(false) + salePrice Int @default(0) + saleStartDateTime DateTime? + saleEndDateTime DateTime? + scheduledAvailability Boolean @default(false) + availabilityStartDateTime DateTime? + availabilityEndDateTime DateTime? + type String @default('VARIABLE') + image String + orderItems OrderItem[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([slug]) + + @@allow('all', true) + } + + model BaseOrder { + id String @id @default(cuid()) + orderNumber String @unique @default(nanoid(16)) + lineItems OrderItem[] + status String @default('PENDING') + type String @default('PARENT') + userType String? + billingAddress BillingAddress @relation(fields: [billingAddressId], references: [id]) + billingAddressId String @map("billing_address_id") + shippingAddress ShippingAddress @relation(fields: [shippingAddressId], references: [id]) + shippingAddressId String @map("shipping_address_id") + notes String? @default('') + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@allow('all', true) + @@delegate(userType) + } + + model Order extends BaseOrder { + parentId String? @map("parent_id") + parent Order? @relation("OrderToParent", fields: [parentId], references: [id]) + groupedOrders Order[] @relation("OrderToParent") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @map("user_id") + } + + model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + orders Order[] + billingAddresses BillingAddress[] + shippingAddresses ShippingAddress[] + + @@allow('create,read', true) + @@allow('update,delete', auth().id == this.id) + } + + model GuestUser { + id String @id @default(cuid()) + name String? + email String @unique + orders GuestOrder[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@auth + @@allow('all', true) + } + + model GuestOrder extends BaseOrder { + guestUser GuestUser @relation(fields: [guestUserId], references: [id], onDelete: Cascade) + guestUserId String @map("guest_user_id") + parentId String? @map("parent_id") + parent GuestOrder? @relation("OrderToParent", fields: [parentId], references: [id]) + groupedOrders GuestOrder[] @relation("OrderToParent") + } + + model OrderItem { + id String @id @default(cuid()) + order BaseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade) + orderId String @map("order_id") + product Product @relation(fields: [productId], references: [id]) + productId String @map("product_id") + quantity Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@allow('all', true) + } + + model OrderAddress { + id String @id @default(cuid()) + firstName String + lastName String + address1 String + address2 String? + city String + state String + postalCode String + country String + email String + phone String + type String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@allow('all', true) + @@delegate(type) + } + + model BillingAddress extends OrderAddress { + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @map("user_id") + order BaseOrder[] + } + + model ShippingAddress extends OrderAddress { + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @map("user_id") + order BaseOrder[] + } + `, + { usePrismaPush: true }, + ); + + await db.user.create({ data: { id: '1', name: 'John', email: 'john@example.com' } }); + + const shipping = await db.shippingAddress.create({ + data: { + id: '1', + firstName: 'John', + lastName: 'Doe', + address1: '123 Main St', + city: 'Anytown', + state: 'CA', + postalCode: '12345', + country: 'US', + email: 'john@example.com', + phone: '123-456-7890', + user: { connect: { id: '1' } }, + }, + }); + + const billing = await db.billingAddress.create({ + data: { + id: '2', + firstName: 'John', + lastName: 'Doe', + address1: '123 Main St', + city: 'Anytown', + state: 'CA', + postalCode: '12345', + country: 'US', + email: 'john@example.com', + phone: '123-456-7890', + user: { connect: { id: '1' } }, + }, + }); + + await db.order.create({ + data: { + id: '1', + orderNumber: '1', + status: 'PENDING', + type: 'PARENT', + shippingAddress: { connect: { id: '1' } }, + billingAddress: { connect: { id: '2' } }, + user: { connect: { id: '1' } }, + }, + }); + + const updated = await db.order.update({ + where: { id: '1' }, + include: { + lineItems: true, + billingAddress: true, + shippingAddress: true, + }, + data: { + type: 'CAMPAIGN', + }, + }); + + expect(updated.shippingAddress).toEqual(shipping); + expect(updated.billingAddress).toEqual(billing); }); - - const billing = await db.billingAddress.create({ - data: { - id: '2', - firstName: 'John', - lastName: 'Doe', - address1: '123 Main St', - city: 'Anytown', - state: 'CA', - postalCode: '12345', - country: 'US', - email: 'john@example.com', - phone: '123-456-7890', - user: { connect: { id: '1' } }, - }, - }); - - await db.order.create({ - data: { - id: '1', - orderNumber: '1', - status: 'PENDING', - type: 'PARENT', - shippingAddress: { connect: { id: '1' } }, - billingAddress: { connect: { id: '2' } }, - user: { connect: { id: '1' } }, - }, - }); - - const updated = await db.order.update({ - where: { id: '1' }, - include: { - lineItems: true, - billingAddress: true, - shippingAddress: true, - }, - data: { - type: 'CAMPAIGN', - }, - }); - - expect(updated.shippingAddress).toEqual(shipping); - expect(updated.billingAddress).toEqual(billing); }); diff --git a/tests/regression/test/v2-migrated/issue-1648.test.ts b/tests/regression/test/v2-migrated/issue-1648.test.ts index 7fe06686..f9e0f3c9 100644 --- a/tests/regression/test/v2-migrated/issue-1648.test.ts +++ b/tests/regression/test/v2-migrated/issue-1648.test.ts @@ -1,42 +1,44 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 1648', async () => { - const db = await createPolicyTestClient( - ` -model User { - id Int @id @default(autoincrement()) - profile Profile? - posts Post[] -} - -model Profile { - id Int @id @default(autoincrement()) - someText String - user User @relation(fields: [userId], references: [id]) - userId Int @unique -} - -model Post { - id Int @id @default(autoincrement()) - title String - - userId Int - user User @relation(fields: [userId], references: [id]) - - // this will always be true, even if the someText field is "canUpdate" - @@deny("post-update", user.profile.someText != "canUpdate") - - @@allow("all", true) -} - `, - ); - - await db.$unuseAll().user.create({ data: { id: 1, profile: { create: { someText: 'canUpdate' } } } }); - await db.$unuseAll().user.create({ data: { id: 2, profile: { create: { someText: 'nothing' } } } }); - await db.$unuseAll().post.create({ data: { id: 1, title: 'Post1', userId: 1 } }); - await db.$unuseAll().post.create({ data: { id: 2, title: 'Post2', userId: 2 } }); - - await expect(db.post.update({ where: { id: 1 }, data: { title: 'Post1-1' } })).toResolveTruthy(); - await expect(db.post.update({ where: { id: 2 }, data: { title: 'Post2-2' } })).toBeRejectedByPolicy(); +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #1648', () => { + it('verifies issue 1648', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id @default(autoincrement()) + profile Profile? + posts Post[] + } + + model Profile { + id Int @id @default(autoincrement()) + someText String + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } + + model Post { + id Int @id @default(autoincrement()) + title String + + userId Int + user User @relation(fields: [userId], references: [id]) + + // this will always be true, even if the someText field is "canUpdate" + @@deny("post-update", user.profile.someText != "canUpdate") + + @@allow("all", true) + } + `, + ); + + await db.$unuseAll().user.create({ data: { id: 1, profile: { create: { someText: 'canUpdate' } } } }); + await db.$unuseAll().user.create({ data: { id: 2, profile: { create: { someText: 'nothing' } } } }); + await db.$unuseAll().post.create({ data: { id: 1, title: 'Post1', userId: 1 } }); + await db.$unuseAll().post.create({ data: { id: 2, title: 'Post2', userId: 2 } }); + + await expect(db.post.update({ where: { id: 1 }, data: { title: 'Post1-1' } })).toResolveTruthy(); + await expect(db.post.update({ where: { id: 2 }, data: { title: 'Post2-2' } })).toBeRejectedByPolicy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1674.test.ts b/tests/regression/test/v2-migrated/issue-1674.test.ts index 759f7d82..88c91625 100644 --- a/tests/regression/test/v2-migrated/issue-1674.test.ts +++ b/tests/regression/test/v2-migrated/issue-1674.test.ts @@ -1,80 +1,82 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1674', async () => { - const db = await createPolicyTestClient( - ` -model User { - id String @id @default(cuid()) - email String @unique @email @length(6, 32) - posts Post[] +describe('Regression for issue #1674', () => { + it('verifies issue 1674', async () => { + const db = await createPolicyTestClient( + ` + model User { + id String @id @default(cuid()) + email String @unique @email @length(6, 32) + posts Post[] - // everybody can signup - @@allow('create', true) + // everybody can signup + @@allow('create', true) - // full access by self - @@allow('all', auth() == this) -} + // full access by self + @@allow('all', auth() == this) + } -model Blog { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + model Blog { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) - postId String? -} + post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) + postId String? + } -model Post { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(1, 256) - content String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String + model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String @length(1, 256) + content String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String - blogs Blog[] + blogs Blog[] - type String + type String - @@delegate(type) -} + @@delegate(type) + } -model PostA extends Post { -} + model PostA extends Post { + } -model PostB extends Post { -} - `, - ); + model PostB extends Post { + } + `, + ); - const user = await db.$unuseAll().user.create({ - data: { email: 'abc@def.com' }, - }); + const user = await db.$unuseAll().user.create({ + data: { email: 'abc@def.com' }, + }); - const blog = await db.$unuseAll().blog.create({ - data: {}, - }); + const blog = await db.$unuseAll().blog.create({ + data: {}, + }); - const authDb = db.$setAuth(user); - await expect( - authDb.postA.create({ - data: { - content: 'content', - title: 'title', - blogs: { - connect: { - id: blog.id, + const authDb = db.$setAuth(user); + await expect( + authDb.postA.create({ + data: { + content: 'content', + title: 'title', + blogs: { + connect: { + id: blog.id, + }, }, - }, - author: { - connect: { - id: user.id, + author: { + connect: { + id: user.id, + }, }, }, - }, - }), - ).toBeRejectedByPolicy(); + }), + ).toBeRejectedByPolicy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1681.test.ts b/tests/regression/test/v2-migrated/issue-1681.test.ts index 5fb20312..0483a1c3 100644 --- a/tests/regression/test/v2-migrated/issue-1681.test.ts +++ b/tests/regression/test/v2-migrated/issue-1681.test.ts @@ -1,29 +1,31 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1681', async () => { - const db = await createTestClient( - ` -model User { - id Int @id @default(autoincrement()) - posts Post[] - @@allow('all', true) -} +describe('Regression for issue #1681', () => { + it('verifies issue 1681', async () => { + const db = await createTestClient( + ` + model User { + id Int @id @default(autoincrement()) + posts Post[] + @@allow('all', true) + } -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - @@allow('all', true) -} - `, - ); + model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int @default(auth().id) + @@allow('all', true) + } + `, + ); - const authDb = db.$setAuth({ id: 1 }); - const user = await db.user.create({ data: {} }); - await expect(authDb.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 }); + const authDb = db.$setAuth({ id: 1 }); + const user = await db.user.create({ data: {} }); + await expect(authDb.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 }); - const r = await authDb.post.createManyAndReturn({ data: [{ title: 'Post2' }] }); - expect(r[0].authorId).toBe(user.id); + const r = await authDb.post.createManyAndReturn({ data: [{ title: 'Post2' }] }); + expect(r[0].authorId).toBe(user.id); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1693.test.ts b/tests/regression/test/v2-migrated/issue-1693.test.ts index 04c4075b..6d0af298 100644 --- a/tests/regression/test/v2-migrated/issue-1693.test.ts +++ b/tests/regression/test/v2-migrated/issue-1693.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1693', async () => { - await loadSchema( - ` +describe('Regression for issue #1693', () => { + it('verifies issue 1693', async () => { + await loadSchema( + ` model Animal { id String @id @default(uuid()) animalType String @default("") @@ -14,5 +15,6 @@ model Dog extends Animal { name String } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1695.test.ts b/tests/regression/test/v2-migrated/issue-1695.test.ts index a713f1af..a297f726 100644 --- a/tests/regression/test/v2-migrated/issue-1695.test.ts +++ b/tests/regression/test/v2-migrated/issue-1695.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1695', async () => { - await loadSchema( - ` +describe('Regression for issue #1695', () => { + it('verifies issue 1695', async () => { + await loadSchema( + ` type SoftDelete { deleted Int @default(0) } @@ -17,5 +18,6 @@ model MyModel with SoftDelete { @@deny('read', this.deleted != 0) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1698.test.ts b/tests/regression/test/v2-migrated/issue-1698.test.ts index c93dfa03..3df2e823 100644 --- a/tests/regression/test/v2-migrated/issue-1698.test.ts +++ b/tests/regression/test/v2-migrated/issue-1698.test.ts @@ -1,72 +1,74 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1698', async () => { - const db = await createTestClient( - ` -model House { - id Int @id @default(autoincrement()) - doorTypeId Int - door Door @relation(fields: [doorTypeId], references: [id]) - houseType String - @@delegate(houseType) -} +describe('Regression for issue #1698', () => { + it('verifies issue 1698', async () => { + const db = await createTestClient( + ` + model House { + id Int @id @default(autoincrement()) + doorTypeId Int + door Door @relation(fields: [doorTypeId], references: [id]) + houseType String + @@delegate(houseType) + } -model PrivateHouse extends House { - size Int -} + model PrivateHouse extends House { + size Int + } -model Skyscraper extends House { - height Int -} + model Skyscraper extends House { + height Int + } -model Door { - id Int @id @default(autoincrement()) - color String - doorType String - houses House[] - @@delegate(doorType) -} + model Door { + id Int @id @default(autoincrement()) + color String + doorType String + houses House[] + @@delegate(doorType) + } -model IronDoor extends Door { - strength Int -} + model IronDoor extends Door { + strength Int + } -model WoodenDoor extends Door { - texture String -} - `, - ); + model WoodenDoor extends Door { + texture String + } + `, + ); - const door1 = await db.ironDoor.create({ - data: { strength: 100, color: 'blue' }, - }); - console.log(door1); + const door1 = await db.ironDoor.create({ + data: { strength: 100, color: 'blue' }, + }); + console.log(door1); - const door2 = await db.woodenDoor.create({ - data: { texture: 'pine', color: 'red' }, - }); - console.log(door2); + const door2 = await db.woodenDoor.create({ + data: { texture: 'pine', color: 'red' }, + }); + console.log(door2); - const house1 = await db.privateHouse.create({ - data: { size: 5000, door: { connect: { id: door1.id } } }, - }); - console.log(house1); + const house1 = await db.privateHouse.create({ + data: { size: 5000, door: { connect: { id: door1.id } } }, + }); + console.log(house1); - const house2 = await db.skyscraper.create({ - data: { height: 3000, door: { connect: { id: door2.id } } }, - }); - console.log(house2); + const house2 = await db.skyscraper.create({ + data: { height: 3000, door: { connect: { id: door2.id } } }, + }); + console.log(house2); - const r1 = await db.privateHouse.findFirst({ include: { door: true } }); - console.log(r1); - expect(r1).toMatchObject({ - door: { color: 'blue', strength: 100 }, - }); + const r1 = await db.privateHouse.findFirst({ include: { door: true } }); + console.log(r1); + expect(r1).toMatchObject({ + door: { color: 'blue', strength: 100 }, + }); - const r2 = (await db.skyscraper.findMany({ include: { door: true } }))[0]; - console.log(r2); - expect(r2).toMatchObject({ - door: { color: 'red', texture: 'pine' }, + const r2 = (await db.skyscraper.findMany({ include: { door: true } }))[0]; + console.log(r2); + expect(r2).toMatchObject({ + door: { color: 'red', texture: 'pine' }, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1745.test.ts b/tests/regression/test/v2-migrated/issue-1745.test.ts index 9140ad84..1dc3b91d 100644 --- a/tests/regression/test/v2-migrated/issue-1745.test.ts +++ b/tests/regression/test/v2-migrated/issue-1745.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1745', async () => { - await loadSchema( - ` +describe('Regression for issue #1745', () => { + it('verifies issue 1745', async () => { + await loadSchema( + ` datasource db { provider = 'postgresql' url = env('DATABASE_URL') @@ -90,5 +91,6 @@ model User with Base { @@allow('all', auth().isAdmin) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1755.test.ts b/tests/regression/test/v2-migrated/issue-1755.test.ts index 47ac92a1..5615e0c3 100644 --- a/tests/regression/test/v2-migrated/issue-1755.test.ts +++ b/tests/regression/test/v2-migrated/issue-1755.test.ts @@ -1,58 +1,60 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 1755', async () => { - const db = await createTestClient( - ` -model User { - id Int @id @default(autoincrement()) - contents Content[] -} - -model Content { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) - userId Int - contentType String - @@delegate(contentType) -} - -model Post extends Content { - title String -} - -model Video extends Content { - name String - duration Int -} - `, - ); - - const user = await db.user.create({ data: {} }); - const now = Date.now(); - await db.post.create({ - data: { title: 'post1', createdAt: new Date(now - 1000), user: { connect: { id: user.id } } }, - }); - await db.post.create({ - data: { title: 'post2', createdAt: new Date(now), user: { connect: { id: user.id } } }, - }); - - // scalar orderBy - await expect(db.post.findFirst({ orderBy: { createdAt: 'desc' } })).resolves.toMatchObject({ - title: 'post2', - }); - - // array orderBy - await expect(db.post.findFirst({ orderBy: [{ createdAt: 'desc' }] })).resolves.toMatchObject({ - title: 'post2', - }); - - // nested orderBy - await expect( - db.user.findFirst({ include: { contents: { orderBy: [{ createdAt: 'desc' }] } } }), - ).resolves.toMatchObject({ - id: user.id, - contents: [{ title: 'post2' }, { title: 'post1' }], +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #1755', () => { + it('verifies issue 1755', async () => { + const db = await createTestClient( + ` + model User { + id Int @id @default(autoincrement()) + contents Content[] + } + + model Content { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id]) + userId Int + contentType String + @@delegate(contentType) + } + + model Post extends Content { + title String + } + + model Video extends Content { + name String + duration Int + } + `, + ); + + const user = await db.user.create({ data: {} }); + const now = Date.now(); + await db.post.create({ + data: { title: 'post1', createdAt: new Date(now - 1000), user: { connect: { id: user.id } } }, + }); + await db.post.create({ + data: { title: 'post2', createdAt: new Date(now), user: { connect: { id: user.id } } }, + }); + + // scalar orderBy + await expect(db.post.findFirst({ orderBy: { createdAt: 'desc' } })).resolves.toMatchObject({ + title: 'post2', + }); + + // array orderBy + await expect(db.post.findFirst({ orderBy: [{ createdAt: 'desc' }] })).resolves.toMatchObject({ + title: 'post2', + }); + + // nested orderBy + await expect( + db.user.findFirst({ include: { contents: { orderBy: [{ createdAt: 'desc' }] } } }), + ).resolves.toMatchObject({ + id: user.id, + contents: [{ title: 'post2' }, { title: 'post1' }], + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1758.test.ts b/tests/regression/test/v2-migrated/issue-1758.test.ts index d62282e6..3a32386f 100644 --- a/tests/regression/test/v2-migrated/issue-1758.test.ts +++ b/tests/regression/test/v2-migrated/issue-1758.test.ts @@ -1,9 +1,10 @@ import { loadSchemaWithError } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1758', async () => { - await loadSchemaWithError( - ` +describe('Regression for issue #1758', () => { + it('verifies issue 1758', async () => { + await loadSchemaWithError( + ` model Organization { id String @id @default(cuid()) contents Content[] @relation("OrganizationContents") @@ -22,6 +23,7 @@ model Store extends Content { @@unique([organizationId, name]) } `, - 'Cannot use fields inherited from a polymorphic base model in `@@unique`', - ); + 'Cannot use fields inherited from a polymorphic base model in `@@unique`', + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1763.test.ts b/tests/regression/test/v2-migrated/issue-1763.test.ts index ec3d5748..ec571787 100644 --- a/tests/regression/test/v2-migrated/issue-1763.test.ts +++ b/tests/regression/test/v2-migrated/issue-1763.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1763', async () => { - await createTestClient( - ` +describe('Regression for issue #1763', () => { + it('verifies issue 1763', async () => { + await createTestClient( + ` model Post { id Int @id @default(autoincrement()) name String @@ -20,9 +21,9 @@ model ConcretePost extends Post { } `, - { - extraSourceFiles: { - main: ` + { + extraSourceFiles: { + main: ` import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './schema'; @@ -36,7 +37,8 @@ async function test() { }, }); }`, + }, }, - }, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-177.test.ts b/tests/regression/test/v2-migrated/issue-177.test.ts index 4c8558fb..146cd776 100644 --- a/tests/regression/test/v2-migrated/issue-177.test.ts +++ b/tests/regression/test/v2-migrated/issue-177.test.ts @@ -1,25 +1,27 @@ import { loadSchemaWithError } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 177', async () => { - await loadSchemaWithError( - ` +describe('Regression for issue 177', () => { + it('verifies issue 177', async () => { + await loadSchemaWithError( + ` model Foo { id String @id @default(cuid()) - + bar Bar @relation(fields: [barId1, barId2], references: [id1, id2]) barId1 String? barId2 String } - + model Bar { id1 String @default(cuid()) id2 String @default(cuid()) foos Foo[] @@id([id1, id2]) - } + } `, - 'relation "bar" is not optional, but field "barId1" is optional', - ); + 'relation "bar" is not optional, but field "barId1" is optional', + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1786.test.ts b/tests/regression/test/v2-migrated/issue-1786.test.ts index f67a98a7..fd373573 100644 --- a/tests/regression/test/v2-migrated/issue-1786.test.ts +++ b/tests/regression/test/v2-migrated/issue-1786.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1786', async () => { - await loadSchema( - ` +describe('Regression for issue #1786', () => { + it('verifies issue 1786', async () => { + await loadSchema( + ` model User { id String @id @default(cuid()) email String @unique @email @length(6, 32) @@ -40,7 +41,8 @@ it('verifies issue 1786', async () => { model Video extends Content { name String duration Int - } + } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1835.test.ts b/tests/regression/test/v2-migrated/issue-1835.test.ts index 2cc6f088..01af7721 100644 --- a/tests/regression/test/v2-migrated/issue-1835.test.ts +++ b/tests/regression/test/v2-migrated/issue-1835.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1835', async () => { - await loadSchema( - ` +describe('Regression for issue #1835', () => { + it('verifies issue 1835', async () => { + await loadSchema( + ` enum Enum { SOME_VALUE ANOTHER_VALUE @@ -19,5 +20,6 @@ model AnotherModel { id String @id @default(cuid()) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1849.test.ts b/tests/regression/test/v2-migrated/issue-1849.test.ts index cf99d6f3..9dc4c2b3 100644 --- a/tests/regression/test/v2-migrated/issue-1849.test.ts +++ b/tests/regression/test/v2-migrated/issue-1849.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1849', async () => { - await loadSchema( - ` +describe('Regression for issue #1849', () => { + it('verifies issue 1849', async () => { + await loadSchema( + ` import './enum' datasource db { @@ -15,13 +16,14 @@ model Post { id Int @id status Status @default(PUBLISHED) }`, - { - enum: ` -enum Status { + { + enum: ` +enum Status { PENDING PUBLISHED -} +} `, - }, - ); + }, + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1857.test.ts b/tests/regression/test/v2-migrated/issue-1857.test.ts index 2df875b2..99c9a862 100644 --- a/tests/regression/test/v2-migrated/issue-1857.test.ts +++ b/tests/regression/test/v2-migrated/issue-1857.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1857', async () => { - await createTestClient( - ` +describe('Regression for issue #1857', () => { + it('verifies issue 1857', async () => { + await createTestClient( + ` type JSONContent { type String text String? @@ -15,12 +16,12 @@ it('verifies issue 1857', async () => { @@allow('all', true) } `, - { - extraSourceFiles: { - main: ` + { + extraSourceFiles: { + main: ` import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './schema'; - + async function main() { const db = new ZenStackClient(schema, {} as any); await db.post.create({ @@ -30,10 +31,11 @@ it('verifies issue 1857', async () => { }); } `, + }, }, - }, - ); + ); - // TODO: zod schema support - // zodSchemas.models.JSONContentSchema.parse({ type: 'foo', text: null }); + // TODO: zod schema support + // zodSchemas.models.JSONContentSchema.parse({ type: 'foo', text: null }); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1870.test.ts b/tests/regression/test/v2-migrated/issue-1870.test.ts index 15c8b668..f2ec84c0 100644 --- a/tests/regression/test/v2-migrated/issue-1870.test.ts +++ b/tests/regression/test/v2-migrated/issue-1870.test.ts @@ -1,14 +1,16 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1870', async () => { - await loadSchema( - ` +describe('Regression for issue #1870', () => { + it('verifies issue 1870', async () => { + await loadSchema( + ` model Polygon { id Int @id @default(autoincrement()) geometry Unsupported("geometry(MultiPolygon, 4326)") @@index([geometry], name: "parcel_polygon_idx", type: Gist) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1894.test.ts b/tests/regression/test/v2-migrated/issue-1894.test.ts index 92f4710e..0f3c3fec 100644 --- a/tests/regression/test/v2-migrated/issue-1894.test.ts +++ b/tests/regression/test/v2-migrated/issue-1894.test.ts @@ -1,46 +1,48 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 1894', async () => { - const db = await createTestClient( - ` -model A { - id Int @id @default(autoincrement()) - b B[] -} - -model B { - id Int @id @default(autoincrement()) - a A @relation(fields: [aId], references: [id]) - aId Int - - type String - @@delegate(type) -} - -model C extends B { - f String? -} - `, - { - extraSourceFiles: { - main: ` - import { ZenStackClient } from '@zenstackhq/runtime'; - import { schema } from './schema'; - - async function main() { - const db = new ZenStackClient(schema, {} as any); - await db.a.create({ data: { id: 0 } }); - await db.c.create({ data: { a: { connect: { id: 0 } } } }); - } - - main(); - -`, +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #1894', () => { + it('verifies issue 1894', async () => { + const db = await createTestClient( + ` + model A { + id Int @id @default(autoincrement()) + b B[] + } + + model B { + id Int @id @default(autoincrement()) + a A @relation(fields: [aId], references: [id]) + aId Int + + type String + @@delegate(type) + } + + model C extends B { + f String? + } + `, + { + extraSourceFiles: { + main: ` + import { ZenStackClient } from '@zenstackhq/runtime'; + import { schema } from './schema'; + + async function main() { + const db = new ZenStackClient(schema, {} as any); + await db.a.create({ data: { id: 0 } }); + await db.c.create({ data: { a: { connect: { id: 0 } } } }); + } + + main(); + + `, + }, }, - }, - ); + ); - await db.a.create({ data: { id: 0 } }); - await expect(db.c.create({ data: { a: { connect: { id: 0 } } } })).toResolveTruthy(); + await db.a.create({ data: { id: 0 } }); + await expect(db.c.create({ data: { a: { connect: { id: 0 } } } })).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1930.test.ts b/tests/regression/test/v2-migrated/issue-1930.test.ts index 08b64e24..48fd35b7 100644 --- a/tests/regression/test/v2-migrated/issue-1930.test.ts +++ b/tests/regression/test/v2-migrated/issue-1930.test.ts @@ -1,76 +1,78 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1930', async () => { - const db = await createPolicyTestClient( - ` -model Organization { - id String @id @default(cuid()) - entities Entity[] +describe('Regression for issue #1930', () => { + it('verifies issue 1930', async () => { + const db = await createPolicyTestClient( + ` + model Organization { + id String @id @default(cuid()) + entities Entity[] - @@allow('all', true) -} + @@allow('all', true) + } -model Entity { - id String @id @default(cuid()) - org Organization? @relation(fields: [orgId], references: [id]) - orgId String? - contents EntityContent[] - entityType String - isDeleted Boolean @default(false) + model Entity { + id String @id @default(cuid()) + org Organization? @relation(fields: [orgId], references: [id]) + orgId String? + contents EntityContent[] + entityType String + isDeleted Boolean @default(false) - @@delegate(entityType) + @@delegate(entityType) - @@allow('all', !isDeleted) -} + @@allow('all', !isDeleted) + } -model EntityContent { - id String @id @default(cuid()) - entity Entity @relation(fields: [entityId], references: [id]) - entityId String + model EntityContent { + id String @id @default(cuid()) + entity Entity @relation(fields: [entityId], references: [id]) + entityId String - entityContentType String + entityContentType String - @@delegate(entityContentType) + @@delegate(entityContentType) - @@allow('create', true) - @@allow('read', check(entity)) -} + @@allow('create', true) + @@allow('read', check(entity)) + } -model Article extends Entity { -} + model Article extends Entity { + } -model ArticleContent extends EntityContent { - body String? -} + model ArticleContent extends EntityContent { + body String? + } -model OtherContent extends EntityContent { - data Int -} - `, - ); + model OtherContent extends EntityContent { + data Int + } + `, + ); - const org = await db.$unuseAll().organization.create({ data: {} }); - const article = await db.$unuseAll().article.create({ - data: { org: { connect: { id: org.id } } }, - }); + const org = await db.$unuseAll().organization.create({ data: {} }); + const article = await db.$unuseAll().article.create({ + data: { org: { connect: { id: org.id } } }, + }); - // normal create/read - await expect( - db.articleContent.create({ - data: { body: 'abc', entity: { connect: { id: article.id } } }, - }), - ).toResolveTruthy(); - await expect(db.article.findFirst({ include: { contents: true } })).resolves.toMatchObject({ - contents: expect.arrayContaining([expect.objectContaining({ body: 'abc' })]), - }); + // normal create/read + await expect( + db.articleContent.create({ + data: { body: 'abc', entity: { connect: { id: article.id } } }, + }), + ).toResolveTruthy(); + await expect(db.article.findFirst({ include: { contents: true } })).resolves.toMatchObject({ + contents: expect.arrayContaining([expect.objectContaining({ body: 'abc' })]), + }); - // deleted article's contents are not readable - const deletedArticle = await db.$unuseAll().article.create({ - data: { org: { connect: { id: org.id } }, isDeleted: true }, - }); - const content1 = await db.$unuseAll().articleContent.create({ - data: { body: 'bcd', entity: { connect: { id: deletedArticle.id } } }, + // deleted article's contents are not readable + const deletedArticle = await db.$unuseAll().article.create({ + data: { org: { connect: { id: org.id } }, isDeleted: true }, + }); + const content1 = await db.$unuseAll().articleContent.create({ + data: { body: 'bcd', entity: { connect: { id: deletedArticle.id } } }, + }); + await expect(db.articleContent.findUnique({ where: { id: content1.id } })).toResolveNull(); }); - await expect(db.articleContent.findUnique({ where: { id: content1.id } })).toResolveNull(); }); diff --git a/tests/regression/test/v2-migrated/issue-1991.test.ts b/tests/regression/test/v2-migrated/issue-1991.test.ts index dd92a16e..228765a4 100644 --- a/tests/regression/test/v2-migrated/issue-1991.test.ts +++ b/tests/regression/test/v2-migrated/issue-1991.test.ts @@ -1,9 +1,10 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1991', async () => { - await createPolicyTestClient( - ` +describe('Regression for issue #1991', () => { + it('verifies issue 1991', async () => { + await createPolicyTestClient( + ` type FooMetadata { isLocked Boolean } @@ -22,10 +23,10 @@ model FooOption { meta FooOptionMetadata @json } `, - { - provider: 'postgresql', - extraSourceFiles: { - main: ` + { + provider: 'postgresql', + extraSourceFiles: { + main: ` import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './schema'; @@ -35,7 +36,8 @@ model FooOption { data: { meta: { color: 'red' } } }) `, + }, }, - }, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1992.test.ts b/tests/regression/test/v2-migrated/issue-1992.test.ts index 4ca6b7d8..4e77f45e 100644 --- a/tests/regression/test/v2-migrated/issue-1992.test.ts +++ b/tests/regression/test/v2-migrated/issue-1992.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 1992', async () => { - await loadSchema( - ` +describe('Regression for issue #1992', () => { + it('verifies issue 1992', async () => { + await loadSchema( + ` enum MyAppUserType { Local Google @@ -56,5 +57,6 @@ model MyAppUserFolder { user MyAppUser @relation(fields: [userId], references: [id]) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1994.test.ts b/tests/regression/test/v2-migrated/issue-1994.test.ts index f072bdb3..ff065378 100644 --- a/tests/regression/test/v2-migrated/issue-1994.test.ts +++ b/tests/regression/test/v2-migrated/issue-1994.test.ts @@ -1,102 +1,104 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1994', async () => { - const db = await createTestClient( - ` -model OrganizationRole { - id Int @id @default(autoincrement()) - rolePrivileges OrganizationRolePrivilege[] - type String - @@delegate(type) -} +describe('Regression for issue #1994', () => { + it('verifies issue 1994', async () => { + const db = await createTestClient( + ` + model OrganizationRole { + id Int @id @default(autoincrement()) + rolePrivileges OrganizationRolePrivilege[] + type String + @@delegate(type) + } -model Organization { - id Int @id @default(autoincrement()) - customRoles CustomOrganizationRole[] -} + model Organization { + id Int @id @default(autoincrement()) + customRoles CustomOrganizationRole[] + } -// roles common to all orgs, defined once -model SystemDefinedRole extends OrganizationRole { - name String @unique -} + // roles common to all orgs, defined once + model SystemDefinedRole extends OrganizationRole { + name String @unique + } -// roles specific to each org -model CustomOrganizationRole extends OrganizationRole { - name String - organizationId Int - organization Organization @relation(fields: [organizationId], references: [id]) + // roles specific to each org + model CustomOrganizationRole extends OrganizationRole { + name String + organizationId Int + organization Organization @relation(fields: [organizationId], references: [id]) - @@unique([organizationId, name]) - @@index([organizationId]) -} + @@unique([organizationId, name]) + @@index([organizationId]) + } -model OrganizationRolePrivilege { - organizationRoleId Int - privilegeId Int + model OrganizationRolePrivilege { + organizationRoleId Int + privilegeId Int - organizationRole OrganizationRole @relation(fields: [organizationRoleId], references: [id]) - privilege Privilege @relation(fields: [privilegeId], references: [id]) + organizationRole OrganizationRole @relation(fields: [organizationRoleId], references: [id]) + privilege Privilege @relation(fields: [privilegeId], references: [id]) - @@id([organizationRoleId, privilegeId]) -} + @@id([organizationRoleId, privilegeId]) + } -model Privilege { - id Int @id @default(autoincrement()) - name String // e.g. "org:manage" + model Privilege { + id Int @id @default(autoincrement()) + name String // e.g. "org:manage" - orgRolePrivileges OrganizationRolePrivilege[] - @@unique([name]) -} - `, - { - extraSourceFiles: { - main: ` - import { ZenStackClient } from '@zenstackhq/runtime'; - import { schema } from './schema'; + orgRolePrivileges OrganizationRolePrivilege[] + @@unique([name]) + } + `, + { + extraSourceFiles: { + main: ` + import { ZenStackClient } from '@zenstackhq/runtime'; + import { schema } from './schema'; - const db = new ZenStackClient(schema, {} as any); + const db = new ZenStackClient(schema, {} as any); - async function main() { - const privilege = await db.privilege.create({ - data: { name: 'org:manage' }, - }); + async function main() { + const privilege = await db.privilege.create({ + data: { name: 'org:manage' }, + }); - await db.systemDefinedRole.create({ - data: { - name: 'Admin', - rolePrivileges: { - create: [ - { - privilegeId: privilege.id, - }, - ], + await db.systemDefinedRole.create({ + data: { + name: 'Admin', + rolePrivileges: { + create: [ + { + privilegeId: privilege.id, + }, + ], + }, }, - }, - }); - } - main() - `, + }); + } + main() + `, + }, }, - }, - ); + ); - const privilege = await db.privilege.create({ - data: { name: 'org:manage' }, - }); + const privilege = await db.privilege.create({ + data: { name: 'org:manage' }, + }); - await expect( - db.systemDefinedRole.create({ - data: { - name: 'Admin', - rolePrivileges: { - create: [ - { - privilegeId: privilege.id, - }, - ], + await expect( + db.systemDefinedRole.create({ + data: { + name: 'Admin', + rolePrivileges: { + create: [ + { + privilegeId: privilege.id, + }, + ], + }, }, - }, - }), - ).toResolveTruthy(); + }), + ).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-1997.test.ts b/tests/regression/test/v2-migrated/issue-1997.test.ts index 591694bd..dc742db7 100644 --- a/tests/regression/test/v2-migrated/issue-1997.test.ts +++ b/tests/regression/test/v2-migrated/issue-1997.test.ts @@ -1,129 +1,131 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 1997', async () => { - const db = await createPolicyTestClient( - ` - model Tenant { - id String @id @default(uuid()) +describe('Regression for issue #1997', () => { + it('verifies issue 1997', async () => { + const db = await createPolicyTestClient( + ` + model Tenant { + id String @id @default(uuid()) - users User[] - posts Post[] - comments Comment[] - postUserLikes PostUserLikes[] - } + users User[] + posts Post[] + comments Comment[] + postUserLikes PostUserLikes[] + } - model User { - id String @id @default(uuid()) - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - posts Post[] - likes PostUserLikes[] + model User { + id String @id @default(uuid()) + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + posts Post[] + likes PostUserLikes[] - @@allow('all', true) - } + @@allow('all', true) + } - model Post { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @default(uuid()) - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) + model Post { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) - comments Comment[] - likes PostUserLikes[] + comments Comment[] + likes PostUserLikes[] - @@id([tenantId, id]) + @@id([tenantId, id]) - @@allow('all', true) - } + @@allow('all', true) + } - model PostUserLikes { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @default(uuid()) + model PostUserLikes { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) - userId String - user User @relation(fields: [userId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) - postId String - post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) + postId String + post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) - @@id([tenantId, id]) - @@unique([tenantId, userId, postId]) + @@id([tenantId, id]) + @@unique([tenantId, userId, postId]) - @@allow('all', true) - } + @@allow('all', true) + } - model Comment { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @default(uuid()) - postId String - post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) + model Comment { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + postId String + post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) - @@id([tenantId, id]) + @@id([tenantId, id]) - @@allow('all', true) - } - `, - ); + @@allow('all', true) + } + `, + ); - const tenant = await db.$unuseAll().tenant.create({ - data: {}, - }); - const user = await db.$unuseAll().user.create({ - data: { tenantId: tenant.id }, - }); + const tenant = await db.$unuseAll().tenant.create({ + data: {}, + }); + const user = await db.$unuseAll().user.create({ + data: { tenantId: tenant.id }, + }); - const authDb = db.$setAuth({ id: user.id, tenantId: tenant.id }); + const authDb = db.$setAuth({ id: user.id, tenantId: tenant.id }); - await expect( - authDb.post.create({ - data: { - likes: { - createMany: { - data: [ - { - userId: user.id, - }, - ], + await expect( + authDb.post.create({ + data: { + likes: { + createMany: { + data: [ + { + userId: user.id, + }, + ], + }, }, }, - }, - include: { - likes: true, - }, - }), - ).resolves.toMatchObject({ - authorId: user.id, - likes: [ - { - tenantId: tenant.id, - userId: user.id, - }, - ], - }); + include: { + likes: true, + }, + }), + ).resolves.toMatchObject({ + authorId: user.id, + likes: [ + { + tenantId: tenant.id, + userId: user.id, + }, + ], + }); - await expect( - authDb.post.create({ - data: { - comments: { - createMany: { - data: [{}], + await expect( + authDb.post.create({ + data: { + comments: { + createMany: { + data: [{}], + }, }, }, - }, - include: { - comments: true, - }, - }), - ).resolves.toMatchObject({ - authorId: user.id, - comments: [ - { - tenantId: tenant.id, - }, - ], + include: { + comments: true, + }, + }), + ).resolves.toMatchObject({ + authorId: user.id, + comments: [ + { + tenantId: tenant.id, + }, + ], + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-1998.test.ts b/tests/regression/test/v2-migrated/issue-1998.test.ts index 038278b0..ca57d280 100644 --- a/tests/regression/test/v2-migrated/issue-1998.test.ts +++ b/tests/regression/test/v2-migrated/issue-1998.test.ts @@ -1,56 +1,58 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 1998', async () => { - const db = await createPolicyTestClient( - ` -model Entity { - id String @id - type String - updatable Boolean - children Relation[] @relation("children") - parents Relation[] @relation("parents") - - @@delegate(type) - @@allow('create,read', true) - @@allow('update', updatable) -} - -model A extends Entity {} - -model B extends Entity {} - -model Relation { - parent Entity @relation("children", fields: [parentId], references: [id]) - parentId String - child Entity @relation("parents", fields: [childId], references: [id]) - childId String - - @@allow('create', true) - @@allow('read', check(parent, 'read') && check(child, 'read')) - @@allow('delete', check(parent, 'update') && check(child, 'update')) - - @@id([parentId, childId]) -} - `, - ); - - await db.a.create({ data: { id: '1', updatable: true } }); - await db.b.create({ data: { id: '2', updatable: true } }); - await db.relation.create({ data: { parentId: '1', childId: '2' } }); - - await expect( - db.relation.deleteMany({ - where: { parentId: '1', childId: '2' }, - }), - ).resolves.toEqual({ count: 1 }); - - await db.a.create({ data: { id: '3', updatable: false } }); - await db.b.create({ data: { id: '4', updatable: false } }); - await db.relation.create({ data: { parentId: '3', childId: '4' } }); - await expect( - db.relation.deleteMany({ - where: { parentId: '3', childId: '4' }, - }), - ).resolves.toEqual({ count: 0 }); +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #1998', () => { + it('verifies issue 1998', async () => { + const db = await createPolicyTestClient( + ` + model Entity { + id String @id + type String + updatable Boolean + children Relation[] @relation("children") + parents Relation[] @relation("parents") + + @@delegate(type) + @@allow('create,read', true) + @@allow('update', updatable) + } + + model A extends Entity {} + + model B extends Entity {} + + model Relation { + parent Entity @relation("children", fields: [parentId], references: [id]) + parentId String + child Entity @relation("parents", fields: [childId], references: [id]) + childId String + + @@allow('create', true) + @@allow('read', check(parent, 'read') && check(child, 'read')) + @@allow('delete', check(parent, 'update') && check(child, 'update')) + + @@id([parentId, childId]) + } + `, + ); + + await db.a.create({ data: { id: '1', updatable: true } }); + await db.b.create({ data: { id: '2', updatable: true } }); + await db.relation.create({ data: { parentId: '1', childId: '2' } }); + + await expect( + db.relation.deleteMany({ + where: { parentId: '1', childId: '2' }, + }), + ).resolves.toEqual({ count: 1 }); + + await db.a.create({ data: { id: '3', updatable: false } }); + await db.b.create({ data: { id: '4', updatable: false } }); + await db.relation.create({ data: { parentId: '3', childId: '4' } }); + await expect( + db.relation.deleteMany({ + where: { parentId: '3', childId: '4' }, + }), + ).resolves.toEqual({ count: 0 }); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-2019.test.ts b/tests/regression/test/v2-migrated/issue-2019.test.ts index d25ab518..1c6c4c6e 100644 --- a/tests/regression/test/v2-migrated/issue-2019.test.ts +++ b/tests/regression/test/v2-migrated/issue-2019.test.ts @@ -1,85 +1,87 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 2019', async () => { - const db = await createPolicyTestClient( - ` -model Tenant { - id String @id @default(uuid()) - - users User[] - content Content[] -} - -model User { - id String @id @default(uuid()) - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - posts Post[] - likes PostUserLikes[] - - @@allow('all', true) -} - -model Content { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @id @default(uuid()) - contentType String - - @@delegate(contentType) - @@allow('all', true) -} - -model Post extends Content { - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - comments Comment[] - likes PostUserLikes[] - - @@allow('all', true) -} - -model PostUserLikes extends Content { - userId String - user User @relation(fields: [userId], references: [id]) - - postId String - post Post @relation(fields: [postId], references: [id]) - - @@unique([userId, postId]) - - @@allow('all', true) -} - -model Comment extends Content { - postId String - post Post @relation(fields: [postId], references: [id]) - - @@allow('all', true) -} - `, - ); - - const tenant = await db.$unuseAll().tenant.create({ data: {} }); - const user = await db.$unuseAll().user.create({ data: { tenantId: tenant.id } }); - const authDb = db.$setAuth({ id: user.id, tenantId: tenant.id }); - const result = await authDb.post.create({ - data: { - likes: { - createMany: { - data: [ - { - userId: user.id, - }, - ], +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #2019', () => { + it('verifies issue 2019', async () => { + const db = await createPolicyTestClient( + ` + model Tenant { + id String @id @default(uuid()) + + users User[] + content Content[] + } + + model User { + id String @id @default(uuid()) + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + posts Post[] + likes PostUserLikes[] + + @@allow('all', true) + } + + model Content { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @id @default(uuid()) + contentType String + + @@delegate(contentType) + @@allow('all', true) + } + + model Post extends Content { + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + comments Comment[] + likes PostUserLikes[] + + @@allow('all', true) + } + + model PostUserLikes extends Content { + userId String + user User @relation(fields: [userId], references: [id]) + + postId String + post Post @relation(fields: [postId], references: [id]) + + @@unique([userId, postId]) + + @@allow('all', true) + } + + model Comment extends Content { + postId String + post Post @relation(fields: [postId], references: [id]) + + @@allow('all', true) + } + `, + ); + + const tenant = await db.$unuseAll().tenant.create({ data: {} }); + const user = await db.$unuseAll().user.create({ data: { tenantId: tenant.id } }); + const authDb = db.$setAuth({ id: user.id, tenantId: tenant.id }); + const result = await authDb.post.create({ + data: { + likes: { + createMany: { + data: [ + { + userId: user.id, + }, + ], + }, }, }, - }, - include: { - likes: true, - }, + include: { + likes: true, + }, + }); + expect(result.likes[0].tenantId).toBe(tenant.id); }); - expect(result.likes[0].tenantId).toBe(tenant.id); }); diff --git a/tests/regression/test/v2-migrated/issue-2025.test.ts b/tests/regression/test/v2-migrated/issue-2025.test.ts index 42da8fc4..e5a8f2de 100644 --- a/tests/regression/test/v2-migrated/issue-2025.test.ts +++ b/tests/regression/test/v2-migrated/issue-2025.test.ts @@ -1,38 +1,40 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 2025', async () => { - const db = await createTestClient( - ` - model User { - id String @id @default(cuid()) - email String @unique @email - termsAndConditions Int? - @@allow('all', true) - } - `, - ); +describe('Regression for issue #2025', () => { + it('verifies issue 2025', async () => { + const db = await createTestClient( + ` + model User { + id String @id @default(cuid()) + email String @unique @email + termsAndConditions Int? + @@allow('all', true) + } + `, + ); - await expect( - db.user.create({ + await expect( + db.user.create({ + data: { + email: 'xyz', + }, + }), + ).toBeRejectedByValidation(); + + const user = await db.$setInputValidation(false).user.create({ data: { email: 'xyz', }, - }), - ).toBeRejectedByValidation(); + }); - const user = await db.$setInputValidation(false).user.create({ - data: { - email: 'xyz', - }, + await expect( + db.user.update({ + where: { id: user.id }, + data: { + termsAndConditions: 1, + }, + }), + ).toResolveTruthy(); }); - - await expect( - db.user.update({ - where: { id: user.id }, - data: { - termsAndConditions: 1, - }, - }), - ).toResolveTruthy(); }); diff --git a/tests/regression/test/v2-migrated/issue-2028.test.ts b/tests/regression/test/v2-migrated/issue-2028.test.ts index cff51a9f..192c40e5 100644 --- a/tests/regression/test/v2-migrated/issue-2028.test.ts +++ b/tests/regression/test/v2-migrated/issue-2028.test.ts @@ -1,90 +1,92 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 2028', async () => { - const db = await createTestClient( - ` -enum FooType { - Bar - Baz -} +describe('Regression for issue #2028', () => { + it('verifies issue 2028', async () => { + const db = await createTestClient( + ` + enum FooType { + Bar + Baz + } -model User { - id String @id @default(cuid()) - userFolders UserFolder[] - @@allow('all', true) -} + model User { + id String @id @default(cuid()) + userFolders UserFolder[] + @@allow('all', true) + } -model Foo { - id String @id @default(cuid()) - type FooType + model Foo { + id String @id @default(cuid()) + type FooType - userFolders UserFolder[] + userFolders UserFolder[] - @@delegate(type) - @@allow('all', true) -} + @@delegate(type) + @@allow('all', true) + } -model Bar extends Foo { - name String -} + model Bar extends Foo { + name String + } -model Baz extends Foo { - age Int -} + model Baz extends Foo { + age Int + } -model UserFolder { - id String @id @default(cuid()) - userId String - fooId String + model UserFolder { + id String @id @default(cuid()) + userId String + fooId String - user User @relation(fields: [userId], references: [id]) - foo Foo @relation(fields: [fooId], references: [id]) + user User @relation(fields: [userId], references: [id]) + foo Foo @relation(fields: [fooId], references: [id]) - @@unique([userId, fooId]) - @@allow('all', true) -} - `, - ); + @@unique([userId, fooId]) + @@allow('all', true) + } + `, + ); - // Ensure we can query by the CompoundUniqueInput - const user = await db.user.create({ data: {} }); - const bar = await db.bar.create({ data: { name: 'bar' } }); - const baz = await db.baz.create({ data: { age: 1 } }); + // Ensure we can query by the CompoundUniqueInput + const user = await db.user.create({ data: {} }); + const bar = await db.bar.create({ data: { name: 'bar' } }); + const baz = await db.baz.create({ data: { age: 1 } }); - const userFolderA = await db.userFolder.create({ - data: { - userId: user.id, - fooId: bar.id, - }, - }); + const userFolderA = await db.userFolder.create({ + data: { + userId: user.id, + fooId: bar.id, + }, + }); - const userFolderB = await db.userFolder.create({ - data: { - userId: user.id, - fooId: baz.id, - }, - }); + const userFolderB = await db.userFolder.create({ + data: { + userId: user.id, + fooId: baz.id, + }, + }); - await expect( - db.userFolder.findUnique({ - where: { - userId_fooId: { - userId: user.id, - fooId: bar.id, + await expect( + db.userFolder.findUnique({ + where: { + userId_fooId: { + userId: user.id, + fooId: bar.id, + }, }, - }, - }), - ).resolves.toMatchObject(userFolderA); + }), + ).resolves.toMatchObject(userFolderA); - await expect( - db.userFolder.findUnique({ - where: { - userId_fooId: { - userId: user.id, - fooId: baz.id, + await expect( + db.userFolder.findUnique({ + where: { + userId_fooId: { + userId: user.id, + fooId: baz.id, + }, }, - }, - }), - ).resolves.toMatchObject(userFolderB); + }), + ).resolves.toMatchObject(userFolderB); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-2038.test.ts b/tests/regression/test/v2-migrated/issue-2038.test.ts index 61fb429a..4220010e 100644 --- a/tests/regression/test/v2-migrated/issue-2038.test.ts +++ b/tests/regression/test/v2-migrated/issue-2038.test.ts @@ -1,25 +1,27 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 2038', async () => { - const db = await createTestClient( - ` -model User { - id Int @id @default(autoincrement()) - flag Boolean - @@allow('all', true) -} +describe('Regression for issue #2038', () => { + it('verifies issue 2038', async () => { + const db = await createTestClient( + ` + model User { + id Int @id @default(autoincrement()) + flag Boolean + @@allow('all', true) + } -model Post { - id Int @id @default(autoincrement()) - published Boolean @default(auth().flag) - @@allow('all', true) -} - `, - ); + model Post { + id Int @id @default(autoincrement()) + published Boolean @default(auth().flag) + @@allow('all', true) + } + `, + ); - const authDb = db.$setAuth({ id: 1, flag: true }); - await expect(authDb.post.create({ data: {} })).resolves.toMatchObject({ - published: true, + const authDb = db.$setAuth({ id: 1, flag: true }); + await expect(authDb.post.create({ data: {} })).resolves.toMatchObject({ + published: true, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-2039.test.ts b/tests/regression/test/v2-migrated/issue-2039.test.ts index ba62278d..973896d6 100644 --- a/tests/regression/test/v2-migrated/issue-2039.test.ts +++ b/tests/regression/test/v2-migrated/issue-2039.test.ts @@ -1,27 +1,29 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 2039', async () => { - const db = await createTestClient( - ` -type Foo { - a String -} +describe('Regression for issue #2039', () => { + it('verifies issue 2039', async () => { + const db = await createTestClient( + ` + type Foo { + a String + } -model Bar { - id String @id @default(cuid()) - foo Foo @json @default("{ \\"a\\": \\"a\\" }") - fooList Foo[] @json @default("[{ \\"a\\": \\"b\\" }]") - @@allow('all', true) -} - `, - { provider: 'postgresql' }, - ); + model Bar { + id String @id @default(cuid()) + foo Foo @json @default("{ \\"a\\": \\"a\\" }") + fooList Foo[] @json @default("[{ \\"a\\": \\"b\\" }]") + @@allow('all', true) + } + `, + { provider: 'postgresql' }, + ); - // Ensure default values are correctly set - await expect(db.bar.create({ data: {} })).resolves.toMatchObject({ - id: expect.any(String), - foo: { a: 'a' }, - fooList: [{ a: 'b' }], + // Ensure default values are correctly set + await expect(db.bar.create({ data: {} })).resolves.toMatchObject({ + id: expect.any(String), + foo: { a: 'a' }, + fooList: [{ a: 'b' }], + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-2106.test.ts b/tests/regression/test/v2-migrated/issue-2106.test.ts index af9afe96..92501841 100644 --- a/tests/regression/test/v2-migrated/issue-2106.test.ts +++ b/tests/regression/test/v2-migrated/issue-2106.test.ts @@ -1,16 +1,18 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 2106', async () => { - const db = await createTestClient( - ` -model User { - id Int @id - age BigInt - @@allow('all', true) -} - `, - ); +describe('Regression for issue #2106', () => { + it('verifies issue 2106', async () => { + const db = await createTestClient( + ` + model User { + id Int @id + age BigInt + @@allow('all', true) + } + `, + ); - await expect(db.user.create({ data: { id: 1, age: 1n } })).toResolveTruthy(); + await expect(db.user.create({ data: { id: 1, age: 1n } })).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-2246.test.ts b/tests/regression/test/v2-migrated/issue-2246.test.ts index d6ff576c..3a4aabdb 100644 --- a/tests/regression/test/v2-migrated/issue-2246.test.ts +++ b/tests/regression/test/v2-migrated/issue-2246.test.ts @@ -1,79 +1,81 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 2246', async () => { - const db = await createTestClient( - ` -model Media { - id Int @id @default(autoincrement()) - title String - mediaType String +describe('Regression for issue #2246', () => { + it('verifies issue 2246', async () => { + const db = await createTestClient( + ` + model Media { + id Int @id @default(autoincrement()) + title String + mediaType String - @@delegate(mediaType) - @@allow('all', true) -} + @@delegate(mediaType) + @@allow('all', true) + } -model Movie extends Media { - director Director @relation(fields: [directorId], references: [id]) - directorId Int - duration Int - rating String -} + model Movie extends Media { + director Director @relation(fields: [directorId], references: [id]) + directorId Int + duration Int + rating String + } -model Director { - id Int @id @default(autoincrement()) - name String - email String - movies Movie[] + model Director { + id Int @id @default(autoincrement()) + name String + email String + movies Movie[] - @@allow('all', true) -} - `, - ); + @@allow('all', true) + } + `, + ); - await db.director.create({ - data: { - name: 'Christopher Nolan', - email: 'christopher.nolan@example.com', - movies: { - create: { - title: 'Inception', - duration: 148, - rating: 'PG-13', + await db.director.create({ + data: { + name: 'Christopher Nolan', + email: 'christopher.nolan@example.com', + movies: { + create: { + title: 'Inception', + duration: 148, + rating: 'PG-13', + }, }, }, - }, - }); + }); - await expect( - db.director.findMany({ - include: { - movies: { - where: { title: 'Inception' }, + await expect( + db.director.findMany({ + include: { + movies: { + where: { title: 'Inception' }, + }, }, - }, - }), - ).resolves.toHaveLength(1); + }), + ).resolves.toHaveLength(1); - await expect( - db.director.findFirst({ - include: { - _count: { select: { movies: { where: { title: 'Inception' } } } }, - }, - }), - ).resolves.toMatchObject({ _count: { movies: 1 } }); + await expect( + db.director.findFirst({ + include: { + _count: { select: { movies: { where: { title: 'Inception' } } } }, + }, + }), + ).resolves.toMatchObject({ _count: { movies: 1 } }); - await expect( - db.movie.findMany({ - where: { title: 'Interstellar' }, - }), - ).resolves.toHaveLength(0); + await expect( + db.movie.findMany({ + where: { title: 'Interstellar' }, + }), + ).resolves.toHaveLength(0); - await expect( - db.director.findFirst({ - include: { - _count: { select: { movies: { where: { title: 'Interstellar' } } } }, - }, - }), - ).resolves.toMatchObject({ _count: { movies: 0 } }); + await expect( + db.director.findFirst({ + include: { + _count: { select: { movies: { where: { title: 'Interstellar' } } } }, + }, + }), + ).resolves.toMatchObject({ _count: { movies: 0 } }); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-2247.test.ts b/tests/regression/test/v2-migrated/issue-2247.test.ts index 6caf02a4..8f1104c1 100644 --- a/tests/regression/test/v2-migrated/issue-2247.test.ts +++ b/tests/regression/test/v2-migrated/issue-2247.test.ts @@ -1,61 +1,63 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; - -it('verifies issue 2247', async () => { - const db = await createTestClient( - ` -model User { - id String @id @default(cuid()) - employerId String? -} - -model Member { - id String @id @default(cuid()) - placeId String - place Place @relation(fields: [placeId], references: [id]) -} - -model Place { - id String @id @default(cuid()) - name String - placeType String @map("owner_type") - members Member[] - - @@delegate(placeType) - @@unique([name, placeType]) -} - -model Country extends Place { - regions Region[] - things Thing[] -} - -model Region extends Place { - countryId String - country Country @relation(fields: [countryId], references: [id]) - cities City[] -} - -model City extends Place { - regionId String - region Region @relation(fields: [regionId], references: [id]) -} - - -model Thing { - id String @id @default(cuid()) - countryId String - country Country @relation(fields: [countryId], references: [id]) - - @@allow('read', - country.members?[id == auth().employerId] - || country.regions?[members?[id == auth().employerId]] - || country.regions?[cities?[members?[id == auth().employerId]]] - ) -} - `, - ); - - const authDb = db.$setAuth({ id: '1', employerId: '1' }); - await expect(authDb.thing.findMany()).toResolveTruthy(); +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #2247', () => { + it('verifies issue 2247', async () => { + const db = await createTestClient( + ` + model User { + id String @id @default(cuid()) + employerId String? + } + + model Member { + id String @id @default(cuid()) + placeId String + place Place @relation(fields: [placeId], references: [id]) + } + + model Place { + id String @id @default(cuid()) + name String + placeType String @map("owner_type") + members Member[] + + @@delegate(placeType) + @@unique([name, placeType]) + } + + model Country extends Place { + regions Region[] + things Thing[] + } + + model Region extends Place { + countryId String + country Country @relation(fields: [countryId], references: [id]) + cities City[] + } + + model City extends Place { + regionId String + region Region @relation(fields: [regionId], references: [id]) + } + + + model Thing { + id String @id @default(cuid()) + countryId String + country Country @relation(fields: [countryId], references: [id]) + + @@allow('read', + country.members?[id == auth().employerId] + || country.regions?[members?[id == auth().employerId]] + || country.regions?[cities?[members?[id == auth().employerId]]] + ) + } + `, + ); + + const authDb = db.$setAuth({ id: '1', employerId: '1' }); + await expect(authDb.thing.findMany()).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-389.test.ts b/tests/regression/test/v2-migrated/issue-389.test.ts index 20777b2e..c1ba5bbe 100644 --- a/tests/regression/test/v2-migrated/issue-389.test.ts +++ b/tests/regression/test/v2-migrated/issue-389.test.ts @@ -1,15 +1,17 @@ -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { createPolicyTestClient } from '@zenstackhq/testtools'; -it('verifies issue 389', async () => { - const db = await createPolicyTestClient(` - model model { - id String @id @default(uuid()) - value Int - @@allow('read', true) - @@allow('create', value > 0) - } - `); - await expect(db.model.create({ data: { value: 0 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { value: 1 } })).toResolveTruthy(); +describe('Regression for issue #389', () => { + it('verifies issue 389', async () => { + const db = await createPolicyTestClient(` + model model { + id String @id @default(uuid()) + value Int + @@allow('read', true) + @@allow('create', value > 0) + } + `); + await expect(db.model.create({ data: { value: 0 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { value: 1 } })).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-392.test.ts b/tests/regression/test/v2-migrated/issue-392.test.ts index bf8acfb4..857f4790 100644 --- a/tests/regression/test/v2-migrated/issue-392.test.ts +++ b/tests/regression/test/v2-migrated/issue-392.test.ts @@ -1,63 +1,65 @@ import { loadDocument } from '@zenstackhq/language'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 392', async () => { - await loadDocument( - ` +describe('Regression for issue #392', () => { + it('verifies issue 392', async () => { + await loadDocument( + ` model M1 { m2_id String @id m2 M2 @relation(fields: [m2_id], references: [id]) } - + model M2 { - id String @id + id String @id m1 M1? } `, - ); + ); - await loadDocument( - ` + await loadDocument( + ` model M1 { id String @id m2_id String @unique m2 M2 @relation(fields: [m2_id], references: [id]) } - + model M2 { - id String @id + id String @id m1 M1? } `, - ); + ); - await loadDocument( - ` + await loadDocument( + ` model M1 { m2_id String m2 M2 @relation(fields: [m2_id], references: [id]) @@id([m2_id]) } - + model M2 { - id String @id + id String @id m1 M1? } `, - ); + ); - await loadDocument( - ` + await loadDocument( + ` model M1 { m2_id String m2 M2 @relation(fields: [m2_id], references: [id]) @@unique([m2_id]) } - + model M2 { - id String @id + id String @id m1 M1? } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-416.test.ts b/tests/regression/test/v2-migrated/issue-416.test.ts index 75776394..827ebec8 100644 --- a/tests/regression/test/v2-migrated/issue-416.test.ts +++ b/tests/regression/test/v2-migrated/issue-416.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 416', async () => { - await loadSchema( - ` +describe('Regression for issue #416', () => { + it('verifies issue 416', async () => { + await loadSchema( + ` datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -16,5 +17,6 @@ model Example { json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-509.test.ts b/tests/regression/test/v2-migrated/issue-509.test.ts index ceefb2f5..a3691c05 100644 --- a/tests/regression/test/v2-migrated/issue-509.test.ts +++ b/tests/regression/test/v2-migrated/issue-509.test.ts @@ -1,16 +1,17 @@ import { loadDocument } from '@zenstackhq/language'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 509', async () => { - await loadDocument( - ` +describe('Regression for issue #509', () => { + it('verifies issue 509', async () => { + await loadDocument( + ` model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } - + model Post { id Int @id @default(autoincrement()) title String @@ -18,12 +19,13 @@ it('verifies issue 509', async () => { published Boolean @default(false) author User? @relation(fields: [authorId], references: [id]) authorId Int? - + deleted Boolean @default(false) @omit - + @@allow('all', true) @@deny('read', deleted) } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-609.test.ts b/tests/regression/test/v2-migrated/issue-609.test.ts index b25314c6..7eca4b1a 100644 --- a/tests/regression/test/v2-migrated/issue-609.test.ts +++ b/tests/regression/test/v2-migrated/issue-609.test.ts @@ -1,59 +1,61 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 609', async () => { - const db = await createPolicyTestClient( - ` -model User { - id String @id @default(cuid()) - comments Comment[] -} +describe('Regression for issue #609', () => { + it('verifies issue 609', async () => { + const db = await createPolicyTestClient( + ` + model User { + id String @id @default(cuid()) + comments Comment[] + } -model Comment { - id String @id @default(cuid()) - parentCommentId String? - replies Comment[] @relation("CommentToComment") - parent Comment? @relation("CommentToComment", fields: [parentCommentId], references: [id]) - comment String - author User @relation(fields: [authorId], references: [id]) - authorId String + model Comment { + id String @id @default(cuid()) + parentCommentId String? + replies Comment[] @relation("CommentToComment") + parent Comment? @relation("CommentToComment", fields: [parentCommentId], references: [id]) + comment String + author User @relation(fields: [authorId], references: [id]) + authorId String - @@allow('read,create', true) - @@allow('update,delete', auth() == author) -} - `, - { usePrismaPush: true }, - ); + @@allow('read,create', true) + @@allow('update,delete', auth() == author) + } + `, + { usePrismaPush: true }, + ); - const rawDb = db.$unuseAll(); + const rawDb = db.$unuseAll(); - await rawDb.user.create({ - data: { - id: '1', - comments: { - create: { - id: '1', - comment: 'Comment 1', + await rawDb.user.create({ + data: { + id: '1', + comments: { + create: { + id: '1', + comment: 'Comment 1', + }, }, }, - }, - }); - - await rawDb.user.create({ - data: { - id: '2', - }, - }); + }); - // connecting a child comment from a different user to a parent comment should succeed - const dbAuth = db.$setAuth({ id: '2' }); - await expect( - dbAuth.comment.create({ + await rawDb.user.create({ data: { - comment: 'Comment 2', - author: { connect: { id: '2' } }, - parent: { connect: { id: '1' } }, + id: '2', }, - }), - ).toResolveTruthy(); + }); + + // connecting a child comment from a different user to a parent comment should succeed + const dbAuth = db.$setAuth({ id: '2' }); + await expect( + dbAuth.comment.create({ + data: { + comment: 'Comment 2', + author: { connect: { id: '2' } }, + parent: { connect: { id: '1' } }, + }, + }), + ).toResolveTruthy(); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-632.test.ts b/tests/regression/test/v2-migrated/issue-632.test.ts index 99d251d2..53a9817c 100644 --- a/tests/regression/test/v2-migrated/issue-632.test.ts +++ b/tests/regression/test/v2-migrated/issue-632.test.ts @@ -1,9 +1,10 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 632', async () => { - await createTestClient( - ` +describe('Regression for issue #632', () => { + it('verifies issue 632', async () => { + await createTestClient( + ` enum InventoryUnit { DIGITAL FL_OZ @@ -13,13 +14,14 @@ enum InventoryUnit { UNIT UNLIMITED } - + model TwoEnumsOneModelTest { id String @id @default(cuid()) inventoryUnit InventoryUnit @default(UNIT) inputUnit InventoryUnit @default(UNIT) } `, - { provider: 'postgresql', usePrismaPush: true }, - ); + { provider: 'postgresql', usePrismaPush: true }, + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-646.test.ts b/tests/regression/test/v2-migrated/issue-646.test.ts index 12daf25b..e2811b13 100644 --- a/tests/regression/test/v2-migrated/issue-646.test.ts +++ b/tests/regression/test/v2-migrated/issue-646.test.ts @@ -1,11 +1,13 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 646', async () => { - await loadSchema(` +describe('Regression for issue #646', () => { + it('verifies issue 646', async () => { + await loadSchema(` model Example { id Int @id epsilon Decimal @default(0.00000001) } `); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-674.test.ts b/tests/regression/test/v2-migrated/issue-674.test.ts index 03ef6cec..9c2a707f 100644 --- a/tests/regression/test/v2-migrated/issue-674.test.ts +++ b/tests/regression/test/v2-migrated/issue-674.test.ts @@ -1,14 +1,16 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 674', async () => { - await loadSchema( - ` +describe('Regression for issue #674', () => { + it('verifies issue 674', async () => { + await loadSchema( + ` model Foo { id Int @id } enum MyUnUsedEnum { ABC CDE @@map('my_unused_enum') } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-689.test.ts b/tests/regression/test/v2-migrated/issue-689.test.ts index d62922ec..7d0c09bb 100644 --- a/tests/regression/test/v2-migrated/issue-689.test.ts +++ b/tests/regression/test/v2-migrated/issue-689.test.ts @@ -1,71 +1,73 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 689', async () => { - const db = await createPolicyTestClient( - ` - model UserRole { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - role String +describe('Regression for issue #689', () => { + it('verifies issue 689', async () => { + const db = await createPolicyTestClient( + ` + model UserRole { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int + role String - @@allow('all', true) - } + @@allow('all', true) + } - model User { - id Int @id @default(autoincrement()) - userRole UserRole[] - deleted Boolean @default(false) + model User { + id Int @id @default(autoincrement()) + userRole UserRole[] + deleted Boolean @default(false) - @@allow('create,read', true) - @@allow('read', auth() == this) - @@allow('read', userRole?[user == auth() && 'Admin' == role]) - @@allow('read', userRole?[user == auth()]) - } - `, - ); + @@allow('create,read', true) + @@allow('read', auth() == this) + @@allow('read', userRole?[user == auth() && 'Admin' == role]) + @@allow('read', userRole?[user == auth()]) + } + `, + ); - const rawDb = db.$unuseAll(); + const rawDb = db.$unuseAll(); - await rawDb.user.create({ - data: { - id: 1, - userRole: { - create: [ - { id: 1, role: 'Admin' }, - { id: 2, role: 'Student' }, - ], + await rawDb.user.create({ + data: { + id: 1, + userRole: { + create: [ + { id: 1, role: 'Admin' }, + { id: 2, role: 'Student' }, + ], + }, }, - }, - }); + }); - await rawDb.user.create({ - data: { - id: 2, - userRole: { - connect: { id: 1 }, + await rawDb.user.create({ + data: { + id: 2, + userRole: { + connect: { id: 1 }, + }, }, - }, - }); + }); - const c1 = await rawDb.user.count({ - where: { - userRole: { - some: { role: 'Student' }, + const c1 = await rawDb.user.count({ + where: { + userRole: { + some: { role: 'Student' }, + }, + NOT: { deleted: true }, }, - NOT: { deleted: true }, - }, - }); + }); - const c2 = await db.user.count({ - where: { - userRole: { - some: { role: 'Student' }, + const c2 = await db.user.count({ + where: { + userRole: { + some: { role: 'Student' }, + }, + NOT: { deleted: true }, }, - NOT: { deleted: true }, - }, - }); + }); - expect(c1).toEqual(c2); + expect(c1).toEqual(c2); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-714.test.ts b/tests/regression/test/v2-migrated/issue-714.test.ts index 854a5c19..71b35596 100644 --- a/tests/regression/test/v2-migrated/issue-714.test.ts +++ b/tests/regression/test/v2-migrated/issue-714.test.ts @@ -1,9 +1,10 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 714', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #714', () => { + it('verifies issue 714', async () => { + const db = await createPolicyTestClient( + ` model User { id Int @id @default(autoincrement()) username String @unique @@ -79,67 +80,68 @@ it('verifies issue 714', async () => { companyId Int @@allow('all', true) - } + } `, - { usePrismaPush: true }, - ); + { usePrismaPush: true }, + ); - await db.user.create({ - data: { - username: 'test@example.com', - }, - }); + await db.user.create({ + data: { + username: 'test@example.com', + }, + }); - await db.company.create({ - data: { - name: 'My Company', - companyUsers: { - create: { - dummyField: '', - user: { - connect: { - id: 1, + await db.company.create({ + data: { + name: 'My Company', + companyUsers: { + create: { + dummyField: '', + user: { + connect: { + id: 1, + }, }, }, }, - }, - propertyUsers: { - connect: { - id: 1, - }, - }, - properties: { - create: [ - { - name: 'Test', + propertyUsers: { + connect: { + id: 1, }, - ], + }, + properties: { + create: [ + { + name: 'Test', + }, + ], + }, }, - }, - }); + }); - await db.property.update({ - data: { - users: { - create: { - dummyField: '', - roles: { - createMany: { - data: { - type: 'Owner', + await db.property.update({ + data: { + users: { + create: { + dummyField: '', + roles: { + createMany: { + data: { + type: 'Owner', + }, }, }, - }, - user: { - connect: { - id: 1, + user: { + connect: { + id: 1, + }, }, }, }, }, - }, - where: { - id: 1, - }, + where: { + id: 1, + }, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-735.test.ts b/tests/regression/test/v2-migrated/issue-735.test.ts index 0739f5d8..a78759d7 100644 --- a/tests/regression/test/v2-migrated/issue-735.test.ts +++ b/tests/regression/test/v2-migrated/issue-735.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 735', async () => { - await loadSchema( - ` +describe('Regression for issue #735', () => { + it('verifies issue 735', async () => { + await loadSchema( + ` model MyModel { id String @id @default(cuid()) view String @@ -15,5 +16,6 @@ it('verifies issue 735', async () => { name String } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-756.test.ts b/tests/regression/test/v2-migrated/issue-756.test.ts index 85b004a6..ae56f130 100644 --- a/tests/regression/test/v2-migrated/issue-756.test.ts +++ b/tests/regression/test/v2-migrated/issue-756.test.ts @@ -1,24 +1,25 @@ import { loadSchemaWithError } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 756', async () => { - await loadSchemaWithError( - ` +describe('Regression for issue #756', () => { + it('verifies issue 756', async () => { + await loadSchemaWithError( + ` generator client { provider = "prisma-client-js" } - + datasource db { provider = "postgresql" url = env("DATABASE_URL") } - + model User { id Int @id @default(autoincrement()) email Int posts Post[] } - + model Post { id Int @id @default(autoincrement()) author User? @relation(fields: [authorId], references: [id]) @@ -26,6 +27,7 @@ it('verifies issue 756', async () => { @@allow('all', auth().posts.authorId == authorId) } `, - `Could not resolve reference to MemberAccessTarget named 'authorId'.`, - ); + `Could not resolve reference to MemberAccessTarget named 'authorId'.`, + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-764.test.ts b/tests/regression/test/v2-migrated/issue-764.test.ts index b34f1bac..29ca3d82 100644 --- a/tests/regression/test/v2-migrated/issue-764.test.ts +++ b/tests/regression/test/v2-migrated/issue-764.test.ts @@ -1,46 +1,48 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 764', async () => { - const db = await createPolicyTestClient( - ` +describe('Regression for issue #764', () => { + it('verifies issue 764', async () => { + const db = await createPolicyTestClient( + ` model User { id Int @id @default(autoincrement()) name String - + post Post? @relation(fields: [postId], references: [id]) postId Int? - + @@allow('all', true) } - + model Post { id Int @id @default(autoincrement()) title String User User[] - + @@allow('all', true) } `, - ); + ); - const user = await db.user.create({ - data: { name: 'Me' }, - }); + const user = await db.user.create({ + data: { name: 'Me' }, + }); - await db.user.update({ - where: { id: user.id }, - data: { - post: { - upsert: { - create: { - title: 'Hello World', - }, - update: { - title: 'Hello World', + await db.user.update({ + where: { id: user.id }, + data: { + post: { + upsert: { + create: { + title: 'Hello World', + }, + update: { + title: 'Hello World', + }, }, }, }, - }, + }); }); }); diff --git a/tests/regression/test/v2-migrated/issue-765.test.ts b/tests/regression/test/v2-migrated/issue-765.test.ts index f4cc959b..6d2d255d 100644 --- a/tests/regression/test/v2-migrated/issue-765.test.ts +++ b/tests/regression/test/v2-migrated/issue-765.test.ts @@ -1,35 +1,37 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 765', async () => { - const db = await createPolicyTestClient( - ` -model User { - id Int @id @default(autoincrement()) - name String +describe('Regression for issue #765', () => { + it('verifies issue 765', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id @default(autoincrement()) + name String - post Post? @relation(fields: [postId], references: [id]) - postId Int? + post Post? @relation(fields: [postId], references: [id]) + postId Int? - @@allow('all', true) -} + @@allow('all', true) + } -model Post { - id Int @id @default(autoincrement()) - title String - User User[] + model Post { + id Int @id @default(autoincrement()) + title String + User User[] - @@allow('all', true) -} - `, - ); + @@allow('all', true) + } + `, + ); - const r = await db.user.create({ - data: { - name: 'Me', - post: undefined, - }, + const r = await db.user.create({ + data: { + name: 'Me', + post: undefined, + }, + }); + expect(r.name).toBe('Me'); + expect(r.post).toBeUndefined(); }); - expect(r.name).toBe('Me'); - expect(r.post).toBeUndefined(); }); diff --git a/tests/regression/test/v2-migrated/issue-804.test.ts b/tests/regression/test/v2-migrated/issue-804.test.ts index fcb1ab4d..8eb349e2 100644 --- a/tests/regression/test/v2-migrated/issue-804.test.ts +++ b/tests/regression/test/v2-migrated/issue-804.test.ts @@ -1,24 +1,25 @@ import { loadSchemaWithError } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 804', async () => { - await loadSchemaWithError( - ` +describe('Regression for issue #804', () => { + it('verifies issue 804', async () => { + await loadSchemaWithError( + ` generator client { provider = "prisma-client-js" } - + datasource db { provider = "postgresql" url = env("DATABASE_URL") } - + model User { id Int @id @default(autoincrement()) email Int posts Post[] } - + model Post { id Int @id @default(autoincrement()) author User? @relation(fields: [authorId], references: [id]) @@ -28,6 +29,7 @@ it('verifies issue 804', async () => { @@allow('all', auth().posts?[published] == 'TRUE') } `, - 'incompatible operand types', - ); + 'incompatible operand types', + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-811.test.ts b/tests/regression/test/v2-migrated/issue-811.test.ts index 384b3d19..95cfd2a7 100644 --- a/tests/regression/test/v2-migrated/issue-811.test.ts +++ b/tests/regression/test/v2-migrated/issue-811.test.ts @@ -1,70 +1,72 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 811', async () => { - const db = await createPolicyTestClient( - ` - model Membership { - id String @id @default(uuid()) - role String @default('STANDARD') - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String @unique +describe('Regression for issue #811', () => { + it('verifies issue 811', async () => { + const db = await createPolicyTestClient( + ` + model Membership { + id String @id @default(uuid()) + role String @default('STANDARD') + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @unique - @@auth - @@allow('create,update,delete', auth().role == 'ADMIN') - @@allow('update', auth() == this) - @@allow('read', true) - } - model User { - id String @id @default(uuid()) - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) - profileId String @unique - memberships Membership[] + @@auth + @@allow('create,update,delete', auth().role == 'ADMIN') + @@allow('update', auth() == this) + @@allow('read', true) + } + model User { + id String @id @default(uuid()) + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String @unique + memberships Membership[] - @@allow('create,update,delete', auth().role == 'ADMIN') - @@allow('update', id == auth().userId) - @@allow('read', true) - } - model Profile { - id String @id @default(uuid()) - firstName String - users User[] + @@allow('create,update,delete', auth().role == 'ADMIN') + @@allow('update', id == auth().userId) + @@allow('read', true) + } + model Profile { + id String @id @default(uuid()) + firstName String + users User[] - @@allow('create,update,delete', auth().role == 'ADMIN') - @@allow('update', users?[id == auth().userId]) - @@allow('read', true) - } - `, - ); + @@allow('create,update,delete', auth().role == 'ADMIN') + @@allow('update', users?[id == auth().userId]) + @@allow('read', true) + } + `, + ); - const r = await db.$unuseAll().user.create({ - data: { - profile: { - create: { firstName: 'Tom' }, + const r = await db.$unuseAll().user.create({ + data: { + profile: { + create: { firstName: 'Tom' }, + }, + memberships: { + create: { role: 'STANDARD' }, + }, }, - memberships: { - create: { role: 'STANDARD' }, + include: { + profile: true, + memberships: true, }, - }, - include: { - profile: true, - memberships: true, - }, - }); + }); - const membershipId = r.memberships[0].id; - const userId = r.id; - const authDb = db.$setAuth({ id: membershipId, role: 'ADMIN', userId }); + const membershipId = r.memberships[0].id; + const userId = r.id; + const authDb = db.$setAuth({ id: membershipId, role: 'ADMIN', userId }); - const r1 = await authDb.membership.update({ - data: { - role: 'VIP', - user: { update: { data: { profile: { update: { data: { firstName: 'Jerry' } } } } } }, - }, - include: { user: { include: { profile: true } } }, - where: { id: membershipId }, - }); + const r1 = await authDb.membership.update({ + data: { + role: 'VIP', + user: { update: { data: { profile: { update: { data: { firstName: 'Jerry' } } } } } }, + }, + include: { user: { include: { profile: true } } }, + where: { id: membershipId }, + }); - expect(r1.role).toBe('VIP'); - expect(r1.user.profile.firstName).toBe('Jerry'); + expect(r1.role).toBe('VIP'); + expect(r1.user.profile.firstName).toBe('Jerry'); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-825.test.ts b/tests/regression/test/v2-migrated/issue-825.test.ts index 56079097..4b5aa8eb 100644 --- a/tests/regression/test/v2-migrated/issue-825.test.ts +++ b/tests/regression/test/v2-migrated/issue-825.test.ts @@ -1,39 +1,41 @@ import { createPolicyTestClient } from '@zenstackhq/testtools'; -import { expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; -it('verifies issue 825', async () => { - const db = await createPolicyTestClient( - ` -model User { - id Int @id @default(autoincrement()) - role String +describe('Regression for issue #825', () => { + it('verifies issue 825', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id @default(autoincrement()) + role String - @@allow('read', true) - @@allow('update', auth().id == id || auth().role == 'superadmin' || auth().role == 'admin') - @@deny('update', - (role == 'superadmin' && auth().id != id) - || (role == 'admin' && auth().id != id && auth().role != 'superadmin')) + @@allow('read', true) + @@allow('update', auth().id == id || auth().role == 'superadmin' || auth().role == 'admin') + @@deny('update', + (role == 'superadmin' && auth().id != id) + || (role == 'admin' && auth().id != id && auth().role != 'superadmin')) - @@deny('post-update', - (before().role != role && auth().role != 'admin' && auth().role != 'superadmin') - || (before().role != role && role == 'superadmin') - || (before().role != role && role == 'admin' && auth().role != 'superadmin')) -} - `, - ); + @@deny('post-update', + (before().role != role && auth().role != 'admin' && auth().role != 'superadmin') + || (before().role != role && role == 'superadmin') + || (before().role != role && role == 'admin' && auth().role != 'superadmin')) + } + `, + ); - const admin = await db.$unuseAll().user.create({ - data: { role: 'admin' }, - }); + const admin = await db.$unuseAll().user.create({ + data: { role: 'admin' }, + }); - const user = await db.$unuseAll().user.create({ - data: { role: 'customer' }, - }); + const user = await db.$unuseAll().user.create({ + data: { role: 'customer' }, + }); - const r = await db.$setAuth(admin).user.update({ - where: { id: user.id }, - data: { role: 'staff' }, - }); + const r = await db.$setAuth(admin).user.update({ + where: { id: user.id }, + data: { role: 'staff' }, + }); - expect(r.role).toEqual('staff'); + expect(r.role).toEqual('staff'); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-947.test.ts b/tests/regression/test/v2-migrated/issue-947.test.ts index 04a24538..21aca40f 100644 --- a/tests/regression/test/v2-migrated/issue-947.test.ts +++ b/tests/regression/test/v2-migrated/issue-947.test.ts @@ -1,9 +1,10 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 947', async () => { - await loadSchema( - ` +describe('Regression for issue #947', () => { + it('verifies issue 947', async () => { + await loadSchema( + ` datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -13,11 +14,12 @@ model Test { id String @id props TestEnum[] @default([]) } - + enum TestEnum { A B } `, - ); + ); + }); }); diff --git a/tests/regression/test/v2-migrated/issue-971.test.ts b/tests/regression/test/v2-migrated/issue-971.test.ts index a20d5bb6..d571344a 100644 --- a/tests/regression/test/v2-migrated/issue-971.test.ts +++ b/tests/regression/test/v2-migrated/issue-971.test.ts @@ -1,16 +1,17 @@ import { loadSchema } from '@zenstackhq/testtools'; -import { it } from 'vitest'; +import { describe, it } from 'vitest'; -it('verifies issue 971', async () => { - await loadSchema( - ` +describe('Regression for issue #971', () => { + it('verifies issue 971', async () => { + await loadSchema( + ` type Level1 { id String @id @default(cuid()) URL String? @@validate(URL != null, "URL must be provided") // works } type Level2 with Level1 { - @@validate(URL != null, "URL must be provided") // works + @@validate(URL != null, "URL must be provided") // works } type Level3 with Level2 { @@validate(URL != null, "URL must be provided") // doesn't work @@ -18,5 +19,6 @@ type Level3 with Level2 { model Foo with Level3 { } `, - ); + ); + }); }); From 2298fc93e385ff6605d0d6f60119df9cd60aadbb Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 16 Oct 2025 09:16:23 -0700 Subject: [PATCH 17/17] refactor(validation): clean up validation functions (#305) * refactor(validation): clean up validation functions * update * address pr comments * update --- TODO.md | 5 +- packages/language/res/stdlib.zmodel | 15 ++-- .../attribute-application-validator.ts | 3 + .../function-invocation-validator.ts | 48 ++++++++++++ .../src/client/crud/validator/index.ts | 37 ++++----- .../src/client/crud/validator/utils.ts | 52 +++++++++---- .../orm/validation/custom-validation.test.ts | 76 +++++++++++++++++-- tests/e2e/orm/validation/toplevel.test.ts | 22 ++++++ 8 files changed, 211 insertions(+), 47 deletions(-) diff --git a/TODO.md b/TODO.md index cd7e8eb8..49ae0537 100644 --- a/TODO.md +++ b/TODO.md @@ -56,7 +56,6 @@ - [x] Array update - [x] Strict typing for checked/unchecked input - [x] Upsert - - [ ] Implement with "on conflict" - [x] Delete - [x] Aggregation - [x] Count @@ -86,7 +85,7 @@ - [ ] Global omit - [ ] DbNull vs JsonNull - [ ] Migrate to tsdown - - [ ] @default validation + - [x] @default validation - [ ] Benchmark - [x] Plugin - [x] Post-mutation hooks should be called after transaction is committed @@ -96,7 +95,7 @@ - [x] ZModel - [x] Runtime - [x] Typing -- [ ] Validation +- [x] Validation - [ ] Access Policy - [ ] Short-circuit pre-create check for scalar-field only policies - [x] Inject "on conflict do update" diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index c49f2606..7ac57ba3 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -48,6 +48,7 @@ enum AttributeTargetField { BytesField ModelField TypeDefField + ListField } /** @@ -486,9 +487,9 @@ attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma ////////////////////////////////////////////// /** - * Validates length of a string field. + * Validates length of a string field or list field. */ -attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation +attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField, ListField]) @@@validation /** * Validates a string field value starts with the given text. @@ -566,9 +567,9 @@ attribute @lte(_ value: Any, _ message: String?) @@@targetField([IntField, Float attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation /** - * Validates length of a string field. + * Returns the length of a string field or a list field. */ -function length(field: String, min: Int, max: Int?): Boolean { +function length(field: Any): Int { } @@@expressionContext([ValidationRule]) @@ -581,19 +582,19 @@ function regex(field: String, regex: String): Boolean { /** * Validates a string field value is a valid email address. */ -function email(field: String): Boolean { +function isEmail(field: String): Boolean { } @@@expressionContext([ValidationRule]) /** * Validates a string field value is a valid ISO datetime. */ -function datetime(field: String): Boolean { +function isDateTime(field: String): Boolean { } @@@expressionContext([ValidationRule]) /** * Validates a string field value is a valid url. */ -function url(field: String): Boolean { +function isUrl(field: String): Boolean { } @@@expressionContext([ValidationRule]) ////////////////////////////////////////////// diff --git a/packages/language/src/validators/attribute-application-validator.ts b/packages/language/src/validators/attribute-application-validator.ts index d1319cf0..981eb814 100644 --- a/packages/language/src/validators/attribute-application-validator.ts +++ b/packages/language/src/validators/attribute-application-validator.ts @@ -491,6 +491,9 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataField) { case 'TypeDefField': allowed = allowed || isTypeDef(targetDecl.type.reference?.ref); break; + case 'ListField': + allowed = allowed || (!isDataModel(targetDecl.type.reference?.ref) && targetDecl.type.array); + break; default: break; } diff --git a/packages/language/src/validators/function-invocation-validator.ts b/packages/language/src/validators/function-invocation-validator.ts index ae759904..a740b86e 100644 --- a/packages/language/src/validators/function-invocation-validator.ts +++ b/packages/language/src/validators/function-invocation-validator.ts @@ -13,6 +13,7 @@ import { isDataFieldAttribute, isDataModel, isDataModelAttribute, + isStringLiteral, } from '../generated/ast'; import { getFunctionExpressionContext, @@ -183,6 +184,53 @@ export default class FunctionInvocationValidator implements AstValidator { let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); if (fieldDef.array) { + fieldSchema = addListValidation(fieldSchema.array(), fieldDef.attributes); fieldSchema = z .union([ - z.array(fieldSchema), + fieldSchema, z.strictObject({ - set: z.array(fieldSchema), + set: fieldSchema, }), ]) .optional(); @@ -1165,14 +1167,14 @@ export class InputValidator { uncheckedVariantFields[field] = fieldSchema; } } else { - let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes).optional(); + let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); if (this.isNumericField(fieldDef)) { fieldSchema = z.union([ fieldSchema, z .object({ - set: this.nullableIf(z.number().optional(), !!fieldDef.optional), + set: this.nullableIf(z.number().optional(), !!fieldDef.optional).optional(), increment: z.number().optional(), decrement: z.number().optional(), multiply: z.number().optional(), @@ -1186,26 +1188,25 @@ export class InputValidator { } if (fieldDef.array) { - fieldSchema = z - .union([ - fieldSchema.array(), - z - .object({ - set: z.array(fieldSchema).optional(), - push: this.orArray(fieldSchema, true).optional(), - }) - .refine( - (v) => Object.keys(v).length === 1, - 'Only one of "set", "push" can be provided', - ), - ]) - .optional(); + const arraySchema = addListValidation(fieldSchema.array(), fieldDef.attributes); + fieldSchema = z.union([ + arraySchema, + z + .object({ + set: arraySchema.optional(), + push: z.union([fieldSchema, fieldSchema.array()]).optional(), + }) + .refine((v) => Object.keys(v).length === 1, 'Only one of "set", "push" can be provided'), + ]); } if (fieldDef.optional) { fieldSchema = fieldSchema.nullable(); } + // all fields are optional in update + fieldSchema = fieldSchema.optional(); + uncheckedVariantFields[field] = fieldSchema; if (!fieldDef.foreignKeyFor) { // non-fk field diff --git a/packages/runtime/src/client/crud/validator/utils.ts b/packages/runtime/src/client/crud/validator/utils.ts index 1fdecb25..980fff50 100644 --- a/packages/runtime/src/client/crud/validator/utils.ts +++ b/packages/runtime/src/client/crud/validator/utils.ts @@ -203,6 +203,32 @@ export function addDecimalValidation( return result; } +export function addListValidation( + schema: z.ZodArray, + attributes: AttributeApplication[] | undefined, +): z.ZodSchema { + if (!attributes || attributes.length === 0) { + return schema; + } + + let result = schema; + for (const attr of attributes) { + match(attr.name) + .with('@length', () => { + const min = getArgValue(attr.args?.[0]?.value); + if (min !== undefined) { + result = result.min(min); + } + const max = getArgValue(attr.args?.[1]?.value); + if (max !== undefined) { + result = result.max(max); + } + }) + .otherwise(() => {}); + } + return result; +} + export function addCustomValidation(schema: z.ZodSchema, attributes: AttributeApplication[] | undefined): z.ZodSchema { const attrs = attributes?.filter((a) => a.name === '@@validate'); if (!attrs || attrs.length === 0) { @@ -329,17 +355,11 @@ function evalCall(data: any, expr: CallExpression) { if (fieldArg === undefined || fieldArg === null) { return false; } - invariant(typeof fieldArg === 'string', `"${f}" first argument must be a string`); - - const min = getArgValue(expr.args?.[1]); - const max = getArgValue(expr.args?.[2]); - if (min !== undefined && fieldArg.length < min) { - return false; - } - if (max !== undefined && fieldArg.length > max) { - return false; - } - return true; + invariant( + typeof fieldArg === 'string' || Array.isArray(fieldArg), + `"${f}" first argument must be a string or a list`, + ); + return fieldArg.length; }) .with(P.union('startsWith', 'endsWith', 'contains'), (f) => { if (fieldArg === undefined || fieldArg === null) { @@ -370,11 +390,17 @@ function evalCall(data: any, expr: CallExpression) { invariant(pattern !== undefined, `"${f}" requires a pattern argument`); return new RegExp(pattern).test(fieldArg); }) - .with(P.union('email', 'url', 'datetime'), (f) => { + .with(P.union('isEmail', 'isUrl', 'isDateTime'), (f) => { if (fieldArg === undefined || fieldArg === null) { return false; } - return z.string()[f]().safeParse(fieldArg).success; + invariant(typeof fieldArg === 'string', `"${f}" first argument must be a string`); + const fn = match(f) + .with('isEmail', () => 'email' as const) + .with('isUrl', () => 'url' as const) + .with('isDateTime', () => 'datetime' as const) + .exhaustive(); + return z.string()[fn]().safeParse(fieldArg).success; }) // list functions .with(P.union('has', 'hasEvery', 'hasSome'), (f) => { diff --git a/tests/e2e/orm/validation/custom-validation.test.ts b/tests/e2e/orm/validation/custom-validation.test.ts index edd0c00e..955121b0 100644 --- a/tests/e2e/orm/validation/custom-validation.test.ts +++ b/tests/e2e/orm/validation/custom-validation.test.ts @@ -1,4 +1,4 @@ -import { createTestClient } from '@zenstackhq/testtools'; +import { createTestClient, loadSchemaWithError } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; describe('Custom validation tests', () => { @@ -15,9 +15,10 @@ describe('Custom validation tests', () => { int1 Int? list1 Int[] list2 Int[] + list3 Int[] @@validate( - (str1 == null || length(str1, 8, 10)) + (str1 == null || (length(str1) >= 8 && length(str1) <= 10)) && (int1 == null || (int1 > 1 && int1 < 4)), 'invalid fields') @@ -25,15 +26,17 @@ describe('Custom validation tests', () => { @@validate(str2 == null || regex(str2, '^x.*z$'), 'invalid str2') - @@validate(str3 == null || email(str3), 'invalid str3') + @@validate(str3 == null || isEmail(str3), 'invalid str3') - @@validate(str4 == null || url(str4), 'invalid str4') + @@validate(str4 == null || isUrl(str4), 'invalid str4') - @@validate(str5 == null || datetime(str5), 'invalid str5') + @@validate(str5 == null || isDateTime(str5), 'invalid str5') @@validate(list1 == null || (has(list1, 1) && hasSome(list1, [2, 3]) && hasEvery(list1, [4, 5])), 'invalid list1') @@validate(list2 == null || isEmpty(list2), 'invalid list2', ['x', 'y']) + + @@validate(list3 == null || length(list3) <2 , 'invalid list3') } `, { provider: 'postgresql' }, @@ -93,6 +96,9 @@ describe('Custom validation tests', () => { } expect(thrown).toBe(true); + // validates list length + await expect(_t({ list3: [1, 2] })).toBeRejectedByValidation(['invalid list3']); + // satisfies all await expect( _t({ @@ -104,6 +110,7 @@ describe('Custom validation tests', () => { int1: 2, list1: [1, 2, 4, 5], list2: [], + list3: [1], }), ).toResolveTruthy(); } @@ -115,7 +122,7 @@ describe('Custom validation tests', () => { model User { id Int @id @default(autoincrement()) email String @unique @email - @@validate(length(email, 8)) + @@validate(length(email) >= 8) @@allow('all', true) } `, @@ -170,4 +177,61 @@ describe('Custom validation tests', () => { }), ).toBeRejectedByValidation(); }); + + it('checks arg type for validation functions', async () => { + // length() on relation field + await loadSchemaWithError( + ` + model Foo { + id Int @id @default(autoincrement()) + bars Bar[] + @@validate(length(bars) > 0) + } + + model Bar { + id Int @id @default(autoincrement()) + foo Foo @relation(fields: [fooId], references: [id]) + fooId Int + } + `, + 'argument must be a string or list field', + ); + + // length() on non-string/list field + await loadSchemaWithError( + ` + model Foo { + id Int @id @default(autoincrement()) + x Int + @@validate(length(x) > 0) + } + `, + 'argument must be a string or list field', + ); + + // invalid regex pattern + await loadSchemaWithError( + ` + model Foo { + id Int @id @default(autoincrement()) + x String + @@validate(regex(x, '[abc')) + } + `, + 'invalid regular expression', + ); + + // using field as regex pattern + await loadSchemaWithError( + ` + model Foo { + id Int @id @default(autoincrement()) + x String + y String + @@validate(regex(x, y)) + } + `, + 'second argument must be a string literal', + ); + }); }); diff --git a/tests/e2e/orm/validation/toplevel.test.ts b/tests/e2e/orm/validation/toplevel.test.ts index a7d76475..f4204b62 100644 --- a/tests/e2e/orm/validation/toplevel.test.ts +++ b/tests/e2e/orm/validation/toplevel.test.ts @@ -171,6 +171,28 @@ describe('Toplevel field validation tests', () => { await expect(db.foo.create({ data: { int1: '3.3', int2: new Decimal(3.9) } })).toResolveTruthy(); }); + it('works with list fields', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + list1 Int[] @length(2, 4) + } + `, + { provider: 'postgresql' }, + ); + + await expect(db.foo.create({ data: { id: 1, list1: [1] } })).toBeRejectedByValidation(); + + await expect(db.foo.create({ data: { id: 1, list1: [1, 2, 3, 4, 5] } })).toBeRejectedByValidation(); + + await expect(db.foo.create({ data: { id: 1, list1: [1, 2, 3] } })).toResolveTruthy(); + + await expect(db.foo.update({ where: { id: 1 }, data: { list1: [1] } })).toBeRejectedByValidation(); + await expect(db.foo.update({ where: { id: 1 }, data: { list1: [1, 2, 3, 4, 5] } })).toBeRejectedByValidation(); + await expect(db.foo.update({ where: { id: 1 }, data: { list1: [2, 3, 4] } })).toResolveTruthy(); + }); + it('rejects accessing relation fields', async () => { await loadSchemaWithError( `