From 420df87bf15638507c1e873ffe9b939ed4e8cd3b Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 24 Apr 2024 21:19:41 +0800 Subject: [PATCH 1/2] merge v2 to dev (#1281) Co-authored-by: Augustin <43639468+Azzerty23@users.noreply.github.com> Co-authored-by: Jonathan Stevens Co-authored-by: Jonathan S Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ErikMCM <70036542+ErikMCM@users.noreply.github.com> Co-authored-by: Jason Kleinberg Co-authored-by: Jonathan S Co-authored-by: Jiasheng --- .eslintrc.json | 4 +- .github/dependabot.yml | 118 ++ .github/release/.release-manifest.json | 14 + .github/release/release-main-config.json | 63 + .github/workflows/build-test.yml | 23 +- .github/workflows/codeql.yml | 75 + .github/workflows/integration-test.yml | 104 ++ .github/workflows/management-changelog.yml | 58 + .github/workflows/regression-test.yml | 104 ++ .../security-defender-for-devops.yml | 66 + .../workflows/security-dependency-review.yml | 33 + .github/workflows/security-ossar.yml | 74 + .github/workflows/security-scorecard.yml | 77 + CONTRIBUTING.md | 4 +- README.md | 2 +- package.json | 22 +- packages/ide/jetbrains/CHANGELOG.md | 14 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 30 +- packages/language/CHANGELOG.md | 8 + packages/language/package.json | 2 +- packages/language/src/ast.ts | 29 +- packages/misc/redwood/CHANGELOG.md | 8 + packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/CHANGELOG.md | 8 + packages/plugins/openapi/package.json | 3 +- .../plugins/openapi/src/generator-base.ts | 14 +- packages/plugins/openapi/src/index.ts | 14 +- .../plugins/openapi/src/rest-generator.ts | 17 +- packages/plugins/openapi/src/rpc-generator.ts | 21 +- .../tests/baseline/rest-3.0.0.baseline.yaml | 76 +- .../tests/baseline/rest-3.1.0.baseline.yaml | 76 +- .../rest-type-coverage-3.0.0.baseline.yaml | 1604 ++++++++-------- .../rest-type-coverage-3.1.0.baseline.yaml | 20 +- .../tests/baseline/rpc-3.0.0.baseline.yaml | 815 +++++--- .../tests/baseline/rpc-3.1.0.baseline.yaml | 769 +++++--- .../rpc-type-coverage-3.0.0.baseline.yaml | 691 ++++--- .../rpc-type-coverage-3.1.0.baseline.yaml | 625 ++++--- packages/plugins/swr/CHANGELOG.md | 8 + packages/plugins/swr/package.json | 3 +- packages/plugins/swr/src/generator.ts | 88 +- packages/plugins/swr/src/index.ts | 13 +- packages/plugins/swr/src/runtime/index.ts | 103 +- .../plugins/swr/tests/react-hooks.test.tsx | 96 + packages/plugins/swr/tests/swr.test.ts | 57 + packages/plugins/swr/tests/test-model-meta.ts | 62 +- packages/plugins/tanstack-query/CHANGELOG.md | 8 + packages/plugins/tanstack-query/package.json | 5 +- .../plugins/tanstack-query/src/generator.ts | 115 +- packages/plugins/tanstack-query/src/index.ts | 13 +- .../tanstack-query/src/runtime-v5/index.ts | 8 +- .../tanstack-query/src/runtime-v5/react.ts | 38 +- .../tanstack-query/src/runtime-v5/svelte.ts | 24 +- .../tanstack-query/src/runtime/common.ts | 118 +- .../tanstack-query/src/runtime/index.ts | 8 +- .../tanstack-query/src/runtime/react.ts | 26 +- .../tanstack-query/src/runtime/svelte.ts | 26 +- .../plugins/tanstack-query/src/runtime/vue.ts | 42 +- .../tanstack-query/tests/plugin.test.ts | 77 +- .../tests/react-hooks-v5.test.tsx | 181 +- .../tanstack-query/tests/react-hooks.test.tsx | 46 +- .../tanstack-query/tests/test-model-meta.ts | 62 +- packages/plugins/tanstack-query/tsconfig.json | 2 +- packages/plugins/trpc/CHANGELOG.md | 8 + packages/plugins/trpc/package.json | 11 +- packages/plugins/trpc/src/generator.ts | 35 +- packages/plugins/trpc/src/helpers.ts | 9 +- packages/plugins/trpc/src/index.ts | 13 +- packages/plugins/trpc/src/utils/removeDir.ts | 15 - .../tests/projects/t3-trpc-v10/.gitignore | 3 +- .../tests/projects/t3-trpc-v10/package.json | 4 +- .../projects/t3-trpc-v10/prisma/schema.prisma | 30 +- .../api/routers/generated/client/next.ts | 2 +- .../api/routers/generated/client/utils.ts | 12 +- .../server/api/routers/generated/helper.ts | 4 +- .../routers/generated/routers/Post.router.ts | 649 +++---- .../routers/generated/routers/User.router.ts | 649 +++---- .../api/routers/generated/routers/index.ts | 31 +- packages/plugins/trpc/tests/trpc.test.ts | 76 +- packages/runtime/CHANGELOG.md | 8 + packages/runtime/package.json | 34 +- packages/runtime/res/enhance.d.ts | 1 + packages/runtime/res/enhance.js | 10 + packages/runtime/res/model-meta.d.ts | 1 + packages/runtime/res/model-meta.js | 10 + packages/runtime/res/models.d.ts | 1 + packages/runtime/src/constants.ts | 7 +- packages/runtime/src/cross/model-meta.ts | 90 +- .../runtime/src/cross/nested-write-visitor.ts | 8 +- packages/runtime/src/cross/query-analyzer.ts | 18 +- packages/runtime/src/cross/utils.ts | 20 +- packages/runtime/src/edge.ts | 1 + packages/runtime/src/enhance.d.ts | 2 + .../src/enhancements/create-enhancement.ts | 171 ++ .../runtime/src/enhancements/default-auth.ts | 146 ++ packages/runtime/src/enhancements/delegate.ts | 1187 ++++++++++++ packages/runtime/src/enhancements/enhance.ts | 52 - packages/runtime/src/enhancements/index.ts | 7 +- .../src/enhancements/{policy => }/logger.ts | 10 +- packages/runtime/src/enhancements/omit.ts | 32 +- packages/runtime/src/enhancements/password.ts | 46 +- .../src/enhancements/policy/handler.ts | 975 +++++----- .../runtime/src/enhancements/policy/index.ts | 118 +- .../src/enhancements/policy/policy-utils.ts | 350 +--- .../src/enhancements/policy/promise.ts | 38 - packages/runtime/src/enhancements/preset.ts | 22 - packages/runtime/src/enhancements/promise.ts | 99 + packages/runtime/src/enhancements/proxy.ts | 143 +- .../runtime/src/enhancements/query-utils.ts | 209 +++ packages/runtime/src/enhancements/types.ts | 6 +- packages/runtime/src/enhancements/utils.ts | 107 +- packages/runtime/src/index.ts | 2 +- packages/runtime/src/loader.ts | 88 - packages/runtime/src/package.json | 1 + packages/runtime/src/types.ts | 12 +- packages/runtime/src/version.ts | 43 +- packages/schema/package.json | 26 +- packages/schema/src/cli/actions/format.ts | 9 +- packages/schema/src/cli/cli-util.ts | 93 +- packages/schema/src/cli/index.ts | 8 +- packages/schema/src/cli/plugin-runner.ts | 264 +-- packages/schema/src/extension.ts | 2 +- .../validator/datamodel-validator.ts | 74 +- .../validator/datasource-validator.ts | 2 +- .../validator/enum-validator.ts | 2 +- .../validator/expression-validator.ts | 49 +- .../function-invocation-validator.ts | 9 +- .../validator/schema-validator.ts | 6 +- .../src/language-server/validator/utils.ts | 6 +- .../src/language-server/zmodel-code-action.ts | 11 +- .../zmodel-completion-provider.ts | 4 +- .../src/language-server/zmodel-formatter.ts | 87 +- .../src/language-server/zmodel-linker.ts | 119 +- .../src/language-server/zmodel-module.ts | 2 +- .../src/language-server/zmodel-scope.ts | 221 ++- .../schema/src/plugins/access-policy/index.ts | 10 - .../enhancer/enhance/auth-type-generator.ts | 151 ++ .../src/plugins/enhancer/enhance/index.ts | 584 ++++++ .../src/plugins/enhancer/enhancer-utils.ts | 20 + packages/schema/src/plugins/enhancer/index.ts | 41 + .../src/plugins/enhancer/model-meta/index.ts | 17 + .../policy}/expression-writer.ts | 113 +- .../src/plugins/enhancer/policy/index.ts | 8 + .../policy}/policy-guard-generator.ts | 62 +- .../schema/src/plugins/model-meta/index.ts | 42 - packages/schema/src/plugins/plugin-utils.ts | 59 +- packages/schema/src/plugins/prisma/index.ts | 98 +- .../src/plugins/prisma/prisma-builder.ts | 9 +- .../src/plugins/prisma/schema-generator.ts | 450 ++++- packages/schema/src/plugins/zod/generator.ts | 896 +++++---- packages/schema/src/plugins/zod/index.ts | 6 +- .../schema/src/plugins/zod/transformer.ts | 86 +- packages/schema/src/plugins/zod/types.ts | 6 +- .../schema/src/plugins/zod/utils/removeDir.ts | 15 - .../src/plugins/zod/utils/schema-gen.ts | 26 +- packages/schema/src/res/stdlib.zmodel | 9 +- packages/schema/src/telemetry.ts | 8 +- packages/schema/src/utils/ast-utils.ts | 143 +- packages/schema/src/utils/exec-utils.ts | 14 +- packages/schema/src/utils/pkg-utils.ts | 22 +- .../tests/generator/expression-writer.test.ts | 2 +- .../tests/generator/prisma-builder.test.ts | 2 +- .../tests/generator/prisma-generator.test.ts | 71 +- packages/schema/tests/schema/abstract.test.ts | 20 + packages/schema/tests/schema/cal-com.zmodel | 9 +- .../schema/tests/schema/formatter.test.ts | 2 +- .../validation/attribute-validation.test.ts | 30 +- .../validation/datamodel-validation.test.ts | 195 +- packages/schema/tests/utils.ts | 12 +- packages/sdk/CHANGELOG.md | 8 + packages/sdk/package.json | 28 +- packages/sdk/src/code-gen.ts | 31 +- .../sdk/src/dmmf-helpers/aggregate-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/include-helpers.ts | 6 +- .../src/dmmf-helpers/missing-types-helper.ts | 2 +- .../sdk/src/dmmf-helpers/model-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/modelArgs-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/select-helpers.ts | 4 +- packages/sdk/src/dmmf-helpers/types.ts | 10 +- packages/sdk/src/index.ts | 2 +- packages/sdk/src/model-meta-generator.ts | 389 ++-- packages/sdk/src/prisma.ts | 115 +- packages/sdk/src/types.ts | 50 +- .../src}/typescript-expression-transformer.ts | 4 +- packages/sdk/src/utils.ts | 195 +- packages/server/CHANGELOG.md | 8 + packages/server/README.md | 4 +- packages/server/package.json | 10 +- packages/server/src/api/base.ts | 7 +- packages/server/src/api/index.ts | 2 + packages/server/src/api/rest/index.ts | 38 +- packages/server/src/api/rpc/index.ts | 13 +- packages/server/src/api/utils.ts | 19 +- packages/server/src/express/index.ts | 2 +- packages/server/src/express/middleware.ts | 11 +- packages/server/src/fastify/index.ts | 2 +- packages/server/src/fastify/plugin.ts | 11 +- packages/server/src/nestjs/index.ts | 1 + packages/server/src/nestjs/zenstack.module.ts | 98 + packages/server/src/next/app-route-handler.ts | 9 +- .../server/src/next/pages-route-handler.ts | 9 +- packages/server/src/shared.ts | 35 +- packages/server/src/sveltekit/handler.ts | 7 +- packages/server/src/sveltekit/index.ts | 2 +- packages/server/src/types.ts | 18 +- packages/server/tests/adapter/express.test.ts | 13 +- packages/server/tests/adapter/fastify.test.ts | 8 +- packages/server/tests/adapter/nestjs.test.ts | 163 ++ packages/server/tests/adapter/next.test.ts | 12 +- .../server/tests/adapter/sveltekit.test.ts | 12 +- packages/server/tests/api/rest.test.ts | 16 +- packages/server/tests/api/rpc.test.ts | 19 +- packages/server/tests/utils.ts | 4 +- packages/testtools/CHANGELOG.md | 8 + packages/testtools/package.json | 4 +- .../testtools/src}/jest-ext.ts | 1 + packages/testtools/src/model.ts | 4 +- packages/testtools/src/schema.ts | 155 +- pnpm-lock.yaml | 1643 +++++++---------- script/test-prisma-v5.sh | 3 - script/test-scaffold.ts | 2 +- test-setup.ts | 9 + tests/integration/test-run/package.json | 4 +- tests/integration/test-setup.ts | 2 +- tests/integration/tests/cli/config.test.ts | 67 - tests/integration/tests/cli/format.test.ts | 26 + tests/integration/tests/cli/generate.test.ts | 34 +- tests/integration/tests/cli/init.test.ts | 37 +- tests/integration/tests/cli/plugins.test.ts | 54 +- .../with-delegate/enhanced-client.test.ts | 1198 ++++++++++++ .../with-delegate/issue-1058.test.ts | 53 + .../with-delegate/issue-1064.test.ts | 291 +++ .../with-delegate/issue-1100.test.ts | 69 + .../with-delegate/issue-1123.test.ts | 47 + .../with-delegate/issue-1135.test.ts | 85 + .../with-delegate/issue-1149.test.ts | 112 ++ .../with-delegate/issue-1243.test.ts | 55 + .../with-delegate/plugin-interaction.test.ts | 25 + .../with-delegate/policy-interaction.test.ts | 217 +++ .../tests/enhancements/with-delegate/utils.ts | 75 + .../enhancements/with-omit/with-omit.test.ts | 21 +- .../with-password/with-password.test.ts | 27 +- .../enhancements/with-policy/auth.test.ts | 499 ++++- .../with-policy/client-extensions.test.ts | 57 +- .../with-policy/connect-disconnect.test.ts | 20 +- .../with-policy/deep-nested.test.ts | 2 +- .../with-policy/empty-policy.test.ts | 12 +- .../with-policy/field-comparison.test.ts | 10 +- .../with-policy/field-level-policy.test.ts | 123 +- .../with-policy/field-validation.test.ts | 155 +- .../with-policy/fluent-api.test.ts | 192 +- .../with-policy/multi-field-unique.test.ts | 12 +- .../with-policy/nested-to-many.test.ts | 40 +- .../with-policy/nested-to-one.test.ts | 40 +- .../enhancements/with-policy/options.test.ts | 50 +- .../with-policy/petstore-sample.test.ts | 4 +- .../with-policy/post-update.test.ts | 44 +- .../with-policy/query-reduction.test.ts | 6 +- .../enhancements/with-policy/refactor.test.ts | 4 +- .../relation-many-to-many-filter.test.ts | 12 +- .../relation-one-to-many-filter.test.ts | 16 +- .../relation-one-to-one-filter.test.ts | 12 +- .../with-policy/self-relation.test.ts | 12 +- .../with-policy/subscription.test.ts | 18 +- .../with-policy/todo-sample.test.ts | 4 +- .../with-policy/toplevel-operations.test.ts | 16 +- .../with-policy/unique-as-id.test.ts | 12 +- .../enhancements/with-policy/view.test.ts | 4 +- .../nextjs/test-project/package.json | 4 +- .../tests/frameworks/trpc/generation.test.ts | 4 +- .../frameworks/trpc/test-project/package.json | 4 +- .../frameworks/trpc/test-project/todo.zmodel | 10 - .../integration/tests/misc/stacktrace.test.ts | 6 +- tests/integration/tests/plugins/zod.test.ts | 124 +- .../integration/tests/schema/petstore.zmodel | 1 - tests/integration/tests/schema/todo.zmodel | 1 - tests/integration/tsconfig.json | 2 +- tests/integration/utils/index.ts | 1 - tests/regression/.eslintrc.json | 13 + tests/regression/jest.config.ts | 10 + tests/regression/jest.d.ts | 16 + tests/regression/package.json | 24 + tests/regression/test-setup.ts | 17 + .../tests}/issue-1014.test.ts | 0 .../tests}/issue-1078.test.ts | 0 .../tests}/issue-1080.test.ts | 0 .../tests}/issue-1129.test.ts | 0 tests/regression/tests/issue-1167.test.ts | 20 + tests/regression/tests/issue-1179.test.ts | 27 + tests/regression/tests/issue-1186.test.ts | 51 + tests/regression/tests/issue-1210.test.ts | 92 + .../tests}/issue-1235.test.ts | 0 .../tests}/issue-1241.test.ts | 68 +- tests/regression/tests/issue-1257.test.ts | 53 + tests/regression/tests/issue-1265.test.ts | 27 + tests/regression/tests/issue-1268.test.ts | 32 + .../tests}/issue-1271.test.ts | 0 .../tests}/issue-177.test.ts | 0 .../tests}/issue-416.test.ts | 0 .../tests}/issue-646.test.ts | 0 .../tests}/issue-657.test.ts | 0 .../tests}/issue-665.test.ts | 10 +- .../tests}/issue-674.test.ts | 0 .../tests}/issue-689.test.ts | 0 .../tests}/issue-703.test.ts | 0 .../tests}/issue-714.test.ts | 0 .../tests}/issue-724.test.ts | 0 .../tests}/issue-735.test.ts | 0 .../tests}/issue-744.test.ts | 0 .../tests}/issue-756.test.ts | 2 +- .../tests}/issue-764.test.ts | 0 .../tests}/issue-765.test.ts | 0 .../tests}/issue-804.test.ts | 0 .../tests}/issue-811.test.ts | 0 .../tests}/issue-814.test.ts | 0 .../tests}/issue-825.test.ts | 0 .../tests}/issue-864.test.ts | 0 .../tests}/issue-886.test.ts | 4 +- .../tests}/issue-925.test.ts | 21 +- .../tests}/issue-947.test.ts | 0 .../tests}/issue-961.test.ts | 0 .../tests}/issue-965.test.ts | 0 .../tests}/issue-971.test.ts | 0 .../tests}/issue-992.test.ts | 0 .../tests}/issues.test.ts | 58 +- tests/regression/tsconfig.json | 12 + 326 files changed, 16692 insertions(+), 7905 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/release/.release-manifest.json create mode 100644 .github/release/release-main-config.json create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/integration-test.yml create mode 100644 .github/workflows/management-changelog.yml create mode 100644 .github/workflows/regression-test.yml create mode 100644 .github/workflows/security-defender-for-devops.yml create mode 100644 .github/workflows/security-dependency-review.yml create mode 100644 .github/workflows/security-ossar.yml create mode 100644 .github/workflows/security-scorecard.yml create mode 100644 packages/language/CHANGELOG.md create mode 100644 packages/misc/redwood/CHANGELOG.md create mode 100644 packages/plugins/openapi/CHANGELOG.md create mode 100644 packages/plugins/swr/CHANGELOG.md create mode 100644 packages/plugins/tanstack-query/CHANGELOG.md create mode 100644 packages/plugins/trpc/CHANGELOG.md delete mode 100644 packages/plugins/trpc/src/utils/removeDir.ts create mode 100644 packages/runtime/CHANGELOG.md create mode 100644 packages/runtime/res/enhance.d.ts create mode 100644 packages/runtime/res/enhance.js create mode 100644 packages/runtime/res/model-meta.d.ts create mode 100644 packages/runtime/res/model-meta.js create mode 100644 packages/runtime/res/models.d.ts create mode 120000 packages/runtime/src/edge.ts create mode 100644 packages/runtime/src/enhance.d.ts create mode 100644 packages/runtime/src/enhancements/create-enhancement.ts create mode 100644 packages/runtime/src/enhancements/default-auth.ts create mode 100644 packages/runtime/src/enhancements/delegate.ts delete mode 100644 packages/runtime/src/enhancements/enhance.ts rename packages/runtime/src/enhancements/{policy => }/logger.ts (75%) delete mode 100644 packages/runtime/src/enhancements/policy/promise.ts delete mode 100644 packages/runtime/src/enhancements/preset.ts create mode 100644 packages/runtime/src/enhancements/promise.ts create mode 100644 packages/runtime/src/enhancements/query-utils.ts delete mode 100644 packages/runtime/src/loader.ts create mode 120000 packages/runtime/src/package.json delete mode 100644 packages/schema/src/plugins/access-policy/index.ts create mode 100644 packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts create mode 100644 packages/schema/src/plugins/enhancer/enhance/index.ts create mode 100644 packages/schema/src/plugins/enhancer/enhancer-utils.ts create mode 100644 packages/schema/src/plugins/enhancer/index.ts create mode 100644 packages/schema/src/plugins/enhancer/model-meta/index.ts rename packages/schema/src/plugins/{access-policy => enhancer/policy}/expression-writer.ts (87%) create mode 100644 packages/schema/src/plugins/enhancer/policy/index.ts rename packages/schema/src/plugins/{access-policy => enhancer/policy}/policy-guard-generator.ts (94%) delete mode 100644 packages/schema/src/plugins/model-meta/index.ts delete mode 100644 packages/schema/src/plugins/zod/utils/removeDir.ts create mode 100644 packages/sdk/CHANGELOG.md rename packages/{schema/src/utils => sdk/src}/typescript-expression-transformer.ts (99%) create mode 100644 packages/server/CHANGELOG.md create mode 100644 packages/server/src/api/index.ts create mode 100644 packages/server/src/nestjs/index.ts create mode 100644 packages/server/src/nestjs/zenstack.module.ts create mode 100644 packages/server/tests/adapter/nestjs.test.ts create mode 100644 packages/testtools/CHANGELOG.md rename {tests/integration/utils => packages/testtools/src}/jest-ext.ts (98%) delete mode 100755 script/test-prisma-v5.sh create mode 100644 test-setup.ts delete mode 100644 tests/integration/tests/cli/config.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1064.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1123.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1135.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1149.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1243.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/utils.ts delete mode 100644 tests/integration/utils/index.ts create mode 100644 tests/regression/.eslintrc.json create mode 100644 tests/regression/jest.config.ts create mode 100644 tests/regression/jest.d.ts create mode 100644 tests/regression/package.json create mode 100644 tests/regression/test-setup.ts rename tests/{integration/tests/regression => regression/tests}/issue-1014.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1078.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1080.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1129.test.ts (100%) create mode 100644 tests/regression/tests/issue-1167.test.ts create mode 100644 tests/regression/tests/issue-1179.test.ts create mode 100644 tests/regression/tests/issue-1186.test.ts create mode 100644 tests/regression/tests/issue-1210.test.ts rename tests/{integration/tests/regression => regression/tests}/issue-1235.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1241.test.ts (57%) create mode 100644 tests/regression/tests/issue-1257.test.ts create mode 100644 tests/regression/tests/issue-1265.test.ts create mode 100644 tests/regression/tests/issue-1268.test.ts rename tests/{integration/tests/regression => regression/tests}/issue-1271.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-177.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-416.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-646.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-657.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-665.test.ts (77%) rename tests/{integration/tests/regression => regression/tests}/issue-674.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-689.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-703.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-714.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-724.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-735.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-744.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-756.test.ts (90%) rename tests/{integration/tests/regression => regression/tests}/issue-764.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-765.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-804.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-811.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-814.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-825.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-864.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-886.test.ts (84%) rename tests/{integration/tests/regression => regression/tests}/issue-925.test.ts (85%) rename tests/{integration/tests/regression => regression/tests}/issue-947.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-961.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-965.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-971.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-992.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issues.test.ts (93%) create mode 100644 tests/regression/tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index e04b04831..707715244 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,7 @@ "plugin:jest/recommended" ], "rules": { - "jest/expect-expect": "off", - "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }] + "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }], + "jest/expect-expect": "off" } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..59ef81ad3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,118 @@ +version: 2 +updates: + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/ide/jetbrains' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/language' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/misc/redwood' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/plugins/openapi' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/plugins/swr' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/plugins/tanstack-query' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/plugins/trpc' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/runtime' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/sdk' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/server' + + - package-ecosystem: 'npm' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/packages/testtools' + + - package-ecosystem: 'github-actions' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/' diff --git a/.github/release/.release-manifest.json b/.github/release/.release-manifest.json new file mode 100644 index 000000000..1857a084d --- /dev/null +++ b/.github/release/.release-manifest.json @@ -0,0 +1,14 @@ +{ + ".": "2.0.0-alpha.1", + "packages/ide/jetbrains": "2.0.0-alpha.2", + "packages/language": "2.0.0-alpha.2", + "packages/misc/redwood": "2.0.0-alpha.2", + "packages/plugins/openapi": "2.0.0-alpha.2", + "packages/plugins/swr": "2.0.0-alpha.2", + "packages/plugins/tanstack-query": "2.0.0-alpha.2", + "packages/plugins/trpc": "2.0.0-alpha.2", + "packages/runtime": "2.0.0-alpha.2", + "packages/sdk": "2.0.0-alpha.2", + "packages/server": "2.0.0-alpha.2", + "packages/testtools": "2.0.0-alpha.2" +} \ No newline at end of file diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json new file mode 100644 index 000000000..57e614a77 --- /dev/null +++ b/.github/release/release-main-config.json @@ -0,0 +1,63 @@ +{ + "packages": { + ".": { + "package-name": "zenstack-monorepo", + "component": "Monorepo", + "exclude-paths": ["tests", ".github"] + }, + "packages/ide/jetbrains": { + "package-name": "jetbrains", + "component": "JetBrains_IDE" + }, + "packages/language": { + "package-name": "@zenstackhq/language", + "component": "Language" + }, + "packages/misc/redwood": { + "package-name": "@zenstackhq/redwood", + "component": "Redwood" + }, + "packages/plugins/openapi": { + "package-name": "@zenstackhq/openapi", + "component": "OpenAPI_Plugin" + }, + "packages/plugins/swr": { + "package-name": "@zenstackhq/swr", + "component": "SWR_Plugin" + }, + "packages/plugins/tanstack-query": { + "package-name": "@zenstackhq/tanstack-query", + "component": "Tanstack_Query_Plugin" + }, + "packages/plugins/trpc": { + "package-name": "@zenstackhq/trpc", + "component": "tRPC_Plugin" + }, + "packages/runtime": { + "package-name": "@zenstackhq/runtime", + "component": "Runtime" + }, + "packages/sdk": { + "package-name": "@zenstackhq/sdk", + "component": "SDK" + }, + "packages/server": { + "package-name": "@zenstackhq/server", + "component": "Server" + }, + "packages/testtools": { + "package-name": "@zenstackhq/testtools", + "component": "Test_Tools" + } + }, + "include-component-in-tag": false, + "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please).", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "sequential-calls": true, + "separate-pull-requests": false, + "versioning": "prerelease", + "release-type": "node", + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 404f42d3a..368551623 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,8 +8,22 @@ env: DO_NOT_TRACK: '1' on: + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 pull_request: - branches: ['dev', 'main'] + branches: + - main + - dev + - release/* + - v2 + +permissions: + contents: read jobs: build-test: @@ -32,18 +46,11 @@ jobs: strategy: matrix: node-version: [20.x] - prisma-version: [v4, v5] steps: - name: Checkout uses: actions/checkout@v3 - - name: Set Prisma Version - if: ${{ matrix.prisma-version == 'v5' }} - shell: bash - run: | - bash ./script/test-prisma-v5.sh - - name: Install pnpm uses: pnpm/action-setup@v2 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..2452a7ca3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,75 @@ +name: Security - CodeQL + +on: + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + schedule: + - cron: '0 0 * * 1' + +permissions: + contents: read + +jobs: + analyze: + permissions: + actions: read + contents: read + security-events: write + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: ['javascript', 'typescript'] + # CodeQL supports [ $supported-codeql-languages ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2.22.12 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2.22.12 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2.22.12 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 000000000..fb3c0bfb0 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,104 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Integration Tests + +env: + TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} + DO_NOT_TRACK: '1' + +on: + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + +permissions: + contents: read + +jobs: + build-test: + runs-on: buildjet-8vcpu-ubuntu-2204 + + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: abc123 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: ^7.15.0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: buildjet/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: buildjet/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Gradle Wrapper Validation + uses: gradle/wrapper-validation-action@v1.1.0 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.4.2 + with: + gradle-home-cache-cleanup: true + + - name: Build + run: DEFAULT_NPM_TAG=latest pnpm run build + + # install again for internal dependencies + - name: Install internal dependencies + run: pnpm install --frozen-lockfile + + - name: Integration Test + run: pnpm run test-scaffold && pnpm run test-integration diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml new file mode 100644 index 000000000..9b6ba1f12 --- /dev/null +++ b/.github/workflows/management-changelog.yml @@ -0,0 +1,58 @@ +on: + push: + branches: [] + # branches: + # - main # Your main branch + # - dev # Your development branch + # - release/* # Your releases branch + # - v2 # Temp V2 integration branch + +permissions: + contents: read + +name: Management - Release Workflow + +jobs: + release: + permissions: + contents: write + pull-requests: write + env: + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + runs-on: ubuntu-latest + steps: + # Harden-Runner provides runtime security for GitHub-hosted and self-hosted environments. + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit + + - uses: google-github-actions/release-please-action@v4 + id: release + with: + config-file: '.github/release/release-main-config.json' + manifest-file: '.github/release/.release-manifest.json' + include-component-in-tag: false + target-branch: ${{ github.ref_name == 'dev' && 'main' || github.ref_name }} + + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.release_created }} + + - uses: pnpm/action-setup@v2 + if: ${{ steps.release.outputs.release_created }} + with: + version: ^7.15.0 + + - uses: actions/setup-node@v4 + if: ${{ steps.release.outputs.release_created }} + with: + node-version: 20.x + registry-url: 'https://registry.npmjs.org' + + - run: pnpm i # Install using pnpm + if: ${{ steps.release.outputs.release_created }} + + - run: pnpm publish-test # Publish using pre-defined pnpm script + if: ${{ steps.release.outputs.release_created }} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/regression-test.yml b/.github/workflows/regression-test.yml new file mode 100644 index 000000000..679cc2b4c --- /dev/null +++ b/.github/workflows/regression-test.yml @@ -0,0 +1,104 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Regression Tests + +env: + TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} + DO_NOT_TRACK: '1' + +on: + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + +permissions: + contents: read + +jobs: + build-test: + runs-on: buildjet-8vcpu-ubuntu-2204 + + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: abc123 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: ^7.15.0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: buildjet/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: buildjet/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Gradle Wrapper Validation + uses: gradle/wrapper-validation-action@v1.1.0 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.4.2 + with: + gradle-home-cache-cleanup: true + + - name: Build + run: DEFAULT_NPM_TAG=latest pnpm run build + + # install again for internal dependencies + - name: Install internal dependencies + run: pnpm install --frozen-lockfile + + - name: Regression Test + run: pnpm run test-scaffold && pnpm run test-regression diff --git a/.github/workflows/security-defender-for-devops.yml b/.github/workflows/security-defender-for-devops.yml new file mode 100644 index 000000000..526cebf1e --- /dev/null +++ b/.github/workflows/security-defender-for-devops.yml @@ -0,0 +1,66 @@ +# Microsoft Security DevOps (MSDO) is a command line application which integrates static analysis tools into the development cycle. +# MSDO installs, configures and runs the latest versions of static analysis tools +# (including, but not limited to, SDL/security and compliance tools). +# +# The Microsoft Security DevOps action is currently in beta and runs on the windows-latest queue, +# as well as Windows self hosted agents. ubuntu-latest support coming soon. +# +# For more information about the action , check out https://github.com/microsoft/security-devops-action +# +# Please note this workflow do not integrate your GitHub Org with Microsoft Defender For DevOps. You have to create an integration +# and provide permission before this can report data back to azure. +# Read the official documentation here : https://learn.microsoft.com/en-us/azure/defender-for-cloud/quickstart-onboard-github + +name: Security - Microsoft Defender For Devops + +on: + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + schedule: + - cron: '34 12 * * 0' + +permissions: + contents: read + security-events: read + +jobs: + MSDO: + # currently only windows latest is supported + runs-on: windows-latest + permissions: + security-events: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit + + # checks out the repository + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: | + 5.0.x + 6.0.x + + - name: Run Microsoft Security DevOps + uses: microsoft/security-devops-action@v1.6.0 + id: msdo + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v2.22.12 + with: + sarif_file: ${{ steps.msdo.outputs.sarifFile }} diff --git a/.github/workflows/security-dependency-review.yml b/.github/workflows/security-dependency-review.yml new file mode 100644 index 000000000..09018a429 --- /dev/null +++ b/.github/workflows/security-dependency-review.yml @@ -0,0 +1,33 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: Security - Dependency Review +on: + merge_group: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit + + # checks out the repository + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + + - name: 'Dependency Review' + uses: actions/dependency-review-action@v2.5.1 diff --git a/.github/workflows/security-ossar.yml b/.github/workflows/security-ossar.yml new file mode 100644 index 000000000..244f2b147 --- /dev/null +++ b/.github/workflows/security-ossar.yml @@ -0,0 +1,74 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow integrates a collection of open source static analysis tools +# with GitHub code scanning. For documentation, or to provide feedback, visit +# https://github.com/github/ossar-action +name: Security - OSSAR + +on: + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + schedule: + - cron: '41 3 * * 5' + +permissions: + contents: read + +jobs: + OSSAR-Scan: + runs-on: windows-latest + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit + + - name: Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v1.8.7 + with: + github_token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + comment_on_pr: false + theme: dark + proc_trace_sys_enable: true + + # checks out the repository + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + + - uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: | + 5.0.x + 6.0.x + + # Run open source static analysis tools + - name: Run OSSAR + uses: github/ossar-action@v1 + id: ossar + + # Upload results to the Security tab + - name: Upload OSSAR results + uses: github/codeql-action/upload-sarif@v2.22.12 + with: + sarif_file: ${{ steps.ossar.outputs.sarifFile }} diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml new file mode 100644 index 000000000..2e8dd159a --- /dev/null +++ b/.github/workflows/security-scorecard.yml @@ -0,0 +1,77 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Security - Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '21 9 * * 6' + push: + branches: + - main + - dev + +# Declare default permissions as read only. +permissions: + contents: read + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit + + - name: Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v1.8.7 + with: + github_token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + comment_on_pr: false + theme: dark + proc_trace_sys_enable: true + + # checks out the repository + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + + - name: 'Run analysis' + uses: ossf/scorecard-action@v2.1.2 + with: + results_file: results.sarif + results_format: sarif + repo_token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: 'Upload artifact' + uses: actions/upload-artifact@v3.1.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: 'Upload to code-scanning' + uses: github/codeql-action/upload-sarif@v2.2.4 + with: + sarif_file: results.sarif diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97319e08c..659fe2184 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,11 +71,11 @@ The ZModel language's definition, including its syntax definition and parser/lin ### `schema` -The `zenstack` CLI and ZModel VSCode extension implementation. The package also contains several built-in plugins: `@core/prisma`, `@core/model-meta`, `@core/access-policy`, and `core/zod`. +The `zenstack` CLI and ZModel VSCode extension implementation. The package also contains several built-in plugins: `@core/prisma`, `@core/enhancer`, and `core/zod`. ### `runtime` -Runtime enhancements to PrismaClient, including infrastructure for creating transparent proxies and concrete implementations for the `withPolicy`, `withPassword`, and `withOmit` proxies. +Runtime enhancements to PrismaClient, including infrastructure for creating transparent proxies and concrete implementations of various proxies. ### `server` diff --git a/README.md b/README.md index 968f5b16a..1e07a1408 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Check out the [Multi-tenant Todo App](https://zenstack-todo.vercel.app/) for a r ### Blog App - [Next.js 13 + Pages Route + SWR](https://github.com/zenstackhq/docs-tutorial-nextjs) -- [Next.js 13 + App Route + SWR](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir) +- [Next.js 13 + App Route + ReactQuery](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir) - [Next.js 13 + App Route + tRPC](https://github.com/zenstackhq/sample-blog-nextjs-app-trpc) - [Nuxt V3 + TanStack Query](https://github.com/zenstackhq/docs-tutorial-nuxt) - [SvelteKit](https://github.com/zenstackhq/docs-tutorial-sveltekit) diff --git a/package.json b/package.json index af774f8c3..56cc21263 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,22 @@ { "name": "zenstack-monorepo", - "version": "1.12.4", + "version": "2.0.0", "description": "", "scripts": { "build": "pnpm -r build", "lint": "pnpm -r lint", "test": "pnpm -r --parallel run test --silent --forceExit", - "test-ci": "pnpm -r --parallel run test --silent --forceExit", + "test-ci": "pnpm -r --parallel run --filter=\"./packages/**\" test --silent --forceExit", + "test-integration": "pnpm run --filter=integration test --silent --forceExit", + "test-regression": "pnpm run --filter=regression test --silent --forceExit", "test-scaffold": "tsx script/test-scaffold.ts", "publish-all": "pnpm --filter \"./packages/**\" -r publish --access public", "publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/", - "unpublish-preview": "pnpm --recursive --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\"" + "unpublish-preview": "pnpm --recursive --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\"", + "publish-next": "pnpm --filter \"./packages/**\" -r publish --access public --tag next", + "publish-preview-next": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/ --tag next", + "unpublish-preview-next": "pnpm --recursive --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ --tag next \"\\$PNPM_PACKAGE_NAME\"", + "publish-test": "pnpm --filter \"./packages/**\" -r publish --access public --tag test" }, "keywords": [], "author": "", @@ -19,12 +25,12 @@ "@changesets/cli": "^2.26.0", "@types/jest": "^29.5.10", "@types/node": "^20.10.2", - "@typescript-eslint/eslint-plugin": "^6.13.1", - "@typescript-eslint/parser": "^6.13.1", + "@typescript-eslint/eslint-plugin": "^7.6.0", + "@typescript-eslint/parser": "^7.6.0", "concurrently": "^7.4.0", "copyfiles": "^2.4.1", - "eslint": "^8.55.0", - "eslint-plugin-jest": "^27.6.0", + "eslint": "^8.56.0", + "eslint-plugin-jest": "^28.2.0", "jest": "^29.7.0", "replace-in-file": "^7.0.1", "rimraf": "^3.0.2", @@ -32,6 +38,6 @@ "ts-node": "^10.9.1", "tsup": "^8.0.1", "tsx": "^4.7.1", - "typescript": "^5.3.2" + "typescript": "^5.4.4" } } diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index 226419eb2..45dfea211 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -1,13 +1,19 @@ # Changelog -## [Unreleased] +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) -### Fixed -- General improvements to language service. +### Miscellaneous Chores -## 1.9.0 +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) +## [Unreleased] +### Added +- Added support to complex usage of `@@index` attribute like `@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)`. +### Fixed +- Fixed several ZModel validation issues related to model inheritance. + +## 1.7.0 ### Added - Added support to complex usage of `@@index` attribute like `@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)`. diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 474b361fc..680a35e24 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.12.4" +version = "2.0.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index c0bf7dcf1..796e92da1 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,17 +1,17 @@ { - "name": "jetbrains", - "version": "1.12.4", - "displayName": "ZenStack JetBrains IDE Plugin", - "description": "ZenStack JetBrains IDE plugin", - "homepage": "https://zenstack.dev", - "private": true, - "scripts": { - "build": "./gradlew buildPlugin" - }, - "author": "ZenStack Team", - "license": "MIT", - "devDependencies": { - "zenstack": "workspace:*", - "@zenstackhq/language": "workspace:*" - } + "name": "jetbrains", + "version": "2.0.0", + "displayName": "ZenStack JetBrains IDE Plugin", + "description": "ZenStack JetBrains IDE plugin", + "homepage": "https://zenstack.dev", + "private": true, + "scripts": { + "build": "./gradlew buildPlugin" + }, + "author": "ZenStack Team", + "license": "MIT", + "devDependencies": { + "zenstack": "workspace:*", + "@zenstackhq/language": "workspace:*" + } } diff --git a/packages/language/CHANGELOG.md b/packages/language/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/language/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/language/package.json b/packages/language/package.json index 3eb95b144..ca23c5bce 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.12.4", + "version": "2.0.0", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/ast.ts b/packages/language/src/ast.ts index c8637115a..3da706a75 100644 --- a/packages/language/src/ast.ts +++ b/packages/language/src/ast.ts @@ -1,7 +1,8 @@ -import { AbstractDeclaration, ExpressionType, BinaryExpr } from './generated/ast'; +import { AstNode } from 'langium'; +import { AbstractDeclaration, BinaryExpr, DataModel, ExpressionType } from './generated/ast'; -export * from './generated/ast'; export { AstNode, Reference } from 'langium'; +export * from './generated/ast'; /** * Shape of type resolution result: an expression type or reference to a declaration @@ -44,16 +45,28 @@ declare module './generated/ast' { $resolvedParam?: AttributeParam; } - interface DataModel { + interface DataModelField { + $inheritedFrom?: DataModel; + } + + interface DataModelAttribute { + $inheritedFrom?: DataModel; + } + + export interface DataModel { /** - * Resolved fields, include inherited fields + * Indicates whether the model is already merged with the base types */ - $resolvedFields: Array; + $baseMerged?: boolean; } +} - interface DataModelField { - $isInherited?: boolean; - } +export interface InheritableNode extends AstNode { + $inheritedFrom?: DataModel; +} + +export interface InheritableNode extends AstNode { + $inheritedFrom?: DataModel; } declare module 'langium' { diff --git a/packages/misc/redwood/CHANGELOG.md b/packages/misc/redwood/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/misc/redwood/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index c5eb9d74d..a5119f967 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.12.4", + "version": "2.0.0", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/CHANGELOG.md b/packages/plugins/openapi/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/plugins/openapi/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 7c03491e1..ac6a46553 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.12.4", + "version": "2.0.0", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { @@ -26,7 +26,6 @@ "author": "ZenStack Team", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "change-case": "^4.1.2", diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index d00c081fc..38cddf16c 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,17 +1,18 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; +import { PluginError, getDataModels, hasAttribute, type PluginOptions, type PluginResult } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; +import type { DMMF } from '@zenstackhq/sdk/prisma'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; +import semver from 'semver'; import { fromZodError } from 'zod-validation-error'; +import { name } from '.'; import { SecuritySchemesSchema } from './schema'; -import semver from 'semver'; export abstract class OpenAPIGeneratorBase { protected readonly DEFAULT_SPEC_VERSION = '3.1.0'; constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) {} - abstract generate(): string[]; + abstract generate(): PluginResult; protected get includedModels() { return getDataModels(this.model).filter((d) => !hasAttribute(d, '@@openapi.ignore')); @@ -91,10 +92,7 @@ export abstract class OpenAPIGeneratorBase { if (securitySchemes) { const parsed = SecuritySchemesSchema.safeParse(securitySchemes); if (!parsed.success) { - throw new PluginError( - this.options.name, - `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}` - ); + throw new PluginError(name, `"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); } return parsed.data; } diff --git a/packages/plugins/openapi/src/index.ts b/packages/plugins/openapi/src/index.ts index ddc752d8c..264403c0a 100644 --- a/packages/plugins/openapi/src/index.ts +++ b/packages/plugins/openapi/src/index.ts @@ -1,12 +1,14 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions } from '@zenstackhq/sdk'; -import { Model } from '@zenstackhq/sdk/ast'; +import { PluginError, PluginFunction } from '@zenstackhq/sdk'; import { RESTfulOpenAPIGenerator } from './rest-generator'; import { RPCOpenAPIGenerator } from './rpc-generator'; export const name = 'OpenAPI'; -export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { +const run: PluginFunction = async (model, options, dmmf) => { + if (!dmmf) { + throw new Error('DMMF is required'); + } + const flavor = options.flavor ? (options.flavor as string) : 'rpc'; switch (flavor) { @@ -17,4 +19,6 @@ export default async function run(model: Model, options: PluginOptions, dmmf: DM default: throw new PluginError(name, `Unknown flavor: ${flavor}`); } -} +}; + +export default run; diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 9dceeec3e..90383f8f9 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -1,6 +1,5 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator -import type { DMMF } from '@prisma/generator-helper'; import { analyzePolicies, getDataModels, @@ -12,6 +11,7 @@ import { resolvePath, } from '@zenstackhq/sdk'; import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; +import type { DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; @@ -76,7 +76,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { fs.writeFileSync(output, JSON.stringify(openapi, undefined, 2)); } - return this.warnings; + return { warnings: this.warnings }; } private generatePaths(): OAPI.PathsObject { @@ -217,6 +217,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { responses: { '201': this.success(`${model.name}Response`), '403': this.forbidden(), + '422': this.validationError(), }, security: resourceMeta?.security ?? policies.create === true ? [] : undefined, }; @@ -292,6 +293,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { '200': this.success(`${model.name}Response`), '403': this.forbidden(), '404': this.notFound(), + '422': this.validationError(), }, security: resourceMeta?.security ?? policies.update === true ? [] : undefined, }; @@ -956,6 +958,17 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }; } + private validationError() { + return { + description: 'Request is unprocessable due to validation errors', + content: { + 'application/vnd.api+json': { + schema: this.ref('_errorResponse'), + }, + }, + }; + } + private notFound() { return { description: 'Resource is not found', diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index e9c66792c..cb388aae2 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,6 +1,5 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator -import type { DMMF } from '@prisma/generator-helper'; import { analyzePolicies, PluginError, requireOption, resolvePath } from '@zenstackhq/sdk'; import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { @@ -11,6 +10,7 @@ import { AggregateOperationSupport, resolveAggregateOperationSupport, } from '@zenstackhq/sdk/dmmf-helpers'; +import type { DMMF } from '@zenstackhq/sdk/prisma'; import * as fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; @@ -89,7 +89,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { fs.writeFileSync(output, JSON.stringify(openapi, undefined, 2)); } - return this.warnings; + return { warnings: this.warnings }; } private generatePaths(components: OAPI.ComponentsObject): OAPI.PathsObject { @@ -524,6 +524,14 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { }, description: 'Request is forbidden', }, + '422': { + content: { + 'application/json': { + schema: this.ref('_Error'), + }, + }, + description: 'Request is unprocessable due to validation errors', + }, }, }; @@ -729,7 +737,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { return this.wrapArray(this.wrapNullable(this.ref(def.type, false), !def.isRequired), def.isList); default: - throw new PluginError(this.options.name, `Unsupported field kind: ${def.kind}`); + throw new PluginError(name, `Unsupported field kind: ${def.kind}`); } } @@ -773,10 +781,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { outputType = this.prismaTypeToOpenAPIType(field.outputType.type, !!field.isNullable); break; case 'outputObjectTypes': - outputType = this.prismaTypeToOpenAPIType( - typeof field.outputType.type === 'string' ? field.outputType.type : field.outputType.type.name, - !!field.isNullable - ); + outputType = this.prismaTypeToOpenAPIType(field.outputType.type, !!field.isNullable); break; } field.outputType; @@ -805,7 +810,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { } } - private prismaTypeToOpenAPIType(type: DMMF.ArgType, nullable: boolean): OAPI.ReferenceObject | OAPI.SchemaObject { + private prismaTypeToOpenAPIType(type: string, nullable: boolean): OAPI.ReferenceObject | OAPI.SchemaObject { const result = match(type) .with('String', () => ({ type: 'string' })) .with(P.union('Int', 'BigInt'), () => ({ type: 'integer' })) diff --git a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml index 2bdc154a5..263624c66 100644 --- a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml @@ -229,7 +229,13 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}': + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /user/{id}: get: operationId: fetch-User description: Fetch a "User" resource @@ -288,6 +294,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' patch: operationId: update-User-patch description: Update a "User" resource @@ -319,6 +331,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' delete: operationId: delete-User description: Delete a "User" resource @@ -341,7 +359,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/posts': + /user/{id}/posts: get: operationId: fetch-User-related-posts description: Fetch the related "posts" resource for "User" @@ -544,7 +562,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/relationships/posts': + /user/{id}/relationships/posts: get: operationId: fetch-User-relationship-posts description: Fetch the "posts" relationships for a "User" @@ -839,7 +857,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/profile': + /user/{id}/profile: get: operationId: fetch-User-related-profile description: Fetch the related "profile" resource for "User" @@ -867,7 +885,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/relationships/profile': + /user/{id}/relationships/profile: get: operationId: fetch-User-relationship-profile description: Fetch the "profile" relationships for a "User" @@ -1067,7 +1085,13 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/profile/{id}': + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /profile/{id}: get: operationId: fetch-Profile description: Fetch a "Profile" resource @@ -1126,6 +1150,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' patch: operationId: update-Profile-patch description: Update a "Profile" resource @@ -1157,6 +1187,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' delete: operationId: delete-Profile description: Delete a "Profile" resource @@ -1179,7 +1215,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/profile/{id}/user': + /profile/{id}/user: get: operationId: fetch-Profile-related-user description: Fetch the related "user" resource for "Profile" @@ -1207,7 +1243,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/profile/{id}/relationships/user': + /profile/{id}/relationships/user: get: operationId: fetch-Profile-relationship-user description: Fetch the "user" relationships for a "Profile" @@ -1593,7 +1629,13 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/post_Item/{id}': + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /post_Item/{id}: get: operationId: fetch-post_Item description: Fetch a "post_Item" resource @@ -1652,6 +1694,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' patch: operationId: update-post_Item-patch description: Update a "post_Item" resource @@ -1683,6 +1731,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' delete: operationId: delete-post_Item description: Delete a "post_Item" resource @@ -1705,7 +1759,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/post_Item/{id}/author': + /post_Item/{id}/author: get: operationId: fetch-post_Item-related-author description: Fetch the related "author" resource for "post_Item" @@ -1733,7 +1787,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/post_Item/{id}/relationships/author': + /post_Item/{id}/relationships/author: get: operationId: fetch-post_Item-relationship-author description: Fetch the "author" relationships for a "post_Item" diff --git a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml index ea85b4aa3..e0c7cce3d 100644 --- a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml @@ -229,7 +229,13 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}': + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /user/{id}: get: operationId: fetch-User description: Fetch a "User" resource @@ -288,6 +294,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' patch: operationId: update-User-patch description: Update a "User" resource @@ -319,6 +331,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' delete: operationId: delete-User description: Delete a "User" resource @@ -341,7 +359,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/posts': + /user/{id}/posts: get: operationId: fetch-User-related-posts description: Fetch the related "posts" resource for "User" @@ -544,7 +562,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/relationships/posts': + /user/{id}/relationships/posts: get: operationId: fetch-User-relationship-posts description: Fetch the "posts" relationships for a "User" @@ -839,7 +857,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/profile': + /user/{id}/profile: get: operationId: fetch-User-related-profile description: Fetch the related "profile" resource for "User" @@ -867,7 +885,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/user/{id}/relationships/profile': + /user/{id}/relationships/profile: get: operationId: fetch-User-relationship-profile description: Fetch the "profile" relationships for a "User" @@ -1067,7 +1085,13 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/profile/{id}': + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /profile/{id}: get: operationId: fetch-Profile description: Fetch a "Profile" resource @@ -1126,6 +1150,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' patch: operationId: update-Profile-patch description: Update a "Profile" resource @@ -1157,6 +1187,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' delete: operationId: delete-Profile description: Delete a "Profile" resource @@ -1179,7 +1215,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/profile/{id}/user': + /profile/{id}/user: get: operationId: fetch-Profile-related-user description: Fetch the related "user" resource for "Profile" @@ -1207,7 +1243,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/profile/{id}/relationships/user': + /profile/{id}/relationships/user: get: operationId: fetch-Profile-relationship-user description: Fetch the "user" relationships for a "Profile" @@ -1593,7 +1629,13 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/post_Item/{id}': + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /post_Item/{id}: get: operationId: fetch-post_Item description: Fetch a "post_Item" resource @@ -1652,6 +1694,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' patch: operationId: update-post_Item-patch description: Update a "post_Item" resource @@ -1683,6 +1731,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' delete: operationId: delete-post_Item description: Delete a "post_Item" resource @@ -1705,7 +1759,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/post_Item/{id}/author': + /post_Item/{id}/author: get: operationId: fetch-post_Item-related-author description: Fetch the related "author" resource for "post_Item" @@ -1733,7 +1787,7 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' - '/post_Item/{id}/relationships/author': + /post_Item/{id}/relationships/author: get: operationId: fetch-post_Item-relationship-author description: Fetch the "author" relationships for a "post_Item" diff --git a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml index a20233b24..78e1f711f 100644 --- a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml @@ -1,803 +1,821 @@ openapi: 3.0.0 info: - title: ZenStack Generated API - version: 1.0.0 + title: ZenStack Generated API + version: 1.0.0 tags: - - name: foo - description: Foo operations + - name: foo + description: Foo operations paths: - /foo: - get: - operationId: list-Foo - description: List "Foo" resources - tags: - - foo - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[string] - required: false - description: Equality filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$contains] - required: false - description: String contains filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$icontains] - required: false - description: String case-insensitive contains filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$search] - required: false - description: String full-text search filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$startsWith] - required: false - description: String startsWith filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$endsWith] - required: false - description: String endsWith filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[int] - required: false - description: Equality filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$lt] - required: false - description: Less-than filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$lte] - required: false - description: Less-than or equal filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$gt] - required: false - description: Greater-than filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$gte] - required: false - description: Greater-than or equal filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt] - required: false - description: Equality filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$lt] - required: false - description: Less-than filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$lte] - required: false - description: Less-than or equal filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$gt] - required: false - description: Greater-than filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$gte] - required: false - description: Greater-than or equal filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[date] - required: false - description: Equality filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$lt] - required: false - description: Less-than filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$lte] - required: false - description: Less-than or equal filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$gt] - required: false - description: Greater-than filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$gte] - required: false - description: Greater-than or equal filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[float] - required: false - description: Equality filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$lt] - required: false - description: Less-than filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$lte] - required: false - description: Less-than or equal filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$gt] - required: false - description: Greater-than filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$gte] - required: false - description: Greater-than or equal filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[decimal] - required: false - description: Equality filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$lt] - required: false - description: Less-than filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$lte] - required: false - description: Less-than or equal filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$gt] - required: false - description: Greater-than filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$gte] - required: false - description: Greater-than or equal filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[boolean] - required: false - description: Equality filter for "boolean" - in: query - style: form - explode: false - schema: - type: boolean - - name: filter[bytes] - required: false - description: Equality filter for "bytes" - in: query - style: form - explode: false - schema: - type: string - format: byte - description: Base64 encoded byte array - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - post: - operationId: create-Foo - description: Create a "Foo" resource - tags: - - foo - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - '/foo/{id}': - get: - operationId: fetch-Foo - description: Fetch a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - put: - operationId: update-Foo-put - description: Update a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - patch: - operationId: update-Foo-patch - description: Update a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - delete: - operationId: delete-Foo - description: Delete a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] + /foo: + get: + operationId: list-Foo + description: List "Foo" resources + tags: + - foo + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/page-offset" + - $ref: "#/components/parameters/page-limit" + - name: filter[id] + required: false + description: Id filter + in: query + style: form + explode: false + schema: + type: string + - name: filter[string] + required: false + description: Equality filter for "string" + in: query + style: form + explode: false + schema: + type: string + - name: filter[string$contains] + required: false + description: String contains filter for "string" + in: query + style: form + explode: false + schema: + type: string + - name: filter[string$icontains] + required: false + description: String case-insensitive contains filter for "string" + in: query + style: form + explode: false + schema: + type: string + - name: filter[string$search] + required: false + description: String full-text search filter for "string" + in: query + style: form + explode: false + schema: + type: string + - name: filter[string$startsWith] + required: false + description: String startsWith filter for "string" + in: query + style: form + explode: false + schema: + type: string + - name: filter[string$endsWith] + required: false + description: String endsWith filter for "string" + in: query + style: form + explode: false + schema: + type: string + - name: filter[int] + required: false + description: Equality filter for "int" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[int$lt] + required: false + description: Less-than filter for "int" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[int$lte] + required: false + description: Less-than or equal filter for "int" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[int$gt] + required: false + description: Greater-than filter for "int" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[int$gte] + required: false + description: Greater-than or equal filter for "int" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[bigInt] + required: false + description: Equality filter for "bigInt" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[bigInt$lt] + required: false + description: Less-than filter for "bigInt" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[bigInt$lte] + required: false + description: Less-than or equal filter for "bigInt" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[bigInt$gt] + required: false + description: Greater-than filter for "bigInt" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[bigInt$gte] + required: false + description: Greater-than or equal filter for "bigInt" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[date] + required: false + description: Equality filter for "date" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[date$lt] + required: false + description: Less-than filter for "date" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[date$lte] + required: false + description: Less-than or equal filter for "date" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[date$gt] + required: false + description: Greater-than filter for "date" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[date$gte] + required: false + description: Greater-than or equal filter for "date" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[float] + required: false + description: Equality filter for "float" + in: query + style: form + explode: false + schema: + type: number + - name: filter[float$lt] + required: false + description: Less-than filter for "float" + in: query + style: form + explode: false + schema: + type: number + - name: filter[float$lte] + required: false + description: Less-than or equal filter for "float" + in: query + style: form + explode: false + schema: + type: number + - name: filter[float$gt] + required: false + description: Greater-than filter for "float" + in: query + style: form + explode: false + schema: + type: number + - name: filter[float$gte] + required: false + description: Greater-than or equal filter for "float" + in: query + style: form + explode: false + schema: + type: number + - name: filter[decimal] + required: false + description: Equality filter for "decimal" + in: query + style: form + explode: false + schema: + oneOf: + - type: number + - type: string + - name: filter[decimal$lt] + required: false + description: Less-than filter for "decimal" + in: query + style: form + explode: false + schema: + oneOf: + - type: number + - type: string + - name: filter[decimal$lte] + required: false + description: Less-than or equal filter for "decimal" + in: query + style: form + explode: false + schema: + oneOf: + - type: number + - type: string + - name: filter[decimal$gt] + required: false + description: Greater-than filter for "decimal" + in: query + style: form + explode: false + schema: + oneOf: + - type: number + - type: string + - name: filter[decimal$gte] + required: false + description: Greater-than or equal filter for "decimal" + in: query + style: form + explode: false + schema: + oneOf: + - type: number + - type: string + - name: filter[boolean] + required: false + description: Equality filter for "boolean" + in: query + style: form + explode: false + schema: + type: boolean + - name: filter[bytes] + required: false + description: Equality filter for "bytes" + in: query + style: form + explode: false + schema: + type: string + format: byte + description: Base64 encoded byte array + responses: + "200": + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooListResponse" + "403": + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + security: [] + post: + operationId: create-Foo + description: Create a "Foo" resource + tags: + - foo + requestBody: + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooCreateRequest" + responses: + "201": + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooResponse" + "403": + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "422": + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + security: [] + /foo/{id}: + get: + operationId: fetch-Foo + description: Fetch a "Foo" resource + tags: + - foo + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooResponse" + "403": + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + security: [] + put: + operationId: update-Foo-put + description: Update a "Foo" resource + tags: + - foo + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooUpdateRequest" + responses: + "200": + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooResponse" + "403": + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "422": + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + security: [] + patch: + operationId: update-Foo-patch + description: Update a "Foo" resource + tags: + - foo + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooUpdateRequest" + responses: + "200": + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/FooResponse" + "403": + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "422": + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + security: [] + delete: + operationId: delete-Foo + description: Delete a "Foo" resource + tags: + - foo + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Successful operation + "403": + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + security: [] components: - schemas: - _jsonapi: - type: object - description: An object describing the server’s implementation - required: - - version - properties: - version: - type: string - _meta: + schemas: + _jsonapi: + type: object + description: An object describing the server’s implementation + required: + - version + properties: + version: + type: string + _meta: + type: object + description: Meta information about the request or response + properties: + serialization: + description: Superjson serialization metadata + additionalProperties: true + _resourceIdentifier: + type: object + description: Identifier for a resource + required: + - type + - id + properties: + type: + type: string + description: Resource type + id: + type: string + description: Resource id + _resource: + allOf: + - $ref: "#/components/schemas/_resourceIdentifier" + - type: object + description: A resource with attributes and relationships + properties: + attributes: + type: object + description: Resource attributes + relationships: + type: object + description: Resource relationships + _links: + type: object + required: + - self + description: Links related to the resource + properties: + self: + type: string + description: Link for refetching the curent results + _pagination: + type: object + description: Pagination information + required: + - first + - last + - prev + - next + properties: + first: + type: string + description: Link to the first page + nullable: true + last: + type: string + description: Link to the last page + nullable: true + prev: + type: string + description: Link to the previous page + nullable: true + next: + type: string + description: Link to the next page + nullable: true + _errors: + type: array + description: An array of error objects + items: + type: object + required: + - status + - code + properties: + status: + type: string + description: HTTP status + code: + type: string + description: Error code + prismaCode: + type: string + description: Prisma error code if the error is thrown by Prisma + title: + type: string + description: Error title + detail: + type: string + description: Error detail + reason: + type: string + description: Detailed error reason + zodErrors: type: object - description: Meta information about the request or response - properties: - serialization: - description: Superjson serialization metadata additionalProperties: true - _resourceIdentifier: - type: object - description: Identifier for a resource - required: - - type - - id - properties: - type: - type: string - description: Resource type - id: - type: string - description: Resource id - _resource: + description: Zod validation errors if the error is due to data validation + failure + _errorResponse: + type: object + required: + - errors + description: An error response + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + errors: + $ref: "#/components/schemas/_errors" + Foo: + type: object + description: The "Foo" model + required: + - id + - type + - attributes + properties: + id: + type: string + type: + type: string + attributes: + type: object + properties: + string: + type: string + int: + type: integer + bigInt: + type: integer + date: + type: string + format: date-time + float: + type: number + decimal: + oneOf: + - type: number + - type: string + boolean: + type: boolean + bytes: + type: string + format: byte + description: Base64 encoded byte array + FooCreateRequest: + type: object + description: Input for creating a "Foo" + required: + - data + properties: + data: + type: object + description: The "Foo" model + required: + - type + - attributes + properties: + type: + type: string + attributes: + type: object + required: + - string + - int + - bigInt + - date + - float + - decimal + - boolean + - bytes + properties: + string: + type: string + int: + type: integer + bigInt: + type: integer + date: + type: string + format: date-time + float: + type: number + decimal: + oneOf: + - type: number + - type: string + boolean: + type: boolean + bytes: + type: string + format: byte + description: Base64 encoded byte array + meta: + $ref: "#/components/schemas/_meta" + FooUpdateRequest: + type: object + description: Input for updating a "Foo" + required: + - data + properties: + data: + type: object + description: The "Foo" model + required: + - id + - type + - attributes + properties: + id: + type: string + type: + type: string + attributes: + type: object + properties: + string: + type: string + int: + type: integer + bigInt: + type: integer + date: + type: string + format: date-time + float: + type: number + decimal: + oneOf: + - type: number + - type: string + boolean: + type: boolean + bytes: + type: string + format: byte + description: Base64 encoded byte array + meta: + $ref: "#/components/schemas/_meta" + FooResponse: + type: object + description: Response for a "Foo" + required: + - data + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/Foo" + - type: object + properties: + relationships: + type: object + properties: &a1 {} + meta: + $ref: "#/components/schemas/_meta" + included: + type: array + items: + $ref: "#/components/schemas/_resource" + links: + $ref: "#/components/schemas/_links" + FooListResponse: + type: object + description: Response for a list of "Foo" + required: + - data + - links + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - - type: object - description: A resource with attributes and relationships - properties: - attributes: - type: object - description: Resource attributes - relationships: - type: object - description: Resource relationships - _links: - type: object - required: - - self - description: Links related to the resource - properties: - self: - type: string - description: Link for refetching the curent results - _pagination: - type: object - description: Pagination information - required: - - first - - last - - prev - - next - properties: - first: - type: string - description: Link to the first page - nullable: true - last: - type: string - description: Link to the last page - nullable: true - prev: - type: string - description: Link to the previous page - nullable: true - next: - type: string - description: Link to the next page - nullable: true - _errors: - type: array - description: An array of error objects - items: - type: object - required: - - status - - code + - $ref: "#/components/schemas/Foo" + - type: object properties: - status: - type: string - description: HTTP status - code: - type: string - description: Error code - prismaCode: - type: string - description: Prisma error code if the error is thrown by Prisma - title: - type: string - description: Error title - detail: - type: string - description: Error detail - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - _errorResponse: - type: object - required: - - errors - description: An error response - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - errors: - $ref: '#/components/schemas/_errors' - Foo: - type: object - description: The "Foo" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: + relationships: type: object - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - FooCreateRequest: - type: object - description: Input for creating a "Foo" - required: - - data - properties: - data: - type: object - description: The "Foo" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - meta: - $ref: '#/components/schemas/_meta' - FooUpdateRequest: - type: object - description: Input for updating a "Foo" - required: - - data - properties: - data: - type: object - description: The "Foo" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - meta: - $ref: '#/components/schemas/_meta' - FooResponse: - type: object - description: Response for a "Foo" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/Foo' - - type: object - properties: - relationships: - type: object - properties: &a1 {} - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - FooListResponse: - type: object - description: Response for a list of "Foo" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/Foo' - - type: object - properties: - relationships: - type: object - properties: *a1 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - parameters: - id: - name: id - in: path - description: The resource id - required: true - schema: - type: string - include: - name: include - in: query - description: Relationships to include - required: false - style: form - schema: - type: string - sort: - name: sort - in: query - description: Fields to sort by - required: false - style: form - schema: - type: string - page-offset: - name: page[offset] - in: query - description: Offset for pagination - required: false - style: form - schema: - type: integer - page-limit: - name: page[limit] - in: query - description: Limit for pagination - required: false - style: form - schema: - type: integer + properties: *a1 + meta: + $ref: "#/components/schemas/_meta" + included: + type: array + items: + $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_links" + - $ref: "#/components/schemas/_pagination" + parameters: + id: + name: id + in: path + description: The resource id + required: true + schema: + type: string + include: + name: include + in: query + description: Relationships to include + required: false + style: form + schema: + type: string + sort: + name: sort + in: query + description: Fields to sort by + required: false + style: form + schema: + type: string + page-offset: + name: page[offset] + in: query + description: Offset for pagination + required: false + style: form + schema: + type: integer + page-limit: + name: page[limit] + in: query + description: Limit for pagination + required: false + style: form + schema: + type: integer diff --git a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml index 30b1dc4f6..3e293eefd 100644 --- a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml @@ -343,8 +343,14 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' security: [] - '/foo/{id}': + /foo/{id}: get: operationId: fetch-Foo description: Fetch a "Foo" resource @@ -404,6 +410,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' security: [] patch: operationId: update-Foo-patch @@ -436,6 +448,12 @@ paths: application/vnd.api+json: schema: $ref: '#/components/schemas/_errorResponse' + '422': + description: Request is unprocessable due to validation errors + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' security: [] delete: operationId: delete-Foo diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml index eacf4d6d7..68885daed 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml @@ -180,7 +180,7 @@ components: $ref: '#/components/schemas/Post_ItemListRelationFilter' profile: oneOf: - - $ref: '#/components/schemas/ProfileRelationFilter' + - $ref: '#/components/schemas/ProfileNullableRelationFilter' - $ref: '#/components/schemas/ProfileWhereInput' nullable: true UserOrderByWithRelationInput: @@ -207,6 +207,43 @@ components: type: string email: type: string + AND: + oneOf: + - $ref: '#/components/schemas/UserWhereInput' + - type: array + items: + $ref: '#/components/schemas/UserWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/UserWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/UserWhereInput' + - type: array + items: + $ref: '#/components/schemas/UserWhereInput' + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + role: + oneOf: + - $ref: '#/components/schemas/EnumroleFilter' + - $ref: '#/components/schemas/Role' + posts: + $ref: '#/components/schemas/Post_ItemListRelationFilter' + profile: + oneOf: + - $ref: '#/components/schemas/ProfileNullableRelationFilter' + - $ref: '#/components/schemas/ProfileWhereInput' + nullable: true UserScalarWhereWithAggregatesInput: type: object properties: @@ -304,6 +341,31 @@ components: type: string userId: type: string + AND: + oneOf: + - $ref: '#/components/schemas/ProfileWhereInput' + - type: array + items: + $ref: '#/components/schemas/ProfileWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/ProfileWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/ProfileWhereInput' + - type: array + items: + $ref: '#/components/schemas/ProfileWhereInput' + image: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + nullable: true + user: + oneOf: + - $ref: '#/components/schemas/UserRelationFilter' + - $ref: '#/components/schemas/UserWhereInput' ProfileScalarWhereWithAggregatesInput: type: object properties: @@ -393,7 +455,7 @@ components: nullable: true author: oneOf: - - $ref: '#/components/schemas/UserRelationFilter' + - $ref: '#/components/schemas/UserNullableRelationFilter' - $ref: '#/components/schemas/UserWhereInput' nullable: true Post_ItemOrderByWithRelationInput: @@ -426,6 +488,59 @@ components: properties: id: type: string + AND: + oneOf: + - $ref: '#/components/schemas/Post_ItemWhereInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/Post_ItemWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/Post_ItemWhereInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemWhereInput' + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + title: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + authorId: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + nullable: true + published: + oneOf: + - $ref: '#/components/schemas/BoolFilter' + - type: boolean + viewCount: + oneOf: + - $ref: '#/components/schemas/IntFilter' + - type: integer + notes: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + nullable: true + author: + oneOf: + - $ref: '#/components/schemas/UserNullableRelationFilter' + - $ref: '#/components/schemas/UserWhereInput' + nullable: true Post_ItemScalarWhereWithAggregatesInput: type: object properties: @@ -750,17 +865,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -788,21 +899,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -826,17 +931,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -850,7 +951,7 @@ components: $ref: '#/components/schemas/Post_ItemWhereInput' none: $ref: '#/components/schemas/Post_ItemWhereInput' - ProfileRelationFilter: + ProfileNullableRelationFilter: type: object properties: is: @@ -872,17 +973,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -916,21 +1013,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -960,17 +1051,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -988,18 +1075,14 @@ components: type: string nullable: true in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true lt: type: string @@ -1026,13 +1109,9 @@ components: type: object properties: is: - allOf: - - $ref: '#/components/schemas/UserWhereInput' - nullable: true + $ref: '#/components/schemas/UserWhereInput' isNot: - allOf: - - $ref: '#/components/schemas/UserWhereInput' - nullable: true + $ref: '#/components/schemas/UserWhereInput' SortOrderInput: type: object properties: @@ -1049,18 +1128,14 @@ components: type: string nullable: true in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true lt: type: string @@ -1104,17 +1179,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1127,6 +1198,17 @@ components: oneOf: - type: integer - $ref: '#/components/schemas/NestedIntFilter' + UserNullableRelationFilter: + type: object + properties: + is: + allOf: + - $ref: '#/components/schemas/UserWhereInput' + nullable: true + isNot: + allOf: + - $ref: '#/components/schemas/UserWhereInput' + nullable: true BoolWithAggregatesFilter: type: object properties: @@ -1148,17 +1230,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1354,13 +1432,18 @@ components: upsert: $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' disconnect: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' delete: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' connect: $ref: '#/components/schemas/ProfileWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' Post_ItemUncheckedUpdateManyWithoutAuthorNestedInput: @@ -1444,13 +1527,18 @@ components: upsert: $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' disconnect: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' delete: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' connect: $ref: '#/components/schemas/ProfileWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' UserCreateNestedOneWithoutProfileInput: @@ -1485,6 +1573,7 @@ components: $ref: '#/components/schemas/UserWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutProfileInput' - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' UserCreateNestedOneWithoutPostsInput: @@ -1528,13 +1617,18 @@ components: upsert: $ref: '#/components/schemas/UserUpsertWithoutPostsInput' disconnect: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserWhereInput' delete: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserWhereInput' connect: $ref: '#/components/schemas/UserWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutPostsInput' - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' NestedStringFilter: @@ -1543,17 +1637,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1579,21 +1669,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1617,17 +1701,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -1638,17 +1718,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1679,17 +1755,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1709,21 +1781,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1753,17 +1819,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -1781,18 +1843,14 @@ components: type: string nullable: true in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true lt: type: string @@ -1820,18 +1878,14 @@ components: type: string nullable: true in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string nullable: true lt: type: string @@ -1865,18 +1919,14 @@ components: type: integer nullable: true in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer nullable: true notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer nullable: true lt: type: integer @@ -1921,17 +1971,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1960,17 +2006,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -2119,7 +2161,7 @@ components: data: oneOf: - $ref: '#/components/schemas/Post_ItemUpdateManyMutationInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateManyWithoutPostsInput' + - $ref: '#/components/schemas/Post_ItemUncheckedUpdateManyWithoutAuthorInput' required: - where - data @@ -2189,9 +2231,22 @@ components: oneOf: - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' + where: + $ref: '#/components/schemas/ProfileWhereInput' required: - update - create + ProfileUpdateToOneWithWhereWithoutUserInput: + type: object + properties: + where: + $ref: '#/components/schemas/ProfileWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' + - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' + required: + - data ProfileUpdateWithoutUserInput: type: object properties: @@ -2278,9 +2333,22 @@ components: oneOf: - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' + where: + $ref: '#/components/schemas/UserWhereInput' required: - update - create + UserUpdateToOneWithWhereWithoutProfileInput: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' + - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' + required: + - data UserUpdateWithoutProfileInput: type: object properties: @@ -2397,9 +2465,22 @@ components: oneOf: - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' + where: + $ref: '#/components/schemas/UserWhereInput' required: - update - create + UserUpdateToOneWithWhereWithoutPostsInput: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' + required: + - data UserUpdateWithoutPostsInput: type: object properties: @@ -2545,7 +2626,7 @@ components: - type: string - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' nullable: true - Post_ItemUncheckedUpdateManyWithoutPostsInput: + Post_ItemUncheckedUpdateManyWithoutAuthorInput: type: object properties: id: @@ -3758,6 +3839,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -3797,6 +3884,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -3836,6 +3929,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -3885,6 +3984,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -3936,6 +4041,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -3985,6 +4096,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4024,6 +4141,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4063,6 +4186,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4105,6 +4234,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4144,6 +4279,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4195,6 +4336,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4244,6 +4391,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4295,6 +4448,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4344,6 +4503,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4383,6 +4548,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4422,6 +4593,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4471,6 +4648,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4522,6 +4705,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4571,6 +4760,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4610,6 +4805,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4649,6 +4850,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4688,6 +4895,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4737,6 +4950,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4788,6 +5007,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4837,6 +5062,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4888,6 +5119,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4937,6 +5174,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4976,6 +5219,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5015,6 +5264,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5064,6 +5319,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5113,6 +5374,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5152,6 +5419,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5191,6 +5464,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5230,6 +5509,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5279,6 +5564,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5330,6 +5621,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5379,6 +5676,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5430,6 +5733,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml index 9608839dd..0f36abca2 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml @@ -183,7 +183,7 @@ components: $ref: '#/components/schemas/Post_ItemListRelationFilter' profile: oneOf: - - $ref: '#/components/schemas/ProfileRelationFilter' + - $ref: '#/components/schemas/ProfileNullableRelationFilter' - $ref: '#/components/schemas/ProfileWhereInput' - type: 'null' UserOrderByWithRelationInput: @@ -210,6 +210,43 @@ components: type: string email: type: string + AND: + oneOf: + - $ref: '#/components/schemas/UserWhereInput' + - type: array + items: + $ref: '#/components/schemas/UserWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/UserWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/UserWhereInput' + - type: array + items: + $ref: '#/components/schemas/UserWhereInput' + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + role: + oneOf: + - $ref: '#/components/schemas/EnumroleFilter' + - $ref: '#/components/schemas/Role' + posts: + $ref: '#/components/schemas/Post_ItemListRelationFilter' + profile: + oneOf: + - $ref: '#/components/schemas/ProfileNullableRelationFilter' + - $ref: '#/components/schemas/ProfileWhereInput' + - type: 'null' UserScalarWhereWithAggregatesInput: type: object properties: @@ -307,6 +344,31 @@ components: type: string userId: type: string + AND: + oneOf: + - $ref: '#/components/schemas/ProfileWhereInput' + - type: array + items: + $ref: '#/components/schemas/ProfileWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/ProfileWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/ProfileWhereInput' + - type: array + items: + $ref: '#/components/schemas/ProfileWhereInput' + image: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + - type: 'null' + user: + oneOf: + - $ref: '#/components/schemas/UserRelationFilter' + - $ref: '#/components/schemas/UserWhereInput' ProfileScalarWhereWithAggregatesInput: type: object properties: @@ -396,7 +458,7 @@ components: - type: 'null' author: oneOf: - - $ref: '#/components/schemas/UserRelationFilter' + - $ref: '#/components/schemas/UserNullableRelationFilter' - $ref: '#/components/schemas/UserWhereInput' - type: 'null' Post_ItemOrderByWithRelationInput: @@ -429,6 +491,59 @@ components: properties: id: type: string + AND: + oneOf: + - $ref: '#/components/schemas/Post_ItemWhereInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/Post_ItemWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/Post_ItemWhereInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemWhereInput' + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + title: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + authorId: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + - type: 'null' + published: + oneOf: + - $ref: '#/components/schemas/BoolFilter' + - type: boolean + viewCount: + oneOf: + - $ref: '#/components/schemas/IntFilter' + - type: integer + notes: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + - type: 'null' + author: + oneOf: + - $ref: '#/components/schemas/UserNullableRelationFilter' + - $ref: '#/components/schemas/UserWhereInput' + - type: 'null' Post_ItemScalarWhereWithAggregatesInput: type: object properties: @@ -758,17 +873,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -796,21 +907,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -834,17 +939,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -858,7 +959,7 @@ components: $ref: '#/components/schemas/Post_ItemWhereInput' none: $ref: '#/components/schemas/Post_ItemWhereInput' - ProfileRelationFilter: + ProfileNullableRelationFilter: type: object properties: is: @@ -880,17 +981,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -924,21 +1021,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -968,17 +1059,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -998,18 +1085,16 @@ components: - type: string in: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' lt: type: string lte: @@ -1035,13 +1120,9 @@ components: type: object properties: is: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserWhereInput' + $ref: '#/components/schemas/UserWhereInput' isNot: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserWhereInput' + $ref: '#/components/schemas/UserWhereInput' SortOrderInput: type: object properties: @@ -1060,18 +1141,16 @@ components: - type: string in: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' lt: type: string lte: @@ -1114,17 +1193,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1137,6 +1212,17 @@ components: oneOf: - type: integer - $ref: '#/components/schemas/NestedIntFilter' + UserNullableRelationFilter: + type: object + properties: + is: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserWhereInput' + isNot: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/UserWhereInput' BoolWithAggregatesFilter: type: object properties: @@ -1158,17 +1244,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1364,13 +1446,18 @@ components: upsert: $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' disconnect: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' delete: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' connect: $ref: '#/components/schemas/ProfileWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' Post_ItemUncheckedUpdateManyWithoutAuthorNestedInput: @@ -1454,13 +1541,18 @@ components: upsert: $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' disconnect: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' delete: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/ProfileWhereInput' connect: $ref: '#/components/schemas/ProfileWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' UserCreateNestedOneWithoutProfileInput: @@ -1496,6 +1588,7 @@ components: $ref: '#/components/schemas/UserWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutProfileInput' - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' UserCreateNestedOneWithoutPostsInput: @@ -1539,13 +1632,18 @@ components: upsert: $ref: '#/components/schemas/UserUpsertWithoutPostsInput' disconnect: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserWhereInput' delete: - type: boolean + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserWhereInput' connect: $ref: '#/components/schemas/UserWhereUniqueInput' update: oneOf: + - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutPostsInput' - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' NestedStringFilter: @@ -1554,17 +1652,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1590,21 +1684,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1628,17 +1716,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -1649,17 +1733,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1690,17 +1770,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1718,23 +1794,17 @@ components: properties: equals: type: string - format: date-time - in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + format: date-time + in: + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1764,17 +1834,13 @@ components: equals: $ref: '#/components/schemas/Role' in: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' notIn: - oneOf: - - type: array - items: - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/Role' + type: array + items: + $ref: '#/components/schemas/Role' not: oneOf: - $ref: '#/components/schemas/Role' @@ -1794,18 +1860,16 @@ components: - type: string in: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' lt: type: string lte: @@ -1834,18 +1898,16 @@ components: - type: string in: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string - - type: string - - type: 'null' lt: type: string lte: @@ -1880,18 +1942,16 @@ components: - type: integer in: oneOf: + - type: 'null' - type: array items: type: integer - - type: integer - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: integer - - type: integer - - type: 'null' lt: type: integer lte: @@ -1935,17 +1995,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1974,17 +2030,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -2137,7 +2189,7 @@ components: data: oneOf: - $ref: '#/components/schemas/Post_ItemUpdateManyMutationInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateManyWithoutPostsInput' + - $ref: '#/components/schemas/Post_ItemUncheckedUpdateManyWithoutAuthorInput' required: - where - data @@ -2207,9 +2259,22 @@ components: oneOf: - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' + where: + $ref: '#/components/schemas/ProfileWhereInput' required: - update - create + ProfileUpdateToOneWithWhereWithoutUserInput: + type: object + properties: + where: + $ref: '#/components/schemas/ProfileWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' + - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' + required: + - data ProfileUpdateWithoutUserInput: type: object properties: @@ -2296,9 +2361,22 @@ components: oneOf: - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' + where: + $ref: '#/components/schemas/UserWhereInput' required: - update - create + UserUpdateToOneWithWhereWithoutProfileInput: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' + - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' + required: + - data UserUpdateWithoutProfileInput: type: object properties: @@ -2415,9 +2493,22 @@ components: oneOf: - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' + where: + $ref: '#/components/schemas/UserWhereInput' required: - update - create + UserUpdateToOneWithWhereWithoutPostsInput: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' + required: + - data UserUpdateWithoutPostsInput: type: object properties: @@ -2564,7 +2655,7 @@ components: - type: string - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - type: 'null' - Post_ItemUncheckedUpdateManyWithoutPostsInput: + Post_ItemUncheckedUpdateManyWithoutAuthorInput: type: object properties: id: @@ -3812,6 +3903,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -3851,6 +3948,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -3890,6 +3993,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -3939,6 +4048,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -3990,6 +4105,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4039,6 +4160,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4078,6 +4205,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4117,6 +4250,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4159,6 +4298,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4198,6 +4343,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4249,6 +4400,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4298,6 +4455,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4349,6 +4512,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4398,6 +4567,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4437,6 +4612,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4476,6 +4657,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4525,6 +4712,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4576,6 +4769,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4625,6 +4824,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4664,6 +4869,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4703,6 +4914,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -4742,6 +4959,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4791,6 +5014,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4842,6 +5071,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4891,6 +5126,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4942,6 +5183,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -4991,6 +5238,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5030,6 +5283,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5069,6 +5328,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5118,6 +5383,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5167,6 +5438,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5206,6 +5483,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5245,6 +5528,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -5284,6 +5573,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5333,6 +5628,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5384,6 +5685,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5433,6 +5740,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -5484,6 +5797,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml index bcb1e6d4c..495ebd42b 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml @@ -157,6 +157,59 @@ components: properties: id: type: string + AND: + oneOf: + - $ref: '#/components/schemas/FooWhereInput' + - type: array + items: + $ref: '#/components/schemas/FooWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/FooWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/FooWhereInput' + - type: array + items: + $ref: '#/components/schemas/FooWhereInput' + string: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + int: + oneOf: + - $ref: '#/components/schemas/IntFilter' + - type: integer + bigInt: + oneOf: + - $ref: '#/components/schemas/BigIntFilter' + - type: integer + date: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + float: + oneOf: + - $ref: '#/components/schemas/FloatFilter' + - type: number + decimal: + oneOf: + - $ref: '#/components/schemas/DecimalFilter' + - oneOf: + - type: string + - type: number + boolean: + oneOf: + - $ref: '#/components/schemas/BoolFilter' + - type: boolean + bytes: + oneOf: + - $ref: '#/components/schemas/BytesNullableFilter' + - type: string + format: byte + nullable: true FooScalarWhereWithAggregatesInput: type: object properties: @@ -379,17 +432,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -416,17 +465,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -445,17 +490,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -475,21 +516,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -513,17 +548,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -544,25 +575,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -602,22 +625,16 @@ components: format: byte nullable: true in: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true notIn: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true not: oneOf: @@ -640,17 +657,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -683,17 +696,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -722,17 +731,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -762,21 +767,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -806,17 +805,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -847,25 +842,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -921,22 +908,16 @@ components: format: byte nullable: true in: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true notIn: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true not: oneOf: @@ -1041,17 +1022,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1076,17 +1053,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1105,17 +1078,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1135,21 +1104,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1173,17 +1136,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -1204,25 +1163,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -1262,22 +1213,16 @@ components: format: byte nullable: true in: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true notIn: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true not: oneOf: @@ -1291,17 +1236,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1332,17 +1273,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1371,17 +1308,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1411,21 +1344,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1455,17 +1382,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -1496,25 +1419,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -1570,22 +1485,16 @@ components: format: byte nullable: true in: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true notIn: - oneOf: - - type: array - items: - type: string - format: byte - - type: string - format: byte + type: array + items: + type: string + format: byte nullable: true not: oneOf: @@ -1606,18 +1515,14 @@ components: type: integer nullable: true in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer nullable: true notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer nullable: true lt: type: integer @@ -2218,6 +2123,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2258,6 +2169,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2298,6 +2215,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2348,6 +2271,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2400,6 +2329,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2450,6 +2385,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2490,6 +2431,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2530,6 +2477,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2570,6 +2523,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2620,6 +2579,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2672,6 +2637,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2722,6 +2693,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2774,6 +2751,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml index 21524fad5..c9327b7f2 100644 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml @@ -158,6 +158,59 @@ components: properties: id: type: string + AND: + oneOf: + - $ref: '#/components/schemas/FooWhereInput' + - type: array + items: + $ref: '#/components/schemas/FooWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/FooWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/FooWhereInput' + - type: array + items: + $ref: '#/components/schemas/FooWhereInput' + string: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + int: + oneOf: + - $ref: '#/components/schemas/IntFilter' + - type: integer + bigInt: + oneOf: + - $ref: '#/components/schemas/BigIntFilter' + - type: integer + date: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + float: + oneOf: + - $ref: '#/components/schemas/FloatFilter' + - type: number + decimal: + oneOf: + - $ref: '#/components/schemas/DecimalFilter' + - oneOf: + - type: string + - type: number + boolean: + oneOf: + - $ref: '#/components/schemas/BoolFilter' + - type: boolean + bytes: + oneOf: + - $ref: '#/components/schemas/BytesNullableFilter' + - type: string + format: byte + - type: 'null' FooScalarWhereWithAggregatesInput: type: object properties: @@ -382,17 +435,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -419,17 +468,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -448,17 +493,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -478,21 +519,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -516,17 +551,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -547,25 +578,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -607,22 +630,18 @@ components: format: byte in: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' not: oneOf: - type: string @@ -644,17 +663,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -687,17 +702,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -726,17 +737,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -766,21 +773,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -810,17 +811,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -851,25 +848,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -927,22 +916,18 @@ components: format: byte in: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' not: oneOf: - type: string @@ -1047,17 +1032,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1082,17 +1063,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1111,17 +1088,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1141,21 +1114,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1179,17 +1146,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -1210,25 +1173,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -1270,22 +1225,18 @@ components: format: byte in: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' not: oneOf: - type: string @@ -1298,17 +1249,13 @@ components: equals: type: string in: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string notIn: - oneOf: - - type: array - items: - type: string - - type: string + type: array + items: + type: string lt: type: string lte: @@ -1339,17 +1286,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1378,17 +1321,13 @@ components: equals: type: integer in: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer notIn: - oneOf: - - type: array - items: - type: integer - - type: integer + type: array + items: + type: integer lt: type: integer lte: @@ -1418,21 +1357,15 @@ components: type: string format: date-time in: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time notIn: - oneOf: - - type: array - items: - type: string - format: date-time - - type: string - format: date-time + type: array + items: + type: string + format: date-time lt: type: string format: date-time @@ -1462,17 +1395,13 @@ components: equals: type: number in: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number notIn: - oneOf: - - type: array - items: - type: number - - type: number + type: array + items: + type: number lt: type: number lte: @@ -1503,25 +1432,17 @@ components: - type: string - type: number in: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number notIn: - oneOf: - - type: array - items: - oneOf: - - type: string - - type: number - - oneOf: - - type: string - - type: number + type: array + items: + oneOf: + - type: string + - type: number lt: oneOf: - type: string @@ -1579,22 +1500,18 @@ components: format: byte in: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: string format: byte - - type: string - format: byte - - type: 'null' not: oneOf: - type: string @@ -1616,18 +1533,16 @@ components: - type: integer in: oneOf: + - type: 'null' - type: array items: type: integer - - type: integer - - type: 'null' notIn: oneOf: + - type: 'null' - type: array items: type: integer - - type: integer - - type: 'null' lt: type: integer lte: @@ -2250,6 +2165,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2290,6 +2211,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2330,6 +2257,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2380,6 +2313,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2432,6 +2371,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2482,6 +2427,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2522,6 +2473,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2562,6 +2519,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors requestBody: content: application/json: @@ -2602,6 +2565,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2652,6 +2621,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2704,6 +2679,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2754,6 +2735,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query @@ -2806,6 +2793,12 @@ paths: schema: $ref: '#/components/schemas/_Error' description: Request is forbidden + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/_Error' + description: Request is unprocessable due to validation errors parameters: - name: q in: query diff --git a/packages/plugins/swr/CHANGELOG.md b/packages/plugins/swr/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/plugins/swr/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 7902164b1..304469748 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "1.12.4", + "version": "2.0.0", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { @@ -38,7 +38,6 @@ } }, "dependencies": { - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "change-case": "^4.1.2", diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index e074b603c..4ab3fb79e 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -1,45 +1,33 @@ -import type { DMMF } from '@prisma/generator-helper'; import { PluginOptions, createProject, + ensureEmptyDir, generateModelMeta, getDataModels, - getPrismaClientImportSpec, - getPrismaVersion, requireOption, resolvePath, saveProject, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; -import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; -import semver from 'semver'; -import { FunctionDeclaration, OptionalKind, ParameterDeclarationStructure, Project, SourceFile } from 'ts-morph'; +import type { OptionalKind, ParameterDeclarationStructure, Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { let outDir = requireOption(options, 'output', name); outDir = resolvePath(outDir, options); + ensureEmptyDir(outDir); const project = createProject(); const warnings: string[] = []; - if (options.useSuperJson !== undefined) { - warnings.push( - 'The option "useSuperJson" is deprecated. The generated hooks always use superjson for serialization.' - ); - } - - const legacyMutations = options.legacyMutations !== false; - const models = getDataModels(model); await generateModelMeta(project, models, { output: path.join(outDir, '__model_meta.ts'), - compile: false, - preserveTsFiles: true, generateAttributes: false, }); @@ -51,11 +39,11 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. warnings.push(`Unable to find mapping for model ${dataModel.name}`); return; } - generateModelHooks(project, outDir, dataModel, mapping, legacyMutations); + generateModelHooks(project, outDir, dataModel, mapping, options); }); await saveProject(project); - return warnings; + return { warnings }; } function generateModelHooks( @@ -63,39 +51,26 @@ function generateModelHooks( outDir: string, model: DataModel, mapping: DMMF.ModelMapping, - legacyMutations: boolean + options: PluginOptions ) { const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); sf.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(model.$container, outDir); + const prismaImport = getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma'], isTypeOnly: true, moduleSpecifier: prismaImport, }); sf.addStatements([ - `import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable, useHooksContext } from '@zenstackhq/swr/runtime';`, + `import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime';`, `import metadata from './__model_meta';`, `import * as request from '@zenstackhq/swr/runtime';`, ]); const modelNameCap = upperCaseFirst(model.name); - const prismaVersion = getPrismaVersion(); - - const useMutation = legacyMutations - ? sf.addFunction({ - name: `useMutate${model.name}`, - isExported: true, - statements: [ - 'const { endpoint, fetch } = useHooksContext();', - `const invalidate = request.useInvalidation('${model.name}', metadata);`, - ], - docs: ['@deprecated Use mutation hooks (useCreateXXX, useUpdateXXX, etc.) instead.'], - }) - : undefined; const mutationFuncs: string[] = []; @@ -103,13 +78,13 @@ function generateModelHooks( // eslint-disable-next-line @typescript-eslint/no-explicit-any if (mapping.create || (mapping as any).createOne) { const argsType = `Prisma.${model.name}CreateArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'POST', 'create', argsType, false)); + mutationFuncs.push(generateMutation(sf, model, 'POST', 'create', argsType, false)); } // createMany if (mapping.createMany) { const argsType = `Prisma.${model.name}CreateManyArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'POST', 'createMany', argsType, true)); + mutationFuncs.push(generateMutation(sf, model, 'POST', 'createMany', argsType, true)); } // findMany @@ -148,13 +123,13 @@ function generateModelHooks( // eslint-disable-next-line @typescript-eslint/no-explicit-any if (mapping.update || (mapping as any).updateOne) { const argsType = `Prisma.${model.name}UpdateArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'PUT', 'update', argsType, false)); + mutationFuncs.push(generateMutation(sf, model, 'PUT', 'update', argsType, false)); } // updateMany if (mapping.updateMany) { const argsType = `Prisma.${model.name}UpdateManyArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'PUT', 'updateMany', argsType, true)); + mutationFuncs.push(generateMutation(sf, model, 'PUT', 'updateMany', argsType, true)); } // upsert @@ -162,7 +137,7 @@ function generateModelHooks( // eslint-disable-next-line @typescript-eslint/no-explicit-any if (mapping.upsert || (mapping as any).upsertOne) { const argsType = `Prisma.${model.name}UpsertArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'POST', 'upsert', argsType, false)); + mutationFuncs.push(generateMutation(sf, model, 'POST', 'upsert', argsType, false)); } // del @@ -170,13 +145,13 @@ function generateModelHooks( // eslint-disable-next-line @typescript-eslint/no-explicit-any if (mapping.delete || (mapping as any).deleteOne) { const argsType = `Prisma.${model.name}DeleteArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'DELETE', 'delete', argsType, false)); + mutationFuncs.push(generateMutation(sf, model, 'DELETE', 'delete', argsType, false)); } // deleteMany if (mapping.deleteMany) { const argsType = `Prisma.${model.name}DeleteManyArgs`; - mutationFuncs.push(generateMutation(sf, useMutation, model, 'DELETE', 'deleteMany', argsType, true)); + mutationFuncs.push(generateMutation(sf, model, 'DELETE', 'deleteMany', argsType, true)); } // aggregate @@ -189,11 +164,7 @@ function generateModelHooks( // groupBy if (mapping.groupBy) { - let useName = modelNameCap; - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - // prisma 4 and 5 different typing for "groupBy" and we have to deal with it separately - useName = model.name; - } + const useName = model.name; const typeParameters = [ `T extends Prisma.${useName}GroupByArgs`, `HasSelectOrTake extends Prisma.Or>, Prisma.Extends<'take', Prisma.Keys>>`, @@ -268,8 +239,6 @@ function generateModelHooks( const returnType = `T extends { select: any; } ? T['select'] extends true ? number : Prisma.GetScalarType : number`; generateQueryHook(sf, model, 'count', argsType, inputType, returnType); } - - useMutation?.addStatements(`return { ${mutationFuncs.join(', ')} };`); } function makeOptimistic(returnType: string) { @@ -330,7 +299,6 @@ function generateQueryHook( function generateMutation( sf: SourceFile, - useMutateModelFunc: FunctionDeclaration | undefined, model: DataModel, method: 'POST' | 'PUT' | 'PATCH' | 'DELETE', operation: string, @@ -343,30 +311,8 @@ function generateMutation( const returnType = batchResult ? 'Prisma.BatchPayload' : `Prisma.${model.name}GetPayload<${argsType}> | undefined`; const genericInputType = `Prisma.SelectSubset`; - const modelRouteName = lowerCaseFirst(model.name); const funcName = `${operation}${model.name}`; - if (useMutateModelFunc) { - // generate async mutation function (legacy) - const mutationFunc = useMutateModelFunc.addFunction({ - name: funcName, - isAsync: true, - typeParameters: [`T extends ${argsType}`], - parameters: [ - { - name: 'args', - type: genericInputType, - }, - ], - }); - mutationFunc.addJsDoc(`@deprecated Use \`use${upperCaseFirst(operation)}${model.name}\` hook instead.`); - mutationFunc - .addBody() - .addStatements([ - `return await request.mutationRequest<${returnType}, ${checkReadBack}>('${method}', \`\${endpoint}/${modelRouteName}/${operation}\`, args, invalidate, fetch, ${checkReadBack});`, - ]); - } - // generate mutation hook sf.addFunction({ name: `use${upperCaseFirst(operation)}${model.name}`, diff --git a/packages/plugins/swr/src/index.ts b/packages/plugins/swr/src/index.ts index 43731f984..16ae20c05 100644 --- a/packages/plugins/swr/src/index.ts +++ b/packages/plugins/swr/src/index.ts @@ -1,10 +1,13 @@ -import type { DMMF } from '@prisma/generator-helper'; -import type { PluginOptions } from '@zenstackhq/sdk'; -import type { Model } from '@zenstackhq/sdk/ast'; +import type { PluginFunction } from '@zenstackhq/sdk'; import { generate } from './generator'; export const name = 'SWR'; -export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { +const run: PluginFunction = async (model, options, dmmf) => { + if (!dmmf) { + throw new Error('DMMF is required'); + } return generate(model, options, dmmf); -} +}; + +export default run; diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts index e8ce6a255..0ca4212cc 100644 --- a/packages/plugins/swr/src/runtime/index.ts +++ b/packages/plugins/swr/src/runtime/index.ts @@ -79,13 +79,6 @@ export type QueryOptions = { */ disabled?: boolean; - /** - * @deprecated Use `fallbackData` instead - * - * Equivalent to @see SWRConfiguration.fallbackData - */ - initialData?: Result; - /** * Whether to enable automatic optimistic update. Defaults to `true`. */ @@ -100,13 +93,6 @@ export type InfiniteQueryOptions = { * Disable data fetching */ disabled?: boolean; - - /** - * @deprecated Use `fallbackData` instead - * - * Equivalent to @see SWRInfiniteConfiguration.fallbackData - */ - initialData?: Result[]; } & Omit>, 'fetcher'>; const QUERY_KEY_PREFIX = 'zenstack:query'; @@ -121,6 +107,46 @@ type QueryKey = { optimisticUpdate?: boolean; }; +/** + * Result of optimistic data provider. + */ +export type OptimisticDataProviderResult = { + /** + * Kind of the result. + * - Update: use the `data` field to update the query cache. + * - Skip: skip the optimistic update for this query. + * - ProceedDefault: proceed with the default optimistic update. + */ + kind: 'Update' | 'Skip' | 'ProceedDefault'; + + /** + * Data to update the query cache. Only applicable if `kind` is 'Update'. + * + * If the data is an object with fields updated, it should have a `$optimistic` + * field set to `true`. If it's an array and an element object is created or updated, + * the element should have a `$optimistic` field set to `true`. + */ + data?: any; +}; + +/** + * Optimistic data provider. + * + * @param args Arguments. + * @param args.queryModel The model of the query. + * @param args.queryOperation The operation of the query, `findMany`, `count`, etc. + * @param args.queryArgs The arguments of the query. + * @param args.currentData The current cache data for the query. + * @param args.mutationArgs The arguments of the mutation. + */ +export type OptimisticDataProvider = (args: { + queryModel: string; + queryOperation: string; + queryArgs: any; + currentData: any; + mutationArgs: any; +}) => OptimisticDataProviderResult | Promise; + /** * Mutation options. */ @@ -129,6 +155,11 @@ export type MutationOptions = { * Whether to automatically optimistic-update queries potentially impacted. Defaults to `false`. */ optimisticUpdate?: boolean; + + /** + * A callback for computing optimistic update data for each query cache entry. + */ + optimisticDataProvider?: OptimisticDataProvider; } & Omit, 'fetcher'>; /** @@ -189,10 +220,7 @@ export function useModelQuery( ? null : getQueryKey(model, operation, args, false, options?.optimisticUpdate !== false); const url = makeUrl(`${endpoint}/${lowerCaseFirst(model)}/${operation}`, args); - return useSWR(key, () => fetcher(url, undefined, fetch, false), { - ...options, - fallbackData: options?.initialData ?? options?.fallbackData, - }); + return useSWR(key, () => fetcher(url, undefined, fetch, false), options); } /** @@ -239,10 +267,7 @@ export function useInfiniteModelQuery( throw new Error('Invalid query key: ' + key); } }, - { - ...options, - fallbackData: options?.initialData ?? options?.fallbackData, - } + options ); } @@ -262,7 +287,7 @@ export function useModelMutation { if (options?.optimisticUpdate) { - optimisticUpdate(model, operation, arg, modelMeta, cache, mutate, logging); + optimisticUpdate(model, operation, arg, options, modelMeta, cache, mutate, logging); } const url = `${endpoint}/${lowerCaseFirst(model)}/${operation}`; return mutationRequest(method, url, arg, invalidate, fetch, checkReadBack); @@ -430,6 +455,7 @@ async function optimisticUpdate( mutationModel: string, mutationOp: string, mutationArgs: any, + options: MutationOptions | undefined, modelMeta: ModelMeta, cache: Cache, mutator: ScopedMutator, @@ -450,14 +476,37 @@ async function optimisticUpdate( } const cacheValue = cache.get(key); - if (!cacheValue) { + if (cacheValue?.error) { + if (logging) { + console.warn(`Skipping optimistic update for ${key} due to error:`, cacheValue.error); + } continue; } - if (cacheValue.error) { - if (logging) { - console.warn(`Skipping optimistic update for ${key} due to error:`, cacheValue.error); + if (options?.optimisticDataProvider) { + const providerResult = await options.optimisticDataProvider({ + queryModel: parsedKey.model, + queryOperation: parsedKey.operation, + queryArgs: parsedKey.args, + currentData: cacheValue?.data, + mutationArgs, + }); + + if (providerResult?.kind === 'Skip') { + if (logging) { + console.log(`Skipping optimistic update for ${key} due to custom provider`); + } + continue; + } else if (providerResult?.kind === 'Update') { + if (logging) { + console.log(`Optimistically updating query ${JSON.stringify(key)} due to provider`); + } + optimisticPromises.push(mutator(key, providerResult.data, { revalidate: false })); + continue; } + } + + if (!cacheValue) { continue; } diff --git a/packages/plugins/swr/tests/react-hooks.test.tsx b/packages/plugins/swr/tests/react-hooks.test.tsx index 60c419bbc..fa495f97a 100644 --- a/packages/plugins/swr/tests/react-hooks.test.tsx +++ b/packages/plugins/swr/tests/react-hooks.test.tsx @@ -656,4 +656,100 @@ describe('SWR React Hooks Test', () => { expect(cacheData?.data).toHaveLength(0); }); }); + + it('optimistic create with custom provider', async () => { + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook(() => useModelQuery('User', 'findMany'), { wrapper }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }) + .persist(); + + const { result: useMutateResult1 } = renderHook( + () => + useModelMutation('User', 'POST', 'create', modelMeta, { + optimisticUpdate: true, + revalidate: false, + optimisticDataProvider: ({ queryModel, queryOperation }) => { + if (queryModel === 'User' && queryOperation === 'findMany') { + return { kind: 'Skip' }; + } else { + return { kind: 'ProceedDefault' }; + } + }, + }), + { + wrapper, + } + ); + + await waitFor(async () => { + const { trigger } = useMutateResult1.current; + const r = await trigger({ data: { name: 'foo' } }); + console.log('Mutate result:', r); + }); + + const { result: cacheResult1 } = renderHook(() => useSWRConfig()); + // cache should not update + await waitFor(() => { + const cache = cacheResult1.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany')); + expect(cacheData?.data).toHaveLength(0); + }); + + const { result: useMutateResult2 } = renderHook( + () => + useModelMutation('User', 'POST', 'create', modelMeta, { + optimisticUpdate: true, + revalidate: false, + optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => { + if (queryModel === 'User' && queryOperation === 'findMany') { + return { + kind: 'Update', + data: [ + ...currentData, + { id: 100, name: mutationArgs.data.name + 'hooray', $optimistic: true }, + ], + }; + } else { + return { kind: 'ProceedDefault' }; + } + }, + }), + { + wrapper, + } + ); + + await waitFor(async () => { + const { trigger } = useMutateResult2.current; + const r = await trigger({ data: { name: 'foo' } }); + console.log('Mutate result:', r); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + // cache should update + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany')); + expect(cacheData?.data).toHaveLength(1); + expect(cacheData?.data[0].name).toBe('foohooray'); + }); + }); }); diff --git a/packages/plugins/swr/tests/swr.test.ts b/packages/plugins/swr/tests/swr.test.ts index d12c3b37b..7c26f18bd 100644 --- a/packages/plugins/swr/tests/swr.test.ts +++ b/packages/plugins/swr/tests/swr.test.ts @@ -1,7 +1,9 @@ /// import { loadSchema, normalizePath } from '@zenstackhq/testtools'; +import fs from 'fs'; import path from 'path'; +import tmp from 'tmp'; describe('SWR Plugin Tests', () => { let origDir: string; @@ -69,4 +71,59 @@ ${sharedModel} } ); }); + + it('clear output', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.mkdirSync(path.join(projectDir, 'swr'), { recursive: true }); + fs.writeFileSync(path.join(projectDir, 'swr', 'test.txt'), 'hello'); + + await loadSchema( + ` + plugin swr { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/swr' + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + password String @omit + } + `, + { + pushDb: false, + projectDir, + extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`], + } + ); + + expect(fs.existsSync(path.join(projectDir, 'swr', 'test.txt'))).toBeFalsy(); + }); + + it('existing output as file', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.writeFileSync(path.join(projectDir, 'swr'), 'hello'); + + await expect( + loadSchema( + ` + plugin swr { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/swr' + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String + password String @omit + } + `, + { pushDb: false, projectDir, extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`] } + ) + ).rejects.toThrow('already exists and is not a directory'); + }); }); diff --git a/packages/plugins/swr/tests/test-model-meta.ts b/packages/plugins/swr/tests/test-model-meta.ts index 9eb4c0e2f..001d773a9 100644 --- a/packages/plugins/swr/tests/test-model-meta.ts +++ b/packages/plugins/swr/tests/test-model-meta.ts @@ -11,42 +11,46 @@ const fieldDefaults = { }; export const modelMeta: ModelMeta = { - fields: { + models: { user: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - name: { ...fieldDefaults, type: 'String', name: 'name' }, - email: { ...fieldDefaults, type: 'String', name: 'name', isOptional: false }, - posts: { - ...fieldDefaults, - type: 'Post', - isDataModel: true, - isArray: true, - name: 'posts', + name: 'user', + fields: { + id: { + ...fieldDefaults, + type: 'String', + isId: true, + name: 'id', + isOptional: false, + }, + name: { ...fieldDefaults, type: 'String', name: 'name' }, + email: { ...fieldDefaults, type: 'String', name: 'name', isOptional: false }, + posts: { + ...fieldDefaults, + type: 'Post', + isDataModel: true, + isArray: true, + name: 'posts', + }, }, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, post: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, + name: 'post', + fields: { + id: { + ...fieldDefaults, + type: 'String', + isId: true, + name: 'id', + isOptional: false, + }, + title: { ...fieldDefaults, type: 'String', name: 'title' }, + owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, + ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, }, - title: { ...fieldDefaults, type: 'String', name: 'title' }, - owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, - ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, }, - uniqueConstraints: { - user: { id: { name: 'id', fields: ['id'] } }, - post: { id: { name: 'id', fields: ['id'] } }, - }, deleteCascade: { user: ['Post'], }, diff --git a/packages/plugins/tanstack-query/CHANGELOG.md b/packages/plugins/tanstack-query/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/plugins/tanstack-query/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 25a5a94b7..fb61ba074 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "1.12.4", + "version": "2.0.0", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { @@ -80,7 +80,6 @@ "author": "ZenStack Team", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "change-case": "^4.1.2", @@ -90,6 +89,7 @@ "semver": "^7.5.2", "superjson": "^1.11.0", "ts-morph": "^16.0.0", + "ts-pattern": "^4.3.0", "upper-case-first": "^2.0.2" }, "devDependencies": { @@ -99,7 +99,6 @@ "@tanstack/svelte-query-v5": "npm:@tanstack/svelte-query@^5.0.0", "@tanstack/vue-query": "^4.37.0", "@testing-library/react": "^14.0.0", - "@types/nock": "^11.1.0", "@types/react": "18.2.0", "@types/semver": "^7.3.13", "@types/tmp": "^0.2.3", diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 89a14ca05..7666cb742 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -1,22 +1,21 @@ -import type { DMMF } from '@prisma/generator-helper'; import { PluginError, PluginOptions, createProject, + ensureEmptyDir, generateModelMeta, getDataModels, - getPrismaClientImportSpec, - getPrismaVersion, requireOption, resolvePath, saveProject, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; -import semver from 'semver'; import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; +import { match } from 'ts-pattern'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; @@ -25,30 +24,26 @@ type TargetFramework = (typeof supportedTargets)[number]; type TanStackVersion = 'v4' | 'v5'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = requireOption(options, 'output', name); - outDir = resolvePath(outDir, options); - const project = createProject(); const warnings: string[] = []; const models = getDataModels(model); const target = requireOption(options, 'target', name); if (!supportedTargets.includes(target)) { - throw new PluginError( - options.name, - `Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}` - ); + throw new PluginError(name, `Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}`); } - const version = typeof options.version === 'string' ? options.version : 'v4'; + const version = typeof options.version === 'string' ? options.version : 'v5'; if (version !== 'v4' && version !== 'v5') { - throw new PluginError(options.name, `Unsupported version "${version}": use "v4" or "v5"`); + throw new PluginError(name, `Unsupported version "${version}": use "v4" or "v5"`); } + let outDir = requireOption(options, 'output', name); + outDir = resolvePath(outDir, options); + ensureEmptyDir(outDir); + await generateModelMeta(project, models, { output: path.join(outDir, '__model_meta.ts'), - compile: false, - preserveTsFiles: true, generateAttributes: false, }); @@ -60,11 +55,11 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. warnings.push(`Unable to find mapping for model ${dataModel.name}`); return; } - generateModelHooks(target, version, project, outDir, dataModel, mapping); + generateModelHooks(target, version, project, outDir, dataModel, mapping, options); }); await saveProject(project); - return warnings; + return { warnings }; } function generateQueryHook( @@ -135,15 +130,6 @@ function generateQueryHook( name: 'options?', type: optionsType, }, - ...(optimistic - ? [ - { - name: 'optimisticUpdate', - type: 'boolean', - initializer: 'true', - }, - ] - : []), ], isExported: true, }); @@ -157,7 +143,7 @@ function generateQueryHook( makeGetContext(target), `return use${generateMode}ModelQuery('${model}', \`\${endpoint}/${lowerCaseFirst( model - )}/${operation}\`, args, options, fetch${optimistic ? ', optimisticUpdate' : ''});`, + )}/${operation}\`, args, options, fetch);`, ]); } } @@ -194,16 +180,6 @@ function generateMutationHook( name: 'options?', type: nonGenericOptionsType, }, - { - name: 'invalidateQueries', - type: 'boolean', - initializer: 'true', - }, - { - name: 'optimisticUpdate', - type: 'boolean', - initializer: 'false', - }, ], }); @@ -220,7 +196,7 @@ function generateMutationHook( overrideReturnType ?? model }, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst( model - )}/${operation}\`, metadata, options, fetch, invalidateQueries, ${checkReadBack}, optimisticUpdate) + )}/${operation}\`, metadata, options, fetch, ${checkReadBack}) `, }, ], @@ -291,16 +267,16 @@ function generateModelHooks( project: Project, outDir: string, model: DataModel, - mapping: DMMF.ModelMapping + mapping: DMMF.ModelMapping, + options: PluginOptions ) { const modelNameCap = upperCaseFirst(model.name); - const prismaVersion = getPrismaVersion(); const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); sf.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(model.$container, outDir); + const prismaImport = getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma', model.name], isTypeOnly: true, @@ -421,11 +397,7 @@ function generateModelHooks( // groupBy if (mapping.groupBy) { - let useName = modelNameCap; - // prisma 4 and 5 different typing for "groupBy" and we have to deal with it separately - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - useName = model.name; - } + const useName = model.name; const returnType = `{} extends InputErrors ? Array & @@ -565,7 +537,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, - `import type { PickEnumerable, CheckSelect, QueryError } from '${runtimeImportBase}';`, + `import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '${runtimeImportBase}';`, `import metadata from './__model_meta';`, `type DefaultError = QueryError;`, ]; @@ -626,47 +598,58 @@ function makeQueryOptions( suspense: boolean, version: TanStackVersion ) { - switch (target) { - case 'react': - return infinite + let result = match(target) + .with('react', () => + infinite ? version === 'v4' ? `Omit, 'queryKey'>` : `Omit>, 'queryKey'>` - : `Omit, 'queryKey'>`; - case 'vue': { + : `Omit, 'queryKey'>` + ) + .with('vue', () => { const baseOption = `Omit, 'queryKey'>`; return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`; - } - case 'svelte': - return infinite + }) + .with('svelte', () => + infinite ? version === 'v4' ? `Omit, 'queryKey'>` : `StoreOrVal>, 'queryKey'>>` : version === 'v4' ? `Omit, 'queryKey'>` - : `StoreOrVal, 'queryKey'>>`; - default: + : `StoreOrVal, 'queryKey'>>` + ) + .otherwise(() => { throw new PluginError(name, `Unsupported target: ${target}`); + }); + + if (!infinite) { + // non-infinite queries support extra options like optimistic updates + result = `(${result} & ExtraQueryOptions)`; } + + return result; } function makeMutationOptions(target: string, returnType: string, argsType: string) { - switch (target) { - case 'react': - return `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`; - case 'vue': { + let result = match(target) + .with('react', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`) + .with('vue', () => { const baseOption = `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`; return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`; - } - case 'svelte': - return `MutationOptions<${returnType}, DefaultError, ${argsType}>`; - default: + }) + .with('svelte', () => `MutationOptions<${returnType}, DefaultError, ${argsType}>`) + .otherwise(() => { throw new PluginError(name, `Unsupported target: ${target}`); - } + }); + + result = `(${result} & ExtraMutationOptions)`; + + return result; } function makeRuntimeImportBase(version: TanStackVersion) { diff --git a/packages/plugins/tanstack-query/src/index.ts b/packages/plugins/tanstack-query/src/index.ts index 181727a02..eb315e00c 100644 --- a/packages/plugins/tanstack-query/src/index.ts +++ b/packages/plugins/tanstack-query/src/index.ts @@ -1,10 +1,13 @@ -import type { DMMF } from '@prisma/generator-helper'; -import type { PluginOptions } from '@zenstackhq/sdk'; -import type { Model } from '@zenstackhq/sdk/ast'; +import type { PluginFunction } from '@zenstackhq/sdk'; import { generate } from './generator'; export const name = 'Tanstack Query'; -export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { +const run: PluginFunction = async (model, options, dmmf) => { + if (!dmmf) { + throw new Error('DMMF is required'); + } return generate(model, options, dmmf); -} +}; + +export default run; diff --git a/packages/plugins/tanstack-query/src/runtime-v5/index.ts b/packages/plugins/tanstack-query/src/runtime-v5/index.ts index 2954d4683..ee494ca7d 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/index.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/index.ts @@ -1,2 +1,8 @@ +export { + getQueryKey, + type ExtraMutationOptions, + type ExtraQueryOptions, + type FetchFn, + type QueryError, +} from '../runtime/common'; export * from '../runtime/prisma-types'; -export { type FetchFn, type QueryError, getQueryKey } from '../runtime/common'; diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index 92194535f..b169ce4fa 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -24,6 +24,8 @@ import { setupInvalidation, setupOptimisticUpdate, type APIContext, + type ExtraMutationOptions, + type ExtraQueryOptions, type FetchFn, } from '../runtime/common'; @@ -56,20 +58,21 @@ export const Provider = RequestHandlerContext.Provider; * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The react-query options object * @param fetch The fetch function to use for sending the HTTP request - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useQuery hook */ export function useModelQuery( model: string, url: string, args?: unknown, - options?: Omit, 'queryKey'>, - fetch?: FetchFn, - optimisticUpdate = false + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn ) { const reqUrl = makeUrl(url, args); return useQuery({ - queryKey: getQueryKey(model, url, args, false, optimisticUpdate), + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }), queryFn: () => fetcher(reqUrl, undefined, fetch, false), ...options, }); @@ -83,20 +86,21 @@ export function useModelQuery( * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The react-query options object * @param fetch The fetch function to use for sending the HTTP request - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useSuspenseQuery hook */ export function useSuspenseModelQuery( model: string, url: string, args?: unknown, - options?: Omit, 'queryKey'>, - fetch?: FetchFn, - optimisticUpdate = false + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn ) { const reqUrl = makeUrl(url, args); return useSuspenseQuery({ - queryKey: getQueryKey(model, url, args, false, optimisticUpdate), + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }), queryFn: () => fetcher(reqUrl, undefined, fetch, false), ...options, }); @@ -120,7 +124,7 @@ export function useInfiniteModelQuery( fetch?: FetchFn ) { return useInfiniteQuery({ - queryKey: getQueryKey(model, url, args, true), + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), queryFn: ({ pageParam }) => { return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); }, @@ -146,7 +150,7 @@ export function useSuspenseInfiniteModelQuery( fetch?: FetchFn ) { return useSuspenseInfiniteQuery({ - queryKey: getQueryKey(model, url, args, true), + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), queryFn: ({ pageParam }) => { return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); }, @@ -163,9 +167,7 @@ export function useSuspenseInfiniteModelQuery( * @param modelMeta The model metadata. * @param options The react-query options. * @param fetch The fetch function to use for sending the HTTP request - * @param invalidateQueries Whether to invalidate queries after mutation. * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @param optimisticUpdate Whether to enable automatic optimistic update */ export function useModelMutation< TArgs, @@ -178,11 +180,9 @@ export function useModelMutation< method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'> & ExtraMutationOptions, fetch?: FetchFn, - invalidateQueries = true, - checkReadBack?: C, - optimisticUpdate = false + checkReadBack?: C ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => { @@ -201,6 +201,8 @@ export function useModelMutation< const finalOptions = { ...options, mutationFn }; const operation = url.split('/').pop(); + const invalidateQueries = options?.invalidateQueries !== false; + const optimisticUpdate = !!options?.optimisticUpdate; if (operation) { const { logging } = useContext(RequestHandlerContext); diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts index 7de2202d6..1c58f83be 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts @@ -22,6 +22,8 @@ import { marshal, setupInvalidation, setupOptimisticUpdate, + type ExtraMutationOptions, + type ExtraQueryOptions, type FetchFn, } from '../runtime/common'; @@ -55,19 +57,20 @@ export function getHooksContext() { * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The svelte-query options object * @param fetch The fetch function to use for sending the HTTP request - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useQuery hook */ export function useModelQuery( model: string, url: string, args?: unknown, - options?: StoreOrVal, 'queryKey'>>, - fetch?: FetchFn, - optimisticUpdate = false + options?: StoreOrVal, 'queryKey'>> & ExtraQueryOptions, + fetch?: FetchFn ) { const reqUrl = makeUrl(url, args); - const queryKey = getQueryKey(model, url, args, false, optimisticUpdate); + const queryKey = getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }); const queryFn = () => fetcher(reqUrl, undefined, fetch, false); let mergedOpt: any; @@ -107,7 +110,7 @@ export function useInfiniteModelQuery( options: StoreOrVal>, 'queryKey'>>, fetch?: FetchFn ) { - const queryKey = getQueryKey(model, url, args, true); + const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); const queryFn = ({ pageParam }: { pageParam: unknown }) => fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); @@ -144,7 +147,6 @@ function isStore(opt: unknown): opt is Readable { * @param modelMeta The model metadata. * @param url The request URL. * @param options The svelte-query options. - * @param invalidateQueries Whether to invalidate queries after mutation. * @returns useMutation hooks */ export function useModelMutation< @@ -158,11 +160,9 @@ export function useModelMutation< method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'> & ExtraMutationOptions, fetch?: FetchFn, - invalidateQueries = true, - checkReadBack?: C, - optimisticUpdate = false + checkReadBack?: C ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => { @@ -181,6 +181,8 @@ export function useModelMutation< const finalOptions = { ...options, mutationFn }; const operation = url.split('/').pop(); + const invalidateQueries = options?.invalidateQueries !== false; + const optimisticUpdate = !!options?.optimisticUpdate; if (operation) { const { logging } = getContext(SvelteQueryContextKey); diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts index b0d45b246..40830b3ee 100644 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ b/packages/plugins/tanstack-query/src/runtime/common.ts @@ -40,6 +40,76 @@ export type QueryError = Error & { status?: number; }; +/** + * Result of optimistic data provider. + */ +export type OptimisticDataProviderResult = { + /** + * Kind of the result. + * - Update: use the `data` field to update the query cache. + * - Skip: skip the optimistic update for this query. + * - ProceedDefault: proceed with the default optimistic update. + */ + kind: 'Update' | 'Skip' | 'ProceedDefault'; + + /** + * Data to update the query cache. Only applicable if `kind` is 'Update'. + * + * If the data is an object with fields updated, it should have a `$optimistic` + * field set to `true`. If it's an array and an element object is created or updated, + * the element should have a `$optimistic` field set to `true`. + */ + data?: any; +}; + +/** + * Optimistic data provider. + * + * @param args Arguments. + * @param args.queryModel The model of the query. + * @param args.queryOperation The operation of the query, `findMany`, `count`, etc. + * @param args.queryArgs The arguments of the query. + * @param args.currentData The current cache data for the query. + * @param args.mutationArgs The arguments of the mutation. + */ +export type OptimisticDataProvider = (args: { + queryModel: string; + queryOperation: string; + queryArgs: any; + currentData: any; + mutationArgs: any; +}) => OptimisticDataProviderResult | Promise; + +/** + * Extra mutation options. + */ +export type ExtraMutationOptions = { + /** + * Whether to automatically invalidate queries potentially affected by the mutation. Defaults to `true`. + */ + invalidateQueries?: boolean; + + /** + * Whether to optimistically update queries potentially affected by the mutation. Defaults to `false`. + */ + optimisticUpdate?: boolean; + + /** + * A callback for computing optimistic update data for each query cache entry. + */ + optimisticDataProvider?: OptimisticDataProvider; +}; + +/** + * Extra query options. + */ +export type ExtraQueryOptions = { + /** + * Whether to opt-in to optimistic updates for this query. Defaults to `true`. + */ + optimisticUpdate?: boolean; +}; + /** * Context type for configuring the hooks. */ @@ -110,21 +180,24 @@ type QueryKey = [ * @param model Model name. * @param urlOrOperation Prisma operation (e.g, `findMany`) or request URL. If it's a URL, the last path segment will be used as the operation name. * @param args Prisma query arguments. - * @param infinite Whether the query is infinite. - * @param optimisticUpdate Whether the query is optimistically updated. + * @param options Query options, including `infinite` indicating if it's an infinite query (defaults to false), and `optimisticUpdate` indicating if optimistic updates are enabled (defaults to true). * @returns Query key */ export function getQueryKey( model: string, urlOrOperation: string, args: unknown, - infinite = false, - optimisticUpdate = false + options: { infinite: boolean; optimisticUpdate: boolean } = { infinite: false, optimisticUpdate: true } ): QueryKey { if (!urlOrOperation) { throw new Error('Invalid urlOrOperation'); } const operation = urlOrOperation.split('/').pop(); + + const infinite = options.infinite; + // infinite query doesn't support optimistic updates + const optimisticUpdate = options.infinite ? false : options.optimisticUpdate; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return [QUERY_KEY_PREFIX, model, operation!, args, { infinite, optimisticUpdate }]; } @@ -246,11 +319,14 @@ type QueryCache = { type SetCacheFunc = (queryKey: readonly unknown[], data: unknown) => void; +/** + * Sets up optimistic update and invalidation (after settled) for a mutation. + */ export function setupOptimisticUpdate( model: string, operation: string, modelMeta: ModelMeta, - options: MutationOptions, + options: MutationOptions & ExtraMutationOptions, queryCache: QueryCache, setCache: SetCacheFunc, invalidate?: InvalidateFunc, @@ -266,6 +342,7 @@ export function setupOptimisticUpdate( model, operation as PrismaWriteActionType, variables, + options, modelMeta, queryCache, setCache, @@ -296,6 +373,7 @@ async function optimisticUpdate( mutationModel: string, mutationOp: string, mutationArgs: any, + options: MutationOptions & ExtraMutationOptions, modelMeta: ModelMeta, queryCache: QueryCache, setCache: SetCacheFunc, @@ -314,7 +392,7 @@ async function optimisticUpdate( continue; } - const [_, queryModel, queryOp, _queryArgs, { optimisticUpdate }] = queryKey as QueryKey; + const [_, queryModel, queryOperation, queryArgs, { optimisticUpdate }] = queryKey as QueryKey; if (!optimisticUpdate) { if (logging) { console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to opt-out`); @@ -322,9 +400,35 @@ async function optimisticUpdate( continue; } + if (options.optimisticDataProvider) { + const providerResult = await options.optimisticDataProvider({ + queryModel, + queryOperation, + queryArgs, + currentData: data, + mutationArgs, + }); + + if (providerResult?.kind === 'Skip') { + // skip + if (logging) { + console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to provider`); + } + continue; + } else if (providerResult?.kind === 'Update') { + // update cache + if (logging) { + console.log(`Optimistically updating query ${JSON.stringify(queryKey)} due to provider`); + } + setCache(queryKey, providerResult.data); + continue; + } + } + + // proceed with default optimistic update const mutatedData = await applyMutation( queryModel, - queryOp, + queryOperation, data, mutationModel, mutationOp as PrismaWriteActionType, diff --git a/packages/plugins/tanstack-query/src/runtime/index.ts b/packages/plugins/tanstack-query/src/runtime/index.ts index 0894bc461..085fd5bf3 100644 --- a/packages/plugins/tanstack-query/src/runtime/index.ts +++ b/packages/plugins/tanstack-query/src/runtime/index.ts @@ -1,2 +1,8 @@ +export { + getQueryKey, + type ExtraMutationOptions, + type ExtraQueryOptions, + type FetchFn, + type QueryError, +} from './common'; export * from './prisma-types'; -export { type FetchFn, type QueryError, getQueryKey } from './common'; diff --git a/packages/plugins/tanstack-query/src/runtime/react.ts b/packages/plugins/tanstack-query/src/runtime/react.ts index 607b57430..30340d6f4 100644 --- a/packages/plugins/tanstack-query/src/runtime/react.ts +++ b/packages/plugins/tanstack-query/src/runtime/react.ts @@ -19,6 +19,8 @@ import { setupInvalidation, setupOptimisticUpdate, type APIContext, + type ExtraMutationOptions, + type ExtraQueryOptions, type FetchFn, } from './common'; @@ -52,20 +54,21 @@ export function getHooksContext() { * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The react-query options object * @param fetch The fetch function to use for sending the HTTP request - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useQuery hook */ export function useModelQuery( model: string, url: string, args?: unknown, - options?: Omit, 'queryKey'>, - fetch?: FetchFn, - optimisticUpdate = false + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn ) { const reqUrl = makeUrl(url, args); return useQuery({ - queryKey: getQueryKey(model, url, args, false, optimisticUpdate), + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }), queryFn: () => fetcher(reqUrl, undefined, fetch, false), ...options, }); @@ -89,7 +92,7 @@ export function useInfiniteModelQuery( fetch?: FetchFn ) { return useInfiniteQuery({ - queryKey: getQueryKey(model, url, args, true), + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), queryFn: ({ pageParam }) => { return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); }, @@ -105,9 +108,7 @@ export function useInfiniteModelQuery( * @param modelMeta The model metadata. * @param url The request URL. * @param options The react-query options. - * @param invalidateQueries Whether to invalidate queries after mutation. * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useMutation hooks */ export function useModelMutation< @@ -121,11 +122,9 @@ export function useModelMutation< method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'> & ExtraMutationOptions, fetch?: FetchFn, - invalidateQueries = true, - checkReadBack?: C, - optimisticUpdate = false + checkReadBack?: C ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => { @@ -144,6 +143,9 @@ export function useModelMutation< const finalOptions = { ...options, mutationFn }; const operation = url.split('/').pop(); + const invalidateQueries = options?.invalidateQueries !== false; + const optimisticUpdate = !!options?.optimisticUpdate; + if (operation) { const { logging } = useContext(RequestHandlerContext); if (invalidateQueries) { diff --git a/packages/plugins/tanstack-query/src/runtime/svelte.ts b/packages/plugins/tanstack-query/src/runtime/svelte.ts index dbd0342aa..54f72cd23 100644 --- a/packages/plugins/tanstack-query/src/runtime/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime/svelte.ts @@ -19,6 +19,8 @@ import { marshal, setupInvalidation, setupOptimisticUpdate, + type ExtraMutationOptions, + type ExtraQueryOptions, type FetchFn, } from './common'; @@ -52,20 +54,21 @@ export function getHooksContext() { * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The svelte-query options object * @param fetch The fetch function to use for sending the HTTP request - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useQuery hook */ export function useModelQuery( model: string, url: string, args?: unknown, - options?: Omit, 'queryKey'>, - fetch?: FetchFn, - optimisticUpdate = false + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn ) { const reqUrl = makeUrl(url, args); return createQuery({ - queryKey: getQueryKey(model, url, args, false, optimisticUpdate), + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }), queryFn: () => fetcher(reqUrl, undefined, fetch, false), ...options, }); @@ -89,7 +92,7 @@ export function useInfiniteModelQuery( fetch?: FetchFn ) { return createInfiniteQuery({ - queryKey: getQueryKey(model, url, args, true), + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), queryFn: ({ pageParam }) => fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false), ...options, @@ -104,9 +107,7 @@ export function useInfiniteModelQuery( * @param modelMeta The model metadata. * @param url The request URL. * @param options The svelte-query options. - * @param invalidateQueries Whether to invalidate queries after mutation. * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @param optimisticUpdate Whether to enable automatic optimistic update. * @returns useMutation hooks */ export function useModelMutation< @@ -120,11 +121,9 @@ export function useModelMutation< method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'> & ExtraMutationOptions, fetch?: FetchFn, - invalidateQueries = true, - checkReadBack?: C, - optimisticUpdate = false + checkReadBack?: C ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => { @@ -143,6 +142,9 @@ export function useModelMutation< const finalOptions = { ...options, mutationFn }; const operation = url.split('/').pop(); + const invalidateQueries = options?.invalidateQueries !== false; + const optimisticUpdate = !!options?.optimisticUpdate; + if (operation) { const { logging } = getContext(SvelteQueryContextKey); diff --git a/packages/plugins/tanstack-query/src/runtime/vue.ts b/packages/plugins/tanstack-query/src/runtime/vue.ts index 4bd3ebf4b..016414722 100644 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime/vue.ts @@ -21,6 +21,8 @@ import { marshal, setupInvalidation, setupOptimisticUpdate, + type ExtraMutationOptions, + type ExtraQueryOptions, type FetchFn, } from './common'; @@ -55,7 +57,6 @@ export function getHooksContext() { * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The vue-query options object * @param fetch The fetch function to use for sending the HTTP request - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useQuery hook */ export function useModelQuery( @@ -63,20 +64,25 @@ export function useModelQuery( url: string, args?: MaybeRefOrGetter | ComputedRef, options?: - | MaybeRefOrGetter, 'queryKey'>> - | ComputedRef, 'queryKey'>>, - fetch?: FetchFn, - optimisticUpdate = false + | MaybeRefOrGetter, 'queryKey'> & ExtraQueryOptions> + | ComputedRef, 'queryKey'> & ExtraQueryOptions>, + fetch?: FetchFn ) { const queryOptions = computed(() => { + const optionsValue = toValue< + (Omit, 'queryKey'> & ExtraQueryOptions) | undefined + >(options); return { - queryKey: getQueryKey(model, url, toValue(args), false, optimisticUpdate), + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: optionsValue?.optimisticUpdate !== false, + }), queryFn: ({ queryKey }: { queryKey: QueryKey }) => { const [_prefix, _model, _op, args] = queryKey; const reqUrl = makeUrl(url, toValue(args)); return fetcher(reqUrl, undefined, fetch, false); }, - ...toValue(options), + ...optionsValue, }; }); return useQuery(queryOptions); @@ -103,7 +109,7 @@ export function useInfiniteModelQuery( ) { // CHECKME: vue-query's `useInfiniteQuery`'s input typing seems wrong const queryOptions: any = computed(() => ({ - queryKey: getQueryKey(model, url, toValue(args), true), + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), queryFn: ({ queryKey, pageParam }: { queryKey: QueryKey; pageParam?: unknown }) => { const [_prefix, _model, _op, args] = queryKey; const reqUrl = makeUrl(url, pageParam ?? toValue(args)); @@ -124,9 +130,7 @@ export function useInfiniteModelQuery( * @param url The request URL. * @param options The vue-query options. * @param fetch The fetch function to use for sending the HTTP request - * @param invalidateQueries Whether to invalidate queries after mutation. * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useMutation hooks */ export function useModelMutation< @@ -141,12 +145,12 @@ export function useModelMutation< url: string, modelMeta: ModelMeta, options?: - | MaybeRefOrGetter, 'mutationFn'>> - | ComputedRef, 'mutationFn'>>, + | MaybeRefOrGetter< + Omit, 'mutationFn'> & ExtraMutationOptions + > + | ComputedRef, 'mutationFn'> & ExtraMutationOptions>, fetch?: FetchFn, - invalidateQueries = true, - checkReadBack?: C, - optimisticUpdate = false + checkReadBack?: C ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => { @@ -163,9 +167,15 @@ export function useModelMutation< return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; }; + const optionsValue = toValue< + (Omit, 'mutationFn'> & ExtraMutationOptions) | undefined + >(options); // TODO: figure out the typing problem - const finalOptions: any = computed(() => ({ ...toValue(options), mutationFn })); + const finalOptions: any = computed(() => ({ ...optionsValue, mutationFn })); const operation = url.split('/').pop(); + const invalidateQueries = optionsValue?.invalidateQueries !== false; + const optimisticUpdate = !!optionsValue?.optimisticUpdate; + if (operation) { const { logging } = getHooksContext(); if (invalidateQueries) { diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index 824174ba5..3dfb2dec0 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -1,7 +1,9 @@ /// import { loadSchema, normalizePath } from '@zenstackhq/testtools'; +import fs from 'fs'; import path from 'path'; +import tmp from 'tmp'; describe('Tanstack Query Plugin Tests', () => { let origDir: string; @@ -53,6 +55,7 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'react' + version = 'v4' } ${sharedModel} @@ -61,7 +64,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@4.29.7'], - copyDependencies: [`${path.join(__dirname, '..')}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -74,7 +77,6 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'react' - version = 'v5' } ${sharedModel} @@ -83,7 +85,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@^5.0.0'], - copyDependencies: [`${path.join(__dirname, '..')}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -96,6 +98,7 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'vue' + version = 'v4' } ${sharedModel} @@ -104,7 +107,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@4.37.0'], - copyDependencies: [`${path.join(__dirname, '..')}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -117,7 +120,6 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'vue' - version = 'v5' } ${sharedModel} @@ -126,7 +128,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@latest'], - copyDependencies: [`${path.join(__dirname, '..')}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -139,6 +141,7 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'svelte' + version = 'v4' } ${sharedModel} @@ -147,7 +150,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'], - copyDependencies: [`${path.join(__dirname, '..')}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -160,7 +163,6 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'svelte' - version = 'v5' } ${sharedModel} @@ -169,9 +171,66 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@^5.0.0'], - copyDependencies: [`${path.join(__dirname, '..')}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); }); + + it('clear output', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.mkdirSync(path.join(projectDir, 'tanstack'), { recursive: true }); + fs.writeFileSync(path.join(projectDir, 'tanstack', 'test.txt'), 'hello'); + + await loadSchema( + ` + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/tanstack' + target = 'react' + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + password String @omit + } + `, + { + pushDb: false, + projectDir, + extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`], + } + ); + + expect(fs.existsSync(path.join(projectDir, 'tanstack', 'test.txt'))).toBeFalsy(); + }); + + it('existing output as file', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.writeFileSync(path.join(projectDir, 'tanstack'), 'hello'); + + await expect( + loadSchema( + ` + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/tanstack' + target = 'react' + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String + password String @omit + } + `, + { pushDb: false, projectDir, extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`] } + ) + ).rejects.toThrow('already exists and is not a directory'); + }); }); diff --git a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx index 3c4f32890..d5e23c374 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx @@ -205,7 +205,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), { wrapper, } @@ -223,17 +223,10 @@ describe('Tanstack Query React Hooks V5 Test', () => { const { result: mutationResult } = renderHook( () => - useModelMutation( - 'User', - 'POST', - makeUrl('User', 'create'), - modelMeta, - undefined, - undefined, - false, - undefined, - true - ), + useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), { wrapper, } @@ -242,7 +235,9 @@ describe('Tanstack Query React Hooks V5 Test', () => { act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined, false, true)); + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); expect(cacheData).toHaveLength(1); expect(cacheData[0].$optimistic).toBe(true); expect(cacheData[0].id).toBeTruthy(); @@ -264,7 +259,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), { wrapper, } @@ -282,17 +277,10 @@ describe('Tanstack Query React Hooks V5 Test', () => { const { result: mutationResult } = renderHook( () => - useModelMutation( - 'User', - 'POST', - makeUrl('User', 'createMany'), - modelMeta, - undefined, - undefined, - false, - undefined, - true - ), + useModelMutation('User', 'POST', makeUrl('User', 'createMany'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), { wrapper, } @@ -301,7 +289,9 @@ describe('Tanstack Query React Hooks V5 Test', () => { act(() => mutationResult.current.mutate({ data: [{ name: 'foo' }, { name: 'bar' }] })); await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined, false, true)); + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); expect(cacheData).toHaveLength(2); }); }); @@ -409,7 +399,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), { wrapper, } @@ -427,17 +417,10 @@ describe('Tanstack Query React Hooks V5 Test', () => { const { result: mutationResult } = renderHook( () => - useModelMutation( - 'User', - 'PUT', - makeUrl('User', 'update'), - modelMeta, - undefined, - undefined, - false, - undefined, - true - ), + useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), { wrapper, } @@ -446,7 +429,9 @@ describe('Tanstack Query React Hooks V5 Test', () => { act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs, false, true)); + const cacheData = queryClient.getQueryData( + getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) + ); expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); }); }); @@ -508,7 +493,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), { wrapper, } @@ -526,17 +511,10 @@ describe('Tanstack Query React Hooks V5 Test', () => { const { result: mutationResult } = renderHook( () => - useModelMutation( - 'User', - 'DELETE', - makeUrl('User', 'delete'), - modelMeta, - undefined, - undefined, - false, - undefined, - true - ), + useModelMutation('User', 'DELETE', makeUrl('User', 'delete'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), { wrapper, } @@ -545,7 +523,9 @@ describe('Tanstack Query React Hooks V5 Test', () => { act(() => mutationResult.current.mutate({ where: { id: '1' } })); await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined, false, true)); + const cacheData = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); expect(cacheData).toHaveLength(0); }); }); @@ -638,4 +618,101 @@ describe('Tanstack Query React Hooks V5 Test', () => { expect(cacheData).toHaveLength(2); }); }); + + it('optimistic create with custom provider', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }) + .persist(); + + const { result: mutationResult1 } = renderHook( + () => + useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + optimisticDataProvider: ({ queryModel, queryOperation }) => { + if (queryModel === 'User' && queryOperation === 'findMany') { + return { kind: 'Skip' }; + } else { + return { kind: 'ProceedDefault' }; + } + }, + }), + { + wrapper, + } + ); + + act(() => mutationResult1.current.mutate({ data: { name: 'foo' } })); + + // cache should not update + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(0); + }); + + const { result: mutationResult2 } = renderHook( + () => + useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => { + if (queryModel === 'User' && queryOperation === 'findMany') { + return { + kind: 'Update', + data: [ + ...currentData, + { id: 100, name: mutationArgs.data.name + 'hooray', $optimistic: true }, + ], + }; + } else { + return { kind: 'ProceedDefault' }; + } + }, + }), + { + wrapper, + } + ); + + act(() => mutationResult2.current.mutate({ data: { name: 'foo' } })); + + // cache should update + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].name).toBe('foohooray'); + }); + }); }); diff --git a/packages/plugins/tanstack-query/tests/react-hooks.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks.test.tsx index a14f8bd06..7bd952fad 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks.test.tsx @@ -14,7 +14,7 @@ import { getQueryKey } from '../src/runtime/common'; import { RequestHandlerContext, useModelMutation, useModelQuery } from '../src/runtime/react'; import { modelMeta } from './test-model-meta'; -describe('Tanstack Query React Hooks Test', () => { +describe('Tanstack Query React Hooks V4 Test', () => { function createWrapper() { const queryClient = new QueryClient(); const Provider = RequestHandlerContext.Provider; @@ -162,7 +162,7 @@ describe('Tanstack Query React Hooks Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), { wrapper, } @@ -185,11 +185,8 @@ describe('Tanstack Query React Hooks Test', () => { 'POST', makeUrl('User', 'create'), modelMeta, - undefined, - undefined, - false, - undefined, - true + { optimisticUpdate: true, invalidateQueries: false }, + undefined ), { wrapper, @@ -199,7 +196,7 @@ describe('Tanstack Query React Hooks Test', () => { act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined, false, true)); + const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); expect(cacheData).toHaveLength(1); expect(cacheData[0].$optimistic).toBe(true); expect(cacheData[0].id).toBeTruthy(); @@ -221,7 +218,7 @@ describe('Tanstack Query React Hooks Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), { wrapper, } @@ -244,11 +241,8 @@ describe('Tanstack Query React Hooks Test', () => { 'POST', makeUrl('User', 'createMany'), modelMeta, - undefined, - undefined, - false, - undefined, - true + { optimisticUpdate: true, invalidateQueries: false }, + undefined ), { wrapper, @@ -258,7 +252,7 @@ describe('Tanstack Query React Hooks Test', () => { act(() => mutationResult.current.mutate({ data: [{ name: 'foo' }, { name: 'bar' }] })); await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined, false, true)); + const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); expect(cacheData).toHaveLength(2); }); }); @@ -322,7 +316,7 @@ describe('Tanstack Query React Hooks Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), { wrapper, } @@ -345,11 +339,8 @@ describe('Tanstack Query React Hooks Test', () => { 'PUT', makeUrl('User', 'update'), modelMeta, - undefined, - undefined, - false, - undefined, - true + { optimisticUpdate: true, invalidateQueries: false }, + undefined ), { wrapper, @@ -359,7 +350,7 @@ describe('Tanstack Query React Hooks Test', () => { act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs, false, true)); + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); }); }); @@ -421,7 +412,7 @@ describe('Tanstack Query React Hooks Test', () => { .persist(); const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, undefined, undefined, true), + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), { wrapper, } @@ -444,11 +435,8 @@ describe('Tanstack Query React Hooks Test', () => { 'DELETE', makeUrl('User', 'delete'), modelMeta, - undefined, - undefined, - false, - undefined, - true + { optimisticUpdate: true, invalidateQueries: false }, + undefined ), { wrapper, @@ -458,7 +446,7 @@ describe('Tanstack Query React Hooks Test', () => { act(() => mutationResult.current.mutate({ where: { id: '1' } })); await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined, false, true)); + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); expect(cacheData).toHaveLength(0); }); }); diff --git a/packages/plugins/tanstack-query/tests/test-model-meta.ts b/packages/plugins/tanstack-query/tests/test-model-meta.ts index 9eb4c0e2f..001d773a9 100644 --- a/packages/plugins/tanstack-query/tests/test-model-meta.ts +++ b/packages/plugins/tanstack-query/tests/test-model-meta.ts @@ -11,42 +11,46 @@ const fieldDefaults = { }; export const modelMeta: ModelMeta = { - fields: { + models: { user: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - name: { ...fieldDefaults, type: 'String', name: 'name' }, - email: { ...fieldDefaults, type: 'String', name: 'name', isOptional: false }, - posts: { - ...fieldDefaults, - type: 'Post', - isDataModel: true, - isArray: true, - name: 'posts', + name: 'user', + fields: { + id: { + ...fieldDefaults, + type: 'String', + isId: true, + name: 'id', + isOptional: false, + }, + name: { ...fieldDefaults, type: 'String', name: 'name' }, + email: { ...fieldDefaults, type: 'String', name: 'name', isOptional: false }, + posts: { + ...fieldDefaults, + type: 'Post', + isDataModel: true, + isArray: true, + name: 'posts', + }, }, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, post: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, + name: 'post', + fields: { + id: { + ...fieldDefaults, + type: 'String', + isId: true, + name: 'id', + isOptional: false, + }, + title: { ...fieldDefaults, type: 'String', name: 'title' }, + owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, + ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, }, - title: { ...fieldDefaults, type: 'String', name: 'title' }, - owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, - ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, }, - uniqueConstraints: { - user: { id: { name: 'id', fields: ['id'] } }, - post: { id: { name: 'id', fields: ['id'] } }, - }, deleteCascade: { user: ['Post'], }, diff --git a/packages/plugins/tanstack-query/tsconfig.json b/packages/plugins/tanstack-query/tsconfig.json index 9e4f772c5..c51ec9bae 100644 --- a/packages/plugins/tanstack-query/tsconfig.json +++ b/packages/plugins/tanstack-query/tsconfig.json @@ -6,5 +6,5 @@ "jsx": "react" }, "include": ["src/**/*.ts"], - "exclude": ["src/runtime"] + "exclude": ["src/runtime", "src/runtime-v5"] } diff --git a/packages/plugins/trpc/CHANGELOG.md b/packages/plugins/trpc/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/plugins/trpc/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 880ac1066..ffa0a6e89 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.12.4", + "version": "2.0.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { @@ -20,11 +20,12 @@ "directory": "dist", "linkDirectory": true }, - "keywords": ["trpc"], + "keywords": [ + "trpc" + ], "author": "ZenStack Team", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/sdk": "workspace:*", "change-case": "^4.1.2", "lower-case-first": "^2.0.2", @@ -38,7 +39,9 @@ "@trpc/react-query": "^10.32.0", "@trpc/server": "^10.32.0", "@types/prettier": "^2.7.2", + "@types/tmp": "^0.2.3", "@zenstackhq/testtools": "workspace:*", - "next": "^13.4.7" + "next": "^13.4.7", + "tmp": "^0.2.3" } } diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 0d252cabc..cf57a1baf 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -1,16 +1,16 @@ -import type { DMMF } from '@prisma/generator-helper'; import { CrudFailureReason, PluginError, - PluginOptions, RUNTIME_PACKAGE, - getPrismaClientImportSpec, + ensureEmptyDir, parseOptionAsStrings, requireOption, resolvePath, saveProject, + type PluginOptions, } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; @@ -27,12 +27,8 @@ import { resolveModelsComments, } from './helpers'; import { project } from './project'; -import removeDir from './utils/removeDir'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = requireOption(options, 'output', name); - outDir = resolvePath(outDir, options); - // resolve "generateModels" option const generateModels = parseOptionAsStrings(options, 'generateModels', name); @@ -49,8 +45,9 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. throw new PluginError(name, `Option "zodSchemasImport" must be a string`); } - await fs.promises.mkdir(outDir, { recursive: true }); - await removeDir(outDir, true); + let outDir = requireOption(options, 'output', name); + outDir = resolvePath(outDir, options); + ensureEmptyDir(outDir); const prismaClientDmmf = dmmf; @@ -72,7 +69,8 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. generateModelActions, generateClientHelpers, model, - zodSchemasImport + zodSchemasImport, + options ); createHelper(outDir); @@ -85,8 +83,9 @@ function createAppRouter( hiddenModels: string[], generateModelActions: string[] | undefined, generateClientHelpers: string[] | undefined, - zmodel: Model, - zodSchemasImport: string + _zmodel: Model, + zodSchemasImport: string, + options: PluginOptions ) { const indexFile = path.resolve(outDir, 'routers', `index.ts`); const appRouter = project.createSourceFile(indexFile, undefined, { @@ -95,7 +94,7 @@ function createAppRouter( appRouter.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(zmodel, path.dirname(indexFile)); + const prismaImport = getPrismaClientImportSpec(path.dirname(indexFile), options); appRouter.addImportDeclarations([ { namedImports: [ @@ -169,8 +168,8 @@ function createAppRouter( outDir, generateModelActions, generateClientHelpers, - zmodel, - zodSchemasImport + zodSchemasImport, + options ); appRouter.addImportDeclaration({ @@ -239,8 +238,8 @@ function generateModelCreateRouter( outputDir: string, generateModelActions: string[] | undefined, generateClientHelpers: string[] | undefined, - zmodel: Model, - zodSchemasImport: string + zodSchemasImport: string, + options: PluginOptions ) { const modelRouter = project.createSourceFile(path.resolve(outputDir, 'routers', `${model}.router.ts`), undefined, { overwrite: true, @@ -258,7 +257,7 @@ function generateModelCreateRouter( generateRouterSchemaImport(modelRouter, zodSchemasImport); generateHelperImport(modelRouter); if (generateClientHelpers) { - generateRouterTypingImports(modelRouter, zmodel); + generateRouterTypingImports(modelRouter, options); } const createRouterFunc = modelRouter.addFunction({ diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index fba27ed84..947b96b98 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -1,6 +1,5 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginError, getPrismaClientImportSpec } from '@zenstackhq/sdk'; -import { Model } from '@zenstackhq/sdk/ast'; +import { PluginError, type PluginOptions } from '@zenstackhq/sdk'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; @@ -225,9 +224,9 @@ export function generateRouterTyping(writer: CodeBlockWriter, opType: string, mo }); } -export function generateRouterTypingImports(sourceFile: SourceFile, model: Model) { +export function generateRouterTypingImports(sourceFile: SourceFile, options: PluginOptions) { const importingDir = sourceFile.getDirectoryPath(); - const prismaImport = getPrismaClientImportSpec(model, importingDir); + const prismaImport = getPrismaClientImportSpec(importingDir, options); sourceFile.addStatements([ `import type { Prisma } from '${prismaImport}';`, `import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared';`, diff --git a/packages/plugins/trpc/src/index.ts b/packages/plugins/trpc/src/index.ts index 85d2a61d8..83125eb74 100644 --- a/packages/plugins/trpc/src/index.ts +++ b/packages/plugins/trpc/src/index.ts @@ -1,11 +1,14 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginOptions } from '@zenstackhq/sdk'; -import { Model } from '@zenstackhq/sdk/ast'; +import type { PluginFunction } from '@zenstackhq/sdk'; import { generate } from './generator'; export const name = 'tRPC'; export const dependencies = ['@core/zod']; -export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { +const run: PluginFunction = async (model, options, dmmf) => { + if (!dmmf) { + throw new Error('DMMF is required'); + } return generate(model, options, dmmf); -} +}; + +export default run; diff --git a/packages/plugins/trpc/src/utils/removeDir.ts b/packages/plugins/trpc/src/utils/removeDir.ts deleted file mode 100644 index 03f8d74f5..000000000 --- a/packages/plugins/trpc/src/utils/removeDir.ts +++ /dev/null @@ -1,15 +0,0 @@ -import path from 'path'; -import { promises as fs } from 'fs'; - -export default async function removeDir(dirPath: string, onlyContent: boolean) { - const dirEntries = await fs.readdir(dirPath, { withFileTypes: true }); - await Promise.all( - dirEntries.map(async (dirEntry) => { - const fullPath = path.join(dirPath, dirEntry.name); - return dirEntry.isDirectory() ? await removeDir(fullPath, false) : await fs.unlink(fullPath); - }) - ); - if (!onlyContent) { - await fs.rmdir(dirPath); - } -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore b/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore index b9331d6ce..e3f5e79f8 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore @@ -40,4 +40,5 @@ yarn-error.log* # typescript *.tsbuildinfo -package-lock.json \ No newline at end of file +package-lock.json +package.json \ No newline at end of file diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json index a9c99ebba..6098ca96b 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json @@ -13,7 +13,7 @@ "start": "next start" }, "dependencies": { - "@prisma/client": "^5.6.0", + "@prisma/client": "5.12.0", "@t3-oss/env-nextjs": "^0.7.1", "@tanstack/react-query": "^4.36.1", "@trpc/client": "^10.43.6", @@ -35,7 +35,7 @@ "@typescript-eslint/parser": "^6.11.0", "eslint": "^8.54.0", "eslint-config-next": "^14.0.4", - "prisma": "^5.6.0", + "prisma": "5.12.0", "typescript": "^5.1.6" }, "ct3aMetadata": { diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma index 2a0b2142a..a28fea9fb 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma @@ -4,28 +4,28 @@ ////////////////////////////////////////////////////////////////////////////////////////////// datasource db { - provider = "sqlite" - url = "file:./dev.db" + provider = "sqlite" + url = "file:./dev.db" } generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" } model User { - id Int @id() @default(autoincrement()) - email String @unique() - posts Post[] + id Int @id() @default(autoincrement()) + email String @unique() + posts Post[] } model Post { - id Int @id() @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int + id Int @id() @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt() + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int - @@index([name]) -} \ No newline at end of file + @@index([name]) +} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts index 982ab7980..fecac441c 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts @@ -10,7 +10,7 @@ export function createTRPCNext< TRouter extends AnyRouter, TPath extends string | undefined = undefined, TSSRContext extends NextPageContext = NextPageContext, - TFlags = null, + TFlags = null >(opts: Parameters[0]) { const r: CreateTRPCNext = _createTRPCNext(opts); return r as DeepOverrideAtPath, ClientType, TPath>; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts index 223fde54d..45a0df890 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts @@ -12,10 +12,10 @@ export type DeepOverride = T extends Primitive : R extends Primitive ? R : { - [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; - } & { - [K in Exclude]: R[K]; - }; + [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; + } & { + [K in Exclude]: R[K]; + }; /** * Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there, @@ -25,8 +25,8 @@ export type DeepOverrideAtPath : Path extends `${infer P1}.${infer P2}` ? P1 extends keyof T - ? Omit & Record>> - : never + ? Omit & Record>> + : never : Path extends keyof T ? Omit & Record> : never; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts index 45e24e20e..7f292fff2 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts @@ -31,6 +31,7 @@ export async function checkMutate(promise: Promise): Promise(promise: Promise): Promise { @@ -58,10 +59,11 @@ export async function checkRead(promise: Promise): Promise { code: 'BAD_REQUEST', message: err.message, cause: err, - }); + }) } } else { throw err; } } + } diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts index 62e570e6d..15408f3ef 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts @@ -1,96 +1,65 @@ /* eslint-disable */ -import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from '.'; +import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; import * as $Schema from '@zenstackhq/runtime/zod/input'; import { checkRead, checkMutate } from '../helper'; import type { Prisma } from '@prisma/client'; -import type { - UseTRPCMutationOptions, - UseTRPCMutationResult, - UseTRPCQueryOptions, - UseTRPCQueryResult, - UseTRPCInfiniteQueryOptions, - UseTRPCInfiniteQueryResult, -} from '@trpc/react-query/shared'; +import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; import type { TRPCClientErrorLike } from '@trpc/client'; import type { AnyRouter } from '@trpc/server'; -export default function createRouter( - router: RouterFactory, - procedure: ProcBuilder, -) { +export default function createRouter(router: RouterFactory, procedure: ProcBuilder) { return router({ - aggregate: procedure - .input($Schema.PostInputSchema.aggregate) - .query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), - - createMany: procedure - .input($Schema.PostInputSchema.createMany) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.createMany(input as any))), - - create: procedure - .input($Schema.PostInputSchema.create) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), - - deleteMany: procedure - .input($Schema.PostInputSchema.deleteMany) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.deleteMany(input as any))), - - delete: procedure - .input($Schema.PostInputSchema.delete) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.delete(input as any))), - - findFirst: procedure - .input($Schema.PostInputSchema.findFirst) - .query(({ ctx, input }) => checkRead(db(ctx).post.findFirst(input as any))), - - findFirstOrThrow: procedure - .input($Schema.PostInputSchema.findFirst) - .query(({ ctx, input }) => checkRead(db(ctx).post.findFirstOrThrow(input as any))), - - findMany: procedure - .input($Schema.PostInputSchema.findMany) - .query(({ ctx, input }) => checkRead(db(ctx).post.findMany(input as any))), - - findUnique: procedure - .input($Schema.PostInputSchema.findUnique) - .query(({ ctx, input }) => checkRead(db(ctx).post.findUnique(input as any))), - - findUniqueOrThrow: procedure - .input($Schema.PostInputSchema.findUnique) - .query(({ ctx, input }) => checkRead(db(ctx).post.findUniqueOrThrow(input as any))), - - groupBy: procedure - .input($Schema.PostInputSchema.groupBy) - .query(({ ctx, input }) => checkRead(db(ctx).post.groupBy(input as any))), - - updateMany: procedure - .input($Schema.PostInputSchema.updateMany) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.updateMany(input as any))), - - update: procedure - .input($Schema.PostInputSchema.update) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.update(input as any))), - - upsert: procedure - .input($Schema.PostInputSchema.upsert) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.upsert(input as any))), - - count: procedure - .input($Schema.PostInputSchema.count) - .query(({ ctx, input }) => checkRead(db(ctx).post.count(input as any))), - }); + + aggregate: procedure.input($Schema.PostInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), + + create: procedure.input($Schema.PostInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), + + deleteMany: procedure.input($Schema.PostInputSchema.deleteMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.deleteMany(input as any))), + + delete: procedure.input($Schema.PostInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.delete(input as any))), + + findFirst: procedure.input($Schema.PostInputSchema.findFirst).query(({ ctx, input }) => checkRead(db(ctx).post.findFirst(input as any))), + + findFirstOrThrow: procedure.input($Schema.PostInputSchema.findFirst).query(({ ctx, input }) => checkRead(db(ctx).post.findFirstOrThrow(input as any))), + + findMany: procedure.input($Schema.PostInputSchema.findMany).query(({ ctx, input }) => checkRead(db(ctx).post.findMany(input as any))), + + findUnique: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUnique(input as any))), + + findUniqueOrThrow: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUniqueOrThrow(input as any))), + + groupBy: procedure.input($Schema.PostInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).post.groupBy(input as any))), + + updateMany: procedure.input($Schema.PostInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.updateMany(input as any))), + + update: procedure.input($Schema.PostInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.update(input as any))), + + upsert: procedure.input($Schema.PostInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.upsert(input as any))), + + count: procedure.input($Schema.PostInputSchema.count).query(({ ctx, input }) => checkRead(db(ctx).post.count(input as any))), + + } + ); } export interface ClientType { aggregate: { + useQuery: >( input: Prisma.Subset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.GetPostAggregateType, + TRPCClientErrorLike + >; + }; createMany: { useMutation: ( @@ -116,134 +85,147 @@ export interface ClientType( - opts?: UseTRPCMutationOptions< - Prisma.PostCreateArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.PostGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostCreateArgs, + TRPCClientErrorLike, + Prisma.PostGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> + }; + }; deleteMany: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.PostDeleteManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.BatchPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, - ) => Promise; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostDeleteManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >,) => + Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise + }; + }; delete: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.PostDeleteArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.PostGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostDeleteArgs, + TRPCClientErrorLike, + Prisma.PostGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> + }; + }; findFirst: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.PostGetPayload, + TRPCClientErrorLike + >; + }; findFirstOrThrow: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.PostGetPayload, + TRPCClientErrorLike + >; + }; findMany: { + useQuery: >>( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions>, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions>, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions>, Error>, - ) => UseTRPCInfiniteQueryResult>, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions>, Error> + ) => UseTRPCInfiniteQueryResult< + Array>, + TRPCClientErrorLike + >; + }; findUnique: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.PostGetPayload, + TRPCClientErrorLike + >; + }; findUniqueOrThrow: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.PostGetPayload, + TRPCClientErrorLike + >; + }; groupBy: { - useQuery: < - T extends Prisma.PostGroupByArgs, + + useQuery: >, Prisma.Extends<'take', Prisma.Keys> >, OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, + ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } + : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, @@ -251,62 +233,62 @@ export interface ClientType, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [Error, 'Field ', P, ` in "having" needs to be provided in "by"`]; - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields], - TData = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, - >( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCQueryOptions< - string, - T, - {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + , TData = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors>( + input: Prisma.SubsetIntersection & InputErrors, + opts?: UseTRPCQueryOptions : InputErrors, TData, Error> + ) => UseTRPCQueryResult< TData, - Error - >, - ) => UseTRPCQueryResult>; - useInfiniteQuery: < - T extends Prisma.PostGroupByArgs, + TRPCClientErrorLike + >; + useInfiniteQuery: >, Prisma.Extends<'take', Prisma.Keys> >, OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, + ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } + : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, @@ -314,165 +296,130 @@ export interface ClientType, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [Error, 'Field ', P, ` in "having" needs to be provided in "by"`]; - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields], + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] >( input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions< - string, - T, - {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, - Error - >, + opts?: UseTRPCInfiniteQueryOptions : InputErrors, Error> ) => UseTRPCInfiniteQueryResult< {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, TRPCClientErrorLike >; + }; updateMany: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.PostUpdateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.BatchPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, - ) => Promise; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostUpdateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >,) => + Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise + }; + }; update: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.PostUpdateArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.PostGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostUpdateArgs, + TRPCClientErrorLike, + Prisma.PostGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> + }; + }; upsert: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.PostUpsertArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.PostGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostUpsertArgs, + TRPCClientErrorLike, + Prisma.PostGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> + }; + }; count: { - useQuery: < - T extends Prisma.PostCountArgs, - TData = 'select' extends keyof T - ? T['select'] extends true + + useQuery: + : number>( + input: Prisma.Subset, + opts?: UseTRPCQueryOptions - : number, - >( - input: Prisma.Subset, - opts?: UseTRPCQueryOptions< - string, - T, - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, + : number, TData, Error> + ) => UseTRPCQueryResult< TData, - Error - >, - ) => UseTRPCQueryResult>; + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions< - string, - T, - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, - Error - >, + opts?: UseTRPCInfiniteQueryOptions + : number, Error> ) => UseTRPCInfiniteQueryResult< 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number, TRPCClientErrorLike >; + }; } diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts index 4c686b057..cb9c8614b 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts @@ -1,96 +1,65 @@ /* eslint-disable */ -import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from '.'; +import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; import * as $Schema from '@zenstackhq/runtime/zod/input'; import { checkRead, checkMutate } from '../helper'; import type { Prisma } from '@prisma/client'; -import type { - UseTRPCMutationOptions, - UseTRPCMutationResult, - UseTRPCQueryOptions, - UseTRPCQueryResult, - UseTRPCInfiniteQueryOptions, - UseTRPCInfiniteQueryResult, -} from '@trpc/react-query/shared'; +import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; import type { TRPCClientErrorLike } from '@trpc/client'; import type { AnyRouter } from '@trpc/server'; -export default function createRouter( - router: RouterFactory, - procedure: ProcBuilder, -) { +export default function createRouter(router: RouterFactory, procedure: ProcBuilder) { return router({ - aggregate: procedure - .input($Schema.UserInputSchema.aggregate) - .query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), - - createMany: procedure - .input($Schema.UserInputSchema.createMany) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.createMany(input as any))), - - create: procedure - .input($Schema.UserInputSchema.create) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), - - deleteMany: procedure - .input($Schema.UserInputSchema.deleteMany) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.deleteMany(input as any))), - - delete: procedure - .input($Schema.UserInputSchema.delete) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.delete(input as any))), - - findFirst: procedure - .input($Schema.UserInputSchema.findFirst) - .query(({ ctx, input }) => checkRead(db(ctx).user.findFirst(input as any))), - - findFirstOrThrow: procedure - .input($Schema.UserInputSchema.findFirst) - .query(({ ctx, input }) => checkRead(db(ctx).user.findFirstOrThrow(input as any))), - - findMany: procedure - .input($Schema.UserInputSchema.findMany) - .query(({ ctx, input }) => checkRead(db(ctx).user.findMany(input as any))), - - findUnique: procedure - .input($Schema.UserInputSchema.findUnique) - .query(({ ctx, input }) => checkRead(db(ctx).user.findUnique(input as any))), - - findUniqueOrThrow: procedure - .input($Schema.UserInputSchema.findUnique) - .query(({ ctx, input }) => checkRead(db(ctx).user.findUniqueOrThrow(input as any))), - - groupBy: procedure - .input($Schema.UserInputSchema.groupBy) - .query(({ ctx, input }) => checkRead(db(ctx).user.groupBy(input as any))), - - updateMany: procedure - .input($Schema.UserInputSchema.updateMany) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.updateMany(input as any))), - - update: procedure - .input($Schema.UserInputSchema.update) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.update(input as any))), - - upsert: procedure - .input($Schema.UserInputSchema.upsert) - .mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.upsert(input as any))), - - count: procedure - .input($Schema.UserInputSchema.count) - .query(({ ctx, input }) => checkRead(db(ctx).user.count(input as any))), - }); + + aggregate: procedure.input($Schema.UserInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), + + create: procedure.input($Schema.UserInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), + + deleteMany: procedure.input($Schema.UserInputSchema.deleteMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.deleteMany(input as any))), + + delete: procedure.input($Schema.UserInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.delete(input as any))), + + findFirst: procedure.input($Schema.UserInputSchema.findFirst).query(({ ctx, input }) => checkRead(db(ctx).user.findFirst(input as any))), + + findFirstOrThrow: procedure.input($Schema.UserInputSchema.findFirst).query(({ ctx, input }) => checkRead(db(ctx).user.findFirstOrThrow(input as any))), + + findMany: procedure.input($Schema.UserInputSchema.findMany).query(({ ctx, input }) => checkRead(db(ctx).user.findMany(input as any))), + + findUnique: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUnique(input as any))), + + findUniqueOrThrow: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUniqueOrThrow(input as any))), + + groupBy: procedure.input($Schema.UserInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).user.groupBy(input as any))), + + updateMany: procedure.input($Schema.UserInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.updateMany(input as any))), + + update: procedure.input($Schema.UserInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.update(input as any))), + + upsert: procedure.input($Schema.UserInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.upsert(input as any))), + + count: procedure.input($Schema.UserInputSchema.count).query(({ ctx, input }) => checkRead(db(ctx).user.count(input as any))), + + } + ); } export interface ClientType { aggregate: { + useQuery: >( input: Prisma.Subset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.GetUserAggregateType, + TRPCClientErrorLike + >; + }; createMany: { useMutation: ( @@ -116,134 +85,147 @@ export interface ClientType( - opts?: UseTRPCMutationOptions< - Prisma.UserCreateArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.UserGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserCreateArgs, + TRPCClientErrorLike, + Prisma.UserGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> + }; + }; deleteMany: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.UserDeleteManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.BatchPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, - ) => Promise; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserDeleteManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >,) => + Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise + }; + }; delete: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.UserDeleteArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.UserGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserDeleteArgs, + TRPCClientErrorLike, + Prisma.UserGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> + }; + }; findFirst: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.UserGetPayload, + TRPCClientErrorLike + >; + }; findFirstOrThrow: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.UserGetPayload, + TRPCClientErrorLike + >; + }; findMany: { + useQuery: >>( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions>, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions>, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions>, Error>, - ) => UseTRPCInfiniteQueryResult>, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions>, Error> + ) => UseTRPCInfiniteQueryResult< + Array>, + TRPCClientErrorLike + >; + }; findUnique: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.UserGetPayload, + TRPCClientErrorLike + >; + }; findUniqueOrThrow: { + useQuery: >( input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error>, - ) => UseTRPCQueryResult>; + opts?: UseTRPCQueryOptions, TData, Error> + ) => UseTRPCQueryResult< + TData, + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error>, - ) => UseTRPCInfiniteQueryResult, TRPCClientErrorLike>; + opts?: UseTRPCInfiniteQueryOptions, Error> + ) => UseTRPCInfiniteQueryResult< + Prisma.UserGetPayload, + TRPCClientErrorLike + >; + }; groupBy: { - useQuery: < - T extends Prisma.UserGroupByArgs, + + useQuery: >, Prisma.Extends<'take', Prisma.Keys> >, OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, + ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } + : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, @@ -251,62 +233,62 @@ export interface ClientType, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [Error, 'Field ', P, ` in "having" needs to be provided in "by"`]; - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields], - TData = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, - >( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCQueryOptions< - string, - T, - {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + , TData = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors>( + input: Prisma.SubsetIntersection & InputErrors, + opts?: UseTRPCQueryOptions : InputErrors, TData, Error> + ) => UseTRPCQueryResult< TData, - Error - >, - ) => UseTRPCQueryResult>; - useInfiniteQuery: < - T extends Prisma.UserGroupByArgs, + TRPCClientErrorLike + >; + useInfiniteQuery: >, Prisma.Extends<'take', Prisma.Keys> >, OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, + ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } + : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, @@ -314,165 +296,130 @@ export interface ClientType, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [Error, 'Field ', P, ` in "having" needs to be provided in "by"`]; - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`; - }[OrderFields], + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] >( input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions< - string, - T, - {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, - Error - >, + opts?: UseTRPCInfiniteQueryOptions : InputErrors, Error> ) => UseTRPCInfiniteQueryResult< {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, TRPCClientErrorLike >; + }; updateMany: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.UserUpdateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.BatchPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, - ) => Promise; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserUpdateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >,) => + Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise + }; + }; update: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.UserUpdateArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.UserGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserUpdateArgs, + TRPCClientErrorLike, + Prisma.UserGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> + }; + }; upsert: { - useMutation: ( - opts?: UseTRPCMutationOptions< - Prisma.UserUpsertArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >, - ) => Omit< - UseTRPCMutationResult< - Prisma.UserGetPayload, - TRPCClientErrorLike, - Prisma.SelectSubset, - Context - >, - 'mutateAsync' - > & { - mutateAsync: ( - variables: T, - opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>, - ) => Promise>; - }; + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserUpsertArgs, + TRPCClientErrorLike, + Prisma.UserGetPayload, + Context + >,) => + Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> + }; + }; count: { - useQuery: < - T extends Prisma.UserCountArgs, - TData = 'select' extends keyof T - ? T['select'] extends true + + useQuery: + : number>( + input: Prisma.Subset, + opts?: UseTRPCQueryOptions - : number, - >( - input: Prisma.Subset, - opts?: UseTRPCQueryOptions< - string, - T, - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, + : number, TData, Error> + ) => UseTRPCQueryResult< TData, - Error - >, - ) => UseTRPCQueryResult>; + TRPCClientErrorLike + >; useInfiniteQuery: ( input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions< - string, - T, - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, - Error - >, + opts?: UseTRPCInfiniteQueryOptions + : number, Error> ) => UseTRPCInfiniteQueryResult< 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number, TRPCClientErrorLike >; + }; } diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts index bcb767b6f..f474aa5b5 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts @@ -1,25 +1,17 @@ /* eslint-disable */ -import { - unsetMarker, - type AnyRouter, - type AnyRootConfig, - type CreateRouterInner, - type Procedure, - type ProcedureBuilder, - type ProcedureParams, - type ProcedureRouterRecord, - type ProcedureType, -} from '@trpc/server'; -import { type PrismaClient } from '@prisma/client'; -import createUserRouter from './User.router'; -import createPostRouter from './Post.router'; -import { ClientType as UserClientType } from './User.router'; -import { ClientType as PostClientType } from './Post.router'; +import { unsetMarker, type AnyRouter, type AnyRootConfig, type CreateRouterInner, type Procedure, type ProcedureBuilder, type ProcedureParams, type ProcedureRouterRecord, type ProcedureType } from "@trpc/server"; +import { type PrismaClient } from "@prisma/client"; +import createUserRouter from "./User.router"; +import createPostRouter from "./Post.router"; +import { ClientType as UserClientType } from "./User.router"; +import { ClientType as PostClientType } from "./Post.router"; export type BaseConfig = AnyRootConfig; -export type RouterFactory = ( - procedures: ProcRouterRecord, +export type RouterFactory = < + ProcRouterRecord extends ProcedureRouterRecord +>( + procedures: ProcRouterRecord ) => CreateRouterInner; export type UnsetMarker = typeof unsetMarker; @@ -39,7 +31,8 @@ export function createRouter(router: RouterFactory { diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index 4c79c740a..fa9b21277 100644 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ b/packages/plugins/trpc/tests/trpc.test.ts @@ -3,6 +3,7 @@ import { loadSchema, normalizePath } from '@zenstackhq/testtools'; import fs from 'fs'; import path from 'path'; +import tmp from 'tmp'; describe('tRPC Plugin Tests', () => { let origDir: string; @@ -56,7 +57,7 @@ model Foo { { provider: 'postgresql', pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -98,7 +99,7 @@ model Foo { `, { pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -128,7 +129,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -153,7 +154,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -183,7 +184,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -230,7 +231,7 @@ model Post { { pushDb: false, extraDependencies: [ - `${path.join(__dirname, '../dist')}`, + path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server', '@trpc/react-query', @@ -254,7 +255,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server', '@trpc/next'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server', '@trpc/next'], compile: true, fullZod: true, } @@ -284,7 +285,7 @@ model post_item { `, { pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -331,7 +332,7 @@ model Foo { { addPrelude: false, pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, } ); @@ -402,7 +403,7 @@ model Foo { { addPrelude: false, pushDb: false, - extraDependencies: [`${path.join(__dirname, '../dist')}`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, } ); @@ -419,4 +420,59 @@ model Foo { fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/FooInput.schema.js')) ).toBeTruthy(); }); + + it('clear output', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.mkdirSync(path.join(projectDir, 'trpc'), { recursive: true }); + fs.writeFileSync(path.join(projectDir, 'trpc', 'test.txt'), 'hello'); + + await loadSchema( + ` + plugin trpc { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/trpc' + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + password String @omit + } + `, + { + pushDb: false, + projectDir, + extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`], + } + ); + + expect(fs.existsSync(path.join(projectDir, 'trpc', 'test.txt'))).toBeFalsy(); + }); + + it('existing output as file', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.writeFileSync(path.join(projectDir, 'trpc'), 'hello'); + + await expect( + loadSchema( + ` + plugin trpc { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/trpc' + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String + password String @omit + } + `, + { pushDb: false, projectDir, extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`] } + ) + ).rejects.toThrow('already exists and is not a directory'); + }); }); diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/runtime/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 893e1e5e0..e1b436993 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.12.4", + "version": "2.0.0", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", @@ -18,21 +18,31 @@ "types": "index.d.ts", "exports": { ".": { + "types": "./index.d.ts", "default": "./index.js" }, - "./package.json": { - "default": "./package.json" + "./edge": { + "types": "./edge.d.ts", + "default": "./edge.js" + }, + "./enhancements": { + "types": "./enhancements/index.d.ts", + "default": "./enhancements/index.js" }, "./zod": { + "types": "./zod/index.d.ts", "default": "./zod/index.js" }, "./zod/input": { + "types": "./zod/input.d.ts", "default": "./zod/input.js" }, "./zod/models": { + "types": "./zod/models.d.ts", "default": "./zod/models.js" }, "./zod/objects": { + "types": "./zod/objects.d.ts", "default": "./zod/objects.js" }, "./browser": { @@ -46,6 +56,16 @@ "import": "./cross/index.mjs", "require": "./cross/index.js", "default": "./cross/index.js" + }, + "./model-meta": { + "types": "./model-meta.d.ts", + "default": "./model-meta.js" + }, + "./models": { + "types": "./models.d.ts" + }, + "./package.json": { + "default": "./package.json" } }, "publishConfig": { @@ -53,15 +73,15 @@ "linkDirectory": true }, "dependencies": { - "@types/bcryptjs": "^2.4.2", "bcryptjs": "^2.4.3", "buffer": "^6.0.3", "change-case": "^4.1.2", - "colors": "1.4.0", "decimal.js": "^10.4.2", "deepcopy": "^2.1.0", + "deepmerge": "^4.3.1", "lower-case-first": "^2.0.2", "pluralize": "^8.0.0", + "safe-json-stringify": "^1.2.0", "semver": "^7.5.2", "superjson": "^1.11.0", "tiny-invariant": "^1.3.1", @@ -71,6 +91,9 @@ "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, + "peerDependencies": { + "@prisma/client": "5.0.0 - 5.12.x" + }, "author": { "name": "ZenStack Team" }, @@ -79,6 +102,7 @@ "devDependencies": { "@types/bcryptjs": "^2.4.2", "@types/pluralize": "^0.0.29", + "@types/safe-json-stringify": "^1.1.5", "@types/semver": "^7.3.13", "@types/uuid": "^8.3.4" } diff --git a/packages/runtime/res/enhance.d.ts b/packages/runtime/res/enhance.d.ts new file mode 100644 index 000000000..4ae717bc4 --- /dev/null +++ b/packages/runtime/res/enhance.d.ts @@ -0,0 +1 @@ +export { enhance } from '.zenstack/enhance'; diff --git a/packages/runtime/res/enhance.js b/packages/runtime/res/enhance.js new file mode 100644 index 000000000..aa19af865 --- /dev/null +++ b/packages/runtime/res/enhance.js @@ -0,0 +1,10 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); + +try { + exports.enhance = require('.zenstack/enhance').enhance; +} catch { + exports.enhance = function () { + throw new Error('Generated "enhance" function not found. Please run `zenstack generate` first.'); + }; +} diff --git a/packages/runtime/res/model-meta.d.ts b/packages/runtime/res/model-meta.d.ts new file mode 100644 index 000000000..faac80c52 --- /dev/null +++ b/packages/runtime/res/model-meta.d.ts @@ -0,0 +1 @@ +export * from '.zenstack/model-meta'; diff --git a/packages/runtime/res/model-meta.js b/packages/runtime/res/model-meta.js new file mode 100644 index 000000000..d4af2b522 --- /dev/null +++ b/packages/runtime/res/model-meta.js @@ -0,0 +1,10 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); + +try { + exports.default = require('.zenstack/model-meta').default; +} catch { + exports.default = function () { + throw new Error('Generated model meta not found. Please run `zenstack generate` first.'); + }; +} diff --git a/packages/runtime/res/models.d.ts b/packages/runtime/res/models.d.ts new file mode 100644 index 000000000..a6f0d404b --- /dev/null +++ b/packages/runtime/res/models.d.ts @@ -0,0 +1 @@ +export type * from '.zenstack/models'; diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts index 36143621f..a85392887 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -61,7 +61,7 @@ export const PRISMA_PROXY_ENHANCER = '$__zenstack_enhancer'; /** * Minimum Prisma version supported */ -export const PRISMA_MINIMUM_VERSION = '4.8.0'; +export const PRISMA_MINIMUM_VERSION = '5.0.0'; /** * Selector function name for fetching pre-update entity values. @@ -97,3 +97,8 @@ export const FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX = 'updateFieldGuardOverrid * Flag that indicates if the model has field-level access control */ export const HAS_FIELD_LEVEL_POLICY_FLAG = 'hasFieldLevelPolicy'; + +/** + * Prefix for auxiliary relation field generated for delegated models + */ +export const DELEGATE_AUX_RELATION_PREFIX = 'delegate_aux'; diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts index a38f7986d..a90b20685 100644 --- a/packages/runtime/src/cross/model-meta.ts +++ b/packages/runtime/src/cross/model-meta.ts @@ -4,10 +4,22 @@ import { lowerCaseFirst } from 'lower-case-first'; * Runtime information of a data model or field attribute */ export type RuntimeAttribute = { + /** + * Attribute name + */ name: string; + + /** + * Attribute arguments + */ args: Array<{ name?: string; value: unknown }>; }; +/** + * Function for computing default value for a field + */ +export type FieldDefaultValueProvider = (userContext: unknown) => unknown; + /** * Runtime information of a data model field */ @@ -63,10 +75,27 @@ export type FieldInfo = { isForeignKey?: boolean; /** - * Mapping from foreign key field names to relation field names + * If the field is a foreign key field, the field name of the corresponding relation field. + * Only available on foreign key fields. + */ + relationField?: string; + + /** + * Mapping from foreign key field names to relation field names. + * Only available on relation fields. */ foreignKeyMapping?: Record; + /** + * Model from which the field is inherited + */ + inheritedFrom?: string; + + /** + * A function that provides a default value for the field + */ + defaultValueProvider?: FieldDefaultValueProvider; + /** * If the field is an auto-increment field */ @@ -80,23 +109,53 @@ export type FieldInfo = { export type UniqueConstraint = { name: string; fields: string[] }; /** - * ZModel data model metadata + * Metadata for a data model */ -export type ModelMeta = { +export type ModelInfo = { + /** + * Model name + */ + name: string; + /** - * Model fields + * Base types (not including abstract base models). */ - fields: Record>; + baseTypes?: string[]; /** - * Model unique constraints + * Fields */ - uniqueConstraints: Record>; + fields: Record; /** - * Information for cascading delete + * Unique constraints */ - deleteCascade: Record; + uniqueConstraints?: Record; + + /** + * Attributes on the model + */ + attributes?: RuntimeAttribute[]; + + /** + * Discriminator field name + */ + discriminator?: string; +}; + +/** + * ZModel data model metadata + */ +export type ModelMeta = { + /** + * Data models + */ + models: Record; + + /** + * Mapping from model name to models that will be deleted because of it due to cascade delete + */ + deleteCascade?: Record; /** * Name of model that backs the `auth()` function @@ -107,8 +166,8 @@ export type ModelMeta = { /** * Resolves a model field to its metadata. Returns undefined if not found. */ -export function resolveField(modelMeta: ModelMeta, model: string, field: string) { - return modelMeta.fields[lowerCaseFirst(model)]?.[field]; +export function resolveField(modelMeta: ModelMeta, model: string, field: string): FieldInfo | undefined { + return modelMeta.models[lowerCaseFirst(model)]?.fields?.[field]; } /** @@ -126,5 +185,12 @@ export function requireField(modelMeta: ModelMeta, model: string, field: string) * Gets all fields of a model. */ export function getFields(modelMeta: ModelMeta, model: string) { - return modelMeta.fields[lowerCaseFirst(model)]; + return modelMeta.models[lowerCaseFirst(model)]?.fields; +} + +/** + * Gets unique constraints of a model. + */ +export function getUniqueConstraints(modelMeta: ModelMeta, model: string) { + return modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints; } diff --git a/packages/runtime/src/cross/nested-write-visitor.ts b/packages/runtime/src/cross/nested-write-visitor.ts index 7c4e8e5e5..4ce4e0ae7 100644 --- a/packages/runtime/src/cross/nested-write-visitor.ts +++ b/packages/runtime/src/cross/nested-write-visitor.ts @@ -34,7 +34,7 @@ export type NestedWriteVisitorContext = { * to let the visitor traverse it instead of its original children. */ export type NestedWriterVisitorCallback = { - create?: (model: string, args: any[], context: NestedWriteVisitorContext) => MaybePromise; + create?: (model: string, data: any, context: NestedWriteVisitorContext) => MaybePromise; createMany?: ( model: string, @@ -219,8 +219,10 @@ export class NestedWriteVisitor { case 'set': if (this.callback.set) { - const newContext = pushNewContext(field, model, {}); - await this.callback.set(model, data, newContext); + for (const item of this.enumerateReverse(data)) { + const newContext = pushNewContext(field, model, item, true); + await this.callback.set(model, item, newContext); + } } break; diff --git a/packages/runtime/src/cross/query-analyzer.ts b/packages/runtime/src/cross/query-analyzer.ts index 5af410e82..9277688d5 100644 --- a/packages/runtime/src/cross/query-analyzer.ts +++ b/packages/runtime/src/cross/query-analyzer.ts @@ -4,6 +4,7 @@ import type { ModelMeta } from './model-meta'; import { NestedReadVisitor } from './nested-read-visitor'; import { NestedWriteVisitor } from './nested-write-visitor'; import type { PrismaWriteActionType } from './types'; +import { getModelInfo } from './utils'; /** * Gets models read (including nested ones) given a query args. @@ -71,6 +72,11 @@ export async function getMutatedModels( await visitor.visit(model, operation, mutationArgs); } + // include delegate base models recursively + result.forEach((m) => { + getBaseRecursively(m, modelMeta, result); + }); + return [...result]; } @@ -81,7 +87,7 @@ function collectDeleteCascades(model: string, modelMeta: ModelMeta, result: Set< } visited.add(model); - const cascades = modelMeta.deleteCascade[lowerCaseFirst(model)]; + const cascades = modelMeta.deleteCascade?.[lowerCaseFirst(model)]; if (!cascades) { return; @@ -92,3 +98,13 @@ function collectDeleteCascades(model: string, modelMeta: ModelMeta, result: Set< collectDeleteCascades(m, modelMeta, result, visited); }); } + +function getBaseRecursively(model: string, modelMeta: ModelMeta, result: Set) { + const bases = getModelInfo(modelMeta, model)?.baseTypes; + if (bases) { + bases.forEach((base) => { + result.add(base); + getBaseRecursively(base, modelMeta, result); + }); + } +} diff --git a/packages/runtime/src/cross/utils.ts b/packages/runtime/src/cross/utils.ts index 08cb29a7e..304b9b618 100644 --- a/packages/runtime/src/cross/utils.ts +++ b/packages/runtime/src/cross/utils.ts @@ -1,5 +1,5 @@ import { lowerCaseFirst } from 'lower-case-first'; -import { ModelMeta, requireField } from '.'; +import { requireField, type ModelInfo, type ModelMeta } from '.'; /** * Gets field names in a data model entity, filtering out internal fields. @@ -47,7 +47,7 @@ export function zip(x: Enumerable, y: Enumerable): Array<[T1, T2 } export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) { - const uniqueConstraints = modelMeta.uniqueConstraints[lowerCaseFirst(model)] ?? {}; + const uniqueConstraints = modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {}; const entries = Object.values(uniqueConstraints); if (entries.length === 0) { @@ -59,3 +59,19 @@ export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound return entries[0].fields.map((f) => requireField(modelMeta, model, f)); } + +export function getModelInfo( + modelMeta: ModelMeta, + model: string, + throwIfNotFound: Throw = false as Throw +): Throw extends true ? ModelInfo : ModelInfo | undefined { + const info = modelMeta.models[lowerCaseFirst(model)]; + if (!info && throwIfNotFound) { + throw new Error(`Unable to load info for ${model}`); + } + return info; +} + +export function isDelegateModel(modelMeta: ModelMeta, model: string) { + return !!getModelInfo(modelMeta, model)?.attributes?.some((attr) => attr.name === '@@delegate'); +} diff --git a/packages/runtime/src/edge.ts b/packages/runtime/src/edge.ts new file mode 120000 index 000000000..a2e78d748 --- /dev/null +++ b/packages/runtime/src/edge.ts @@ -0,0 +1 @@ +index.ts \ No newline at end of file diff --git a/packages/runtime/src/enhance.d.ts b/packages/runtime/src/enhance.d.ts new file mode 100644 index 000000000..48e877878 --- /dev/null +++ b/packages/runtime/src/enhance.d.ts @@ -0,0 +1,2 @@ +// @ts-expect-error stub for re-exporting generated code +export { enhance } from '.zenstack/enhance'; diff --git a/packages/runtime/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts new file mode 100644 index 000000000..596c3e763 --- /dev/null +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -0,0 +1,171 @@ +import semver from 'semver'; +import { PRISMA_MINIMUM_VERSION } from '../constants'; +import { isDelegateModel, type ModelMeta } from '../cross'; +import type { AuthUser } from '../types'; +import { withDefaultAuth } from './default-auth'; +import { withDelegate } from './delegate'; +import { Logger } from './logger'; +import { withOmit } from './omit'; +import { withPassword } from './password'; +import { withPolicy } from './policy'; +import type { ErrorTransformer } from './proxy'; +import type { PolicyDef, ZodSchemas } from './types'; + +/** + * Kinds of enhancements to `PrismaClient` + */ +export type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate'; + +/** + * All enhancement kinds + */ +const ALL_ENHANCEMENTS: EnhancementKind[] = ['password', 'omit', 'policy', 'validation', 'delegate']; + +/** + * Transaction isolation levels: https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-isolation-level + */ +export type TransactionIsolationLevel = + | 'ReadUncommitted' + | 'ReadCommitted' + | 'RepeatableRead' + | 'Snapshot' + | 'Serializable'; + +export type EnhancementOptions = { + /** + * The kinds of enhancements to apply. By default all enhancements are applied. + */ + kinds?: EnhancementKind[]; + + /** + * Whether to log Prisma query + */ + logPrismaQuery?: boolean; + + /** + * Hook for transforming errors before they are thrown to the caller. + */ + errorTransformer?: ErrorTransformer; + + /** + * The `maxWait` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. + */ + transactionMaxWait?: number; + + /** + * The `timeout` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. + */ + transactionTimeout?: number; + + /** + * The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. + */ + transactionIsolationLevel?: TransactionIsolationLevel; +}; + +/** + * Options for {@link createEnhancement} + * + * @private + */ +export type InternalEnhancementOptions = EnhancementOptions & { + /** + * Policy definition + */ + policy: PolicyDef; + + /** + * Model metadata + */ + modelMeta: ModelMeta; + + /** + * Zod schemas for validation + */ + zodSchemas?: ZodSchemas; + + /** + * The Node module that contains PrismaClient + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prismaModule: any; +}; + +/** + * Context for creating enhanced `PrismaClient` + */ +export type EnhancementContext = { + user?: User; +}; + +/** + * Gets a Prisma client enhanced with all enhancement behaviors, including access + * policy, field validation, field omission and password hashing. + * + * @private + * + * @param prisma The Prisma client to enhance. + * @param context Context. + * @param options Options. + */ +export function createEnhancement( + prisma: DbClient, + options: InternalEnhancementOptions, + context?: EnhancementContext +) { + if (!prisma) { + throw new Error('Invalid prisma instance'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const prismaVer = (prisma as any)._clientVersion; + if (prismaVer && semver.lt(prismaVer, PRISMA_MINIMUM_VERSION)) { + console.warn( + `ZenStack requires Prisma version "${PRISMA_MINIMUM_VERSION}" or higher. Detected version is "${prismaVer}".` + ); + } + + // TODO: move the detection logic into each enhancement + // TODO: how to properly cache the detection result? + const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => Object.values(modelInfo.fields)); + const hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password')); + const hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit')); + const hasDefaultAuth = allFields.some((field) => field.defaultValueProvider); + + const kinds = options.kinds ?? ALL_ENHANCEMENTS; + let result = prisma; + + // delegate proxy needs to be wrapped inside policy proxy, since it may translate `deleteMany` + // and `updateMany` to plain `delete` and `update` + if (Object.values(options.modelMeta.models).some((model) => isDelegateModel(options.modelMeta, model.name))) { + if (!kinds.includes('delegate')) { + const logger = new Logger(prisma); + logger.warn( + 'Your ZModel contains delegate models but "delegate" enhancement kind is not enabled. This may result in unexpected behavior.' + ); + } else { + result = withDelegate(result, options); + } + } + + // 'policy' and 'validation' enhancements are both enabled by `withPolicy` + if (kinds.includes('policy') || kinds.includes('validation')) { + result = withPolicy(result, options, context); + if (kinds.includes('policy') && hasDefaultAuth) { + // @default(auth()) proxy + result = withDefaultAuth(result, options, context); + } + } + + if (hasPassword && kinds.includes('password')) { + // @password proxy + result = withPassword(result, options); + } + + if (hasOmit && kinds.includes('omit')) { + // @omit proxy + result = withOmit(result, options); + } + + return result; +} diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts new file mode 100644 index 000000000..56e43ab29 --- /dev/null +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -0,0 +1,146 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import deepcopy from 'deepcopy'; +import { FieldInfo, NestedWriteVisitor, PrismaWriteActionType, enumerate, getFields, requireField } from '../cross'; +import { DbClientContract } from '../types'; +import { EnhancementContext, InternalEnhancementOptions } from './create-enhancement'; +import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; +import { isUnsafeMutate } from './utils'; + +/** + * Gets an enhanced Prisma client that supports `@default(auth())` attribute. + * + * @private + */ +export function withDefaultAuth( + prisma: DbClient, + options: InternalEnhancementOptions, + context: EnhancementContext = {} +): DbClient { + return makeProxy( + prisma, + options.modelMeta, + (_prisma, model) => new DefaultAuthHandler(_prisma as DbClientContract, model, options, context), + 'defaultAuth' + ); +} + +class DefaultAuthHandler extends DefaultPrismaProxyHandler { + private readonly userContext: any; + + constructor( + prisma: DbClientContract, + model: string, + options: InternalEnhancementOptions, + private readonly context: EnhancementContext + ) { + super(prisma, model, options); + + this.userContext = this.context.user; + } + + // base override + protected async preprocessArgs(action: PrismaProxyActions, args: any) { + const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert']; + if (actionsOfInterest.includes(action)) { + const newArgs = await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args); + return newArgs; + } + return args; + } + + private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) { + const newArgs = deepcopy(args); + + const processCreatePayload = (model: string, data: any) => { + const fields = getFields(this.options.modelMeta, model); + for (const fieldInfo of Object.values(fields)) { + if (fieldInfo.name in data) { + // create payload already sets field value + continue; + } + + if (!fieldInfo.defaultValueProvider) { + // field doesn't have a runtime default value provider + continue; + } + + const authDefaultValue = this.getDefaultValueFromAuth(fieldInfo); + if (authDefaultValue !== undefined) { + // set field value extracted from `auth()` + this.setAuthDefaultValue(fieldInfo, model, data, authDefaultValue); + } + } + }; + + // visit create payload and set default value to fields using `auth()` in `@default()` + const visitor = new NestedWriteVisitor(this.options.modelMeta, { + create: (model, data) => { + processCreatePayload(model, data); + }, + + createMany: (model, args) => { + for (const item of enumerate(args.data)) { + processCreatePayload(model, item); + } + }, + }); + + await visitor.visit(model, action, newArgs); + return newArgs; + } + + private setAuthDefaultValue(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) { + if (fieldInfo.isForeignKey && fieldInfo.relationField && fieldInfo.relationField in data) { + // if the field is a fk, and the relation field is already set, we should not override it + return; + } + + if (fieldInfo.isForeignKey && !isUnsafeMutate(model, data, this.options.modelMeta)) { + // if the field is a fk, and the create payload is not unsafe, we need to translate + // the fk field setting to a `connect` of the corresponding relation field + const relFieldName = fieldInfo.relationField; + if (!relFieldName) { + throw new Error( + `Field \`${fieldInfo.name}\` is a foreign key field but no corresponding relation field is found` + ); + } + const relationField = requireField(this.options.modelMeta, model, relFieldName); + + // construct a `{ connect: { ... } }` payload + let connect = data[relationField.name]?.connect; + if (!connect) { + connect = {}; + data[relationField.name] = { connect }; + } + + // sets the opposite fk field to value `authDefaultValue` + const oppositeFkFieldName = this.getOppositeFkFieldName(relationField, fieldInfo); + if (!oppositeFkFieldName) { + throw new Error( + `Cannot find opposite foreign key field for \`${fieldInfo.name}\` in relation field \`${relFieldName}\`` + ); + } + connect[oppositeFkFieldName] = authDefaultValue; + } else { + // set default value directly + data[fieldInfo.name] = authDefaultValue; + } + } + + private getOppositeFkFieldName(relationField: FieldInfo, fieldInfo: FieldInfo) { + if (!relationField.foreignKeyMapping) { + return undefined; + } + const entry = Object.entries(relationField.foreignKeyMapping).find(([, v]) => v === fieldInfo.name); + return entry?.[0]; + } + + private getDefaultValueFromAuth(fieldInfo: FieldInfo) { + if (!this.userContext) { + throw new Error(`Evaluating default value of field \`${fieldInfo.name}\` requires a user context`); + } + return fieldInfo.defaultValueProvider?.(this.userContext); + } +} diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts new file mode 100644 index 000000000..e2fdc65f2 --- /dev/null +++ b/packages/runtime/src/enhancements/delegate.ts @@ -0,0 +1,1187 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import deepcopy from 'deepcopy'; +import deepmerge, { type ArrayMergeOptions } from 'deepmerge'; +import { lowerCaseFirst } from 'lower-case-first'; +import { DELEGATE_AUX_RELATION_PREFIX } from '../constants'; +import { + FieldInfo, + ModelInfo, + NestedWriteVisitor, + enumerate, + getIdFields, + getModelInfo, + isDelegateModel, + resolveField, +} from '../cross'; +import type { CrudContract, DbClientContract } from '../types'; +import type { InternalEnhancementOptions } from './create-enhancement'; +import { Logger } from './logger'; +import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; +import { QueryUtils } from './query-utils'; +import { formatObject, prismaClientValidationError } from './utils'; + +export function withDelegate(prisma: DbClient, options: InternalEnhancementOptions): DbClient { + return makeProxy( + prisma, + options.modelMeta, + (_prisma, model) => new DelegateProxyHandler(_prisma as DbClientContract, model, options), + 'delegate' + ); +} + +export class DelegateProxyHandler extends DefaultPrismaProxyHandler { + private readonly logger: Logger; + private readonly queryUtils: QueryUtils; + + constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { + super(prisma, model, options); + this.logger = new Logger(prisma); + this.queryUtils = new QueryUtils(prisma, this.options); + } + + // #region find + + override findFirst(args: any): Promise { + return this.doFind(this.prisma, this.model, 'findFirst', args); + } + + override findFirstOrThrow(args: any): Promise { + return this.doFind(this.prisma, this.model, 'findFirstOrThrow', args); + } + + override findUnique(args: any): Promise { + return this.doFind(this.prisma, this.model, 'findUnique', args); + } + + override findUniqueOrThrow(args: any): Promise { + return this.doFind(this.prisma, this.model, 'findUniqueOrThrow', args); + } + + override async findMany(args: any): Promise { + return this.doFind(this.prisma, this.model, 'findMany', args); + } + + private async doFind( + db: CrudContract, + model: string, + method: 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany', + args: any + ) { + if (!this.involvesDelegateModel(model)) { + return super[method](args); + } + + args = args ? deepcopy(args) : {}; + + this.injectWhereHierarchy(model, args?.where); + this.injectSelectIncludeHierarchy(model, args); + + if (args.orderBy) { + // `orderBy` may contain fields from base types + args.orderBy = this.buildWhereHierarchy(this.model, args.orderBy); + } + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`${method}\` ${this.getModelName(model)}: ${formatObject(args)}`); + } + const entity = await db[model][method](args); + + if (Array.isArray(entity)) { + return entity.map((item) => this.assembleHierarchy(model, item)); + } else { + return this.assembleHierarchy(model, entity); + } + } + + private injectWhereHierarchy(model: string, where: any) { + if (!where || typeof where !== 'object') { + return; + } + + Object.entries(where).forEach(([field, value]) => { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + if (!fieldInfo?.inheritedFrom) { + return; + } + + let base = this.getBaseModel(model); + let target = where; + + while (base) { + const baseRelationName = this.makeAuxRelationName(base); + + // prepare base layer where + let thisLayer: any; + if (target[baseRelationName]) { + thisLayer = target[baseRelationName]; + } else { + thisLayer = target[baseRelationName] = {}; + } + + if (base.name === fieldInfo.inheritedFrom) { + thisLayer[field] = value; + delete where[field]; + break; + } else { + target = thisLayer; + base = this.getBaseModel(base.name); + } + } + }); + } + + private buildWhereHierarchy(model: string, where: any) { + if (!where) { + return undefined; + } + + where = deepcopy(where); + Object.entries(where).forEach(([field, value]) => { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + if (!fieldInfo?.inheritedFrom) { + return; + } + + let base = this.getBaseModel(model); + let target = where; + + while (base) { + const baseRelationName = this.makeAuxRelationName(base); + + // prepare base layer where + let thisLayer: any; + if (target[baseRelationName]) { + thisLayer = target[baseRelationName]; + } else { + thisLayer = target[baseRelationName] = {}; + } + + if (base.name === fieldInfo.inheritedFrom) { + thisLayer[field] = value; + delete where[field]; + break; + } else { + target = thisLayer; + base = this.getBaseModel(base.name); + } + } + }); + + return where; + } + + private injectSelectIncludeHierarchy(model: string, args: any) { + if (!args || typeof args !== 'object') { + return; + } + + for (const kind of ['select', 'include'] as const) { + if (args[kind] && typeof args[kind] === 'object') { + for (const [field, value] of Object.entries(args[kind])) { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + if (fieldInfo && value !== undefined) { + if (value?.orderBy) { + // `orderBy` may contain fields from base types + value.orderBy = this.buildWhereHierarchy(fieldInfo.type, value.orderBy); + } + + if (this.injectBaseFieldSelect(model, field, value, args, kind)) { + delete args[kind][field]; + } else { + if (fieldInfo && this.isDelegateOrDescendantOfDelegate(fieldInfo.type)) { + let nextValue = value; + if (nextValue === true) { + // make sure the payload is an object + args[kind][field] = nextValue = {}; + } + this.injectSelectIncludeHierarchy(fieldInfo.type, nextValue); + } + } + } + } + } + } + + if (!args.select) { + // include base models upwards + this.injectBaseIncludeRecursively(model, args); + + // include sub models downwards + this.injectConcreteIncludeRecursively(model, args); + } + } + + private buildSelectIncludeHierarchy(model: string, args: any) { + args = deepcopy(args); + const selectInclude: any = this.extractSelectInclude(args) || {}; + + if (selectInclude.select && typeof selectInclude.select === 'object') { + Object.entries(selectInclude.select).forEach(([field, value]) => { + if (value) { + if (this.injectBaseFieldSelect(model, field, value, selectInclude, 'select')) { + delete selectInclude.select[field]; + } + } + }); + } else if (selectInclude.include && typeof selectInclude.include === 'object') { + Object.entries(selectInclude.include).forEach(([field, value]) => { + if (value) { + if (this.injectBaseFieldSelect(model, field, value, selectInclude, 'include')) { + delete selectInclude.include[field]; + } + } + }); + } + + if (!selectInclude.select) { + this.injectBaseIncludeRecursively(model, selectInclude); + this.injectConcreteIncludeRecursively(model, selectInclude); + } + return selectInclude; + } + + private injectBaseFieldSelect( + model: string, + field: string, + value: any, + selectInclude: any, + context: 'select' | 'include' + ) { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + if (!fieldInfo?.inheritedFrom) { + return false; + } + + let base = this.getBaseModel(model); + let target = selectInclude; + + while (base) { + const baseRelationName = this.makeAuxRelationName(base); + + // prepare base layer select/include + // let selectOrInclude = 'select'; + let thisLayer: any; + if (target.include) { + // selectOrInclude = 'include'; + thisLayer = target.include; + } else if (target.select) { + // selectOrInclude = 'select'; + thisLayer = target.select; + } else { + // selectInclude = 'include'; + thisLayer = target.select = {}; + } + + if (base.name === fieldInfo.inheritedFrom) { + if (!thisLayer[baseRelationName]) { + thisLayer[baseRelationName] = { [context]: {} }; + } + thisLayer[baseRelationName][context][field] = value; + break; + } else { + if (!thisLayer[baseRelationName]) { + thisLayer[baseRelationName] = { select: {} }; + } + target = thisLayer[baseRelationName]; + base = this.getBaseModel(base.name); + } + } + + return true; + } + + private injectBaseIncludeRecursively(model: string, selectInclude: any) { + const base = this.getBaseModel(model); + if (!base) { + return; + } + const baseRelationName = this.makeAuxRelationName(base); + + if (selectInclude.select) { + selectInclude.include = { [baseRelationName]: {}, ...selectInclude.select }; + delete selectInclude.select; + } else { + selectInclude.include = { [baseRelationName]: {}, ...selectInclude.include }; + } + this.injectBaseIncludeRecursively(base.name, selectInclude.include[baseRelationName]); + } + + private injectConcreteIncludeRecursively(model: string, selectInclude: any) { + const modelInfo = getModelInfo(this.options.modelMeta, model); + if (!modelInfo) { + return; + } + + // get sub models of this model + const subModels = Object.values(this.options.modelMeta.models).filter((m) => + m.baseTypes?.includes(modelInfo.name) + ); + + for (const subModel of subModels) { + // include sub model relation field + const subRelationName = this.makeAuxRelationName(subModel); + if (selectInclude.select) { + selectInclude.include = { [subRelationName]: {}, ...selectInclude.select }; + delete selectInclude.select; + } else { + selectInclude.include = { [subRelationName]: {}, ...selectInclude.include }; + } + this.injectConcreteIncludeRecursively(subModel.name, selectInclude.include[subRelationName]); + } + } + + // #endregion + + // #region create + + override async create(args: any) { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!args.data) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + 'data field is required in query argument' + ); + } + + if (isDelegateModel(this.options.modelMeta, this.model)) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + `Model "${this.model}" is a delegate and cannot be created directly` + ); + } + + if (!this.involvesDelegateModel(this.model)) { + return super.create(args); + } + + return this.doCreate(this.prisma, this.model, args); + } + + override createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!args.data) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + 'data field is required in query argument' + ); + } + + if (!this.involvesDelegateModel(this.model)) { + return super.createMany(args); + } + + if (this.isDelegateOrDescendantOfDelegate(this.model) && args.skipDuplicates) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + '`createMany` with `skipDuplicates` set to true is not supported for delegated models' + ); + } + + // note that we can't call `createMany` directly because it doesn't support + // nested created, which is needed for creating base entities + return this.queryUtils.transaction(this.prisma, async (tx) => { + const r = await Promise.all( + enumerate(args.data).map(async (item) => { + return this.doCreate(tx, this.model, { data: item }); + }) + ); + + // filter out undefined value (due to skipping duplicates) + return { count: r.filter((item) => !!item).length }; + }); + } + + private async doCreate(db: CrudContract, model: string, args: any) { + args = deepcopy(args); + + await this.injectCreateHierarchy(model, args); + this.injectSelectIncludeHierarchy(model, args); + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`create\` ${this.getModelName(model)}: ${formatObject(args)}`); + } + const result = await db[model].create(args); + return this.assembleHierarchy(model, result); + } + + private async injectCreateHierarchy(model: string, args: any) { + const visitor = new NestedWriteVisitor(this.options.modelMeta, { + create: (model, args, _context) => { + this.doProcessCreatePayload(model, args); + }, + + createMany: (model, args, _context) => { + if (args.skipDuplicates) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + '`createMany` with `skipDuplicates` set to true is not supported for delegated models' + ); + } + + for (const item of enumerate(args?.data)) { + this.doProcessCreatePayload(model, item); + } + }, + }); + + await visitor.visit(model, 'create', args); + } + + private doProcessCreatePayload(model: string, args: any) { + if (!args) { + return; + } + + this.ensureBaseCreateHierarchy(model, args); + + for (const [field, value] of Object.entries(args)) { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + if (fieldInfo?.inheritedFrom) { + this.injectBaseFieldData(model, fieldInfo, value, args, 'create'); + delete args[field]; + } + } + } + + // ensure the full nested "create" structure is created for base types + private ensureBaseCreateHierarchy(model: string, result: any) { + let curr = result; + let base = this.getBaseModel(model); + let sub = this.getModelInfo(model); + + while (base) { + const baseRelationName = this.makeAuxRelationName(base); + + if (!curr[baseRelationName]) { + curr[baseRelationName] = {}; + } + if (!curr[baseRelationName].create) { + curr[baseRelationName].create = {}; + if (base.discriminator) { + // set discriminator field + curr[baseRelationName].create[base.discriminator] = sub.name; + } + } + curr = curr[baseRelationName].create; + sub = base; + base = this.getBaseModel(base.name); + } + } + + // inject field data that belongs to base type into proper nesting structure + private injectBaseFieldData( + model: string, + fieldInfo: FieldInfo, + value: unknown, + args: any, + mode: 'create' | 'update' + ) { + let base = this.getBaseModel(model); + let curr = args; + + while (base) { + if (base.discriminator === fieldInfo.name) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + `fields "${fieldInfo.name}" is a discriminator and cannot be set directly` + ); + } + + const baseRelationName = this.makeAuxRelationName(base); + + if (!curr[baseRelationName]) { + curr[baseRelationName] = {}; + } + if (!curr[baseRelationName][mode]) { + curr[baseRelationName][mode] = {}; + } + curr = curr[baseRelationName][mode]; + + if (fieldInfo.inheritedFrom === base.name) { + curr[fieldInfo.name] = value; + break; + } + + base = this.getBaseModel(base.name); + } + } + + // #endregion + + // #region update + + override update(args: any): Promise { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!args.data) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + 'data field is required in query argument' + ); + } + + if (!this.involvesDelegateModel(this.model)) { + return super.update(args); + } + + return this.queryUtils.transaction(this.prisma, (tx) => this.doUpdate(tx, this.model, args)); + } + + override async updateMany(args: any): Promise<{ count: number }> { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!args.data) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + 'data field is required in query argument' + ); + } + + if (!this.involvesDelegateModel(this.model)) { + return super.updateMany(args); + } + + const simpleUpdateMany = Object.keys(args.data).every((key) => { + // check if the `data` clause involves base fields + const fieldInfo = resolveField(this.options.modelMeta, this.model, key); + return !fieldInfo?.inheritedFrom; + }); + + return this.queryUtils.transaction(this.prisma, (tx) => + this.doUpdateMany(tx, this.model, args, simpleUpdateMany) + ); + } + + override async upsert(args: any): Promise { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!args.where) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + 'where field is required in query argument' + ); + } + + if (isDelegateModel(this.options.modelMeta, this.model)) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + `Model "${this.model}" is a delegate and doesn't support upsert` + ); + } + + if (!this.involvesDelegateModel(this.model)) { + return super.upsert(args); + } + + args = deepcopy(args); + this.injectWhereHierarchy(this.model, (args as any)?.where); + this.injectSelectIncludeHierarchy(this.model, args); + if (args.create) { + this.doProcessCreatePayload(this.model, args.create); + } + if (args.update) { + this.doProcessUpdatePayload(this.model, args.update); + } + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`upsert\` ${this.getModelName(this.model)}: ${formatObject(args)}`); + } + const result = await this.prisma[this.model].upsert(args); + return this.assembleHierarchy(this.model, result); + } + + private async doUpdate(db: CrudContract, model: string, args: any): Promise { + args = deepcopy(args); + + await this.injectUpdateHierarchy(db, model, args); + this.injectSelectIncludeHierarchy(model, args); + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`update\` ${this.getModelName(model)}: ${formatObject(args)}`); + } + const result = await db[model].update(args); + return this.assembleHierarchy(model, result); + } + + private async doUpdateMany( + db: CrudContract, + model: string, + args: any, + simpleUpdateMany: boolean + ): Promise<{ count: number }> { + if (simpleUpdateMany) { + // do a direct `updateMany` + args = deepcopy(args); + await this.injectUpdateHierarchy(db, model, args); + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`updateMany\` ${this.getModelName(model)}: ${formatObject(args)}`); + } + return db[model].updateMany(args); + } else { + // translate to plain `update` for nested write into base fields + const findArgs = { + where: deepcopy(args.where), + select: this.queryUtils.makeIdSelection(model), + }; + await this.injectUpdateHierarchy(db, model, findArgs); + if (this.options.logPrismaQuery) { + this.logger.info( + `[delegate] \`updateMany\` find candidates: ${this.getModelName(model)}: ${formatObject(findArgs)}` + ); + } + const entities = await db[model].findMany(findArgs); + + const updatePayload = { data: deepcopy(args.data), select: this.queryUtils.makeIdSelection(model) }; + await this.injectUpdateHierarchy(db, model, updatePayload); + const result = await Promise.all( + entities.map((entity) => { + const updateArgs = { + where: entity, + ...updatePayload, + }; + if (this.options.logPrismaQuery) { + this.logger.info( + `[delegate] \`updateMany\` update: ${this.getModelName(model)}: ${formatObject(updateArgs)}` + ); + } + return db[model].update(updateArgs); + }) + ); + return { count: result.length }; + } + } + + private async injectUpdateHierarchy(db: CrudContract, model: string, args: any) { + const visitor = new NestedWriteVisitor(this.options.modelMeta, { + update: (model, args, _context) => { + this.injectWhereHierarchy(model, (args as any)?.where); + this.doProcessUpdatePayload(model, (args as any)?.data); + }, + + updateMany: async (model, args, context) => { + let simpleUpdateMany = Object.keys(args.data).every((key) => { + // check if the `data` clause involves base fields + const fieldInfo = resolveField(this.options.modelMeta, model, key); + return !fieldInfo?.inheritedFrom; + }); + + if (simpleUpdateMany) { + // check if the `where` clause involves base fields + simpleUpdateMany = Object.keys(args.where || {}).every((key) => { + const fieldInfo = resolveField(this.options.modelMeta, model, key); + return !fieldInfo?.inheritedFrom; + }); + } + + if (simpleUpdateMany) { + this.injectWhereHierarchy(model, (args as any)?.where); + this.doProcessUpdatePayload(model, (args as any)?.data); + } else { + const where = this.queryUtils.buildReversedQuery(context, false, false); + await this.queryUtils.transaction(db, async (tx) => { + await this.doUpdateMany(tx, model, { ...args, where }, simpleUpdateMany); + }); + delete context.parent['updateMany']; + } + }, + + upsert: (model, args, _context) => { + this.injectWhereHierarchy(model, (args as any)?.where); + if (args.create) { + this.doProcessCreatePayload(model, (args as any)?.create); + } + if (args.update) { + this.doProcessUpdatePayload(model, (args as any)?.update); + } + }, + + create: (model, args, _context) => { + if (isDelegateModel(this.options.modelMeta, model)) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + `Model "${model}" is a delegate and cannot be created directly` + ); + } + this.doProcessCreatePayload(model, args); + }, + + createMany: (model, args, _context) => { + if (args.skipDuplicates) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + '`createMany` with `skipDuplicates` set to true is not supported for delegated models' + ); + } + + for (const item of enumerate(args?.data)) { + this.doProcessCreatePayload(model, item); + } + }, + + connect: (model, args, _context) => { + this.injectWhereHierarchy(model, args); + }, + + connectOrCreate: (model, args, _context) => { + this.injectWhereHierarchy(model, args.where); + if (args.create) { + this.doProcessCreatePayload(model, args.create); + } + }, + + disconnect: (model, args, _context) => { + this.injectWhereHierarchy(model, args); + }, + + set: (model, args, _context) => { + this.injectWhereHierarchy(model, args); + }, + + delete: async (model, _args, context) => { + const where = this.queryUtils.buildReversedQuery(context, false, false); + await this.queryUtils.transaction(db, async (tx) => { + await this.doDelete(tx, model, { where }); + }); + delete context.parent['delete']; + }, + + deleteMany: async (model, _args, context) => { + const where = this.queryUtils.buildReversedQuery(context, false, false); + await this.queryUtils.transaction(db, async (tx) => { + await this.doDeleteMany(tx, model, where); + }); + delete context.parent['deleteMany']; + }, + }); + + await visitor.visit(model, 'update', args); + } + + private doProcessUpdatePayload(model: string, data: any) { + if (!data) { + return; + } + + for (const [field, value] of Object.entries(data)) { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + if (fieldInfo?.inheritedFrom) { + this.injectBaseFieldData(model, fieldInfo, value, data, 'update'); + delete data[field]; + } + } + } + + // #endregion + + // #region delete + + override delete(args: any): Promise { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + + if (!this.involvesDelegateModel(this.model)) { + return super.delete(args); + } + + return this.queryUtils.transaction(this.prisma, async (tx) => { + const selectInclude = this.buildSelectIncludeHierarchy(this.model, args); + + // make sure id fields are selected + const idFields = this.getIdFields(this.model); + for (const idField of idFields) { + if (selectInclude?.select && !(idField.name in selectInclude.select)) { + selectInclude.select[idField.name] = true; + } + } + + const deleteArgs = { ...deepcopy(args), ...selectInclude }; + return this.doDelete(tx, this.model, deleteArgs); + }); + } + + override deleteMany(args: any): Promise<{ count: number }> { + if (!this.involvesDelegateModel(this.model)) { + return super.deleteMany(args); + } + + return this.queryUtils.transaction(this.prisma, (tx) => this.doDeleteMany(tx, this.model, args?.where)); + } + + private async doDeleteMany(db: CrudContract, model: string, where: any): Promise<{ count: number }> { + // query existing entities with id + const idSelection = this.queryUtils.makeIdSelection(model); + const findArgs = { where: deepcopy(where), select: idSelection }; + this.injectWhereHierarchy(model, findArgs.where); + + if (this.options.logPrismaQuery) { + this.logger.info( + `[delegate] \`deleteMany\` find candidates: ${this.getModelName(model)}: ${formatObject(findArgs)}` + ); + } + const entities = await db[model].findMany(findArgs); + + // recursively delete base entities (they all have the same id values) + await Promise.all(entities.map((entity) => this.doDelete(db, model, { where: entity }))); + + return { count: entities.length }; + } + + private async deleteBaseRecursively(db: CrudContract, model: string, idValues: any) { + let base = this.getBaseModel(model); + while (base) { + await db[base.name].delete({ where: idValues }); + base = this.getBaseModel(base.name); + } + } + + private async doDelete(db: CrudContract, model: string, args: any): Promise { + this.injectWhereHierarchy(model, args.where); + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`delete\` ${this.getModelName(model)}: ${formatObject(args)}`); + } + const result = await db[model].delete(args); + const idValues = this.queryUtils.getEntityIds(model, result); + + // recursively delete base entities (they all have the same id values) + await this.deleteBaseRecursively(db, model, idValues); + return this.assembleHierarchy(model, result); + } + + // #endregion + + // #region aggregation + + override aggregate(args: any): Promise { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!this.involvesDelegateModel(this.model)) { + return super.aggregate(args); + } + + // check if any aggregation operator is using fields from base + this.checkAggregationArgs('aggregate', args); + + args = deepcopy(args); + + if (args.cursor) { + args.cursor = this.buildWhereHierarchy(this.model, args.cursor); + } + + if (args.orderBy) { + args.orderBy = this.buildWhereHierarchy(this.model, args.orderBy); + } + + if (args.where) { + args.where = this.buildWhereHierarchy(this.model, args.where); + } + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`aggregate\` ${this.getModelName(this.model)}: ${formatObject(args)}`); + } + return super.aggregate(args); + } + + override count(args: any): Promise { + if (!this.involvesDelegateModel(this.model)) { + return super.count(args); + } + + // check if count select is using fields from base + this.checkAggregationArgs('count', args); + + args = deepcopy(args); + + if (args?.cursor) { + args.cursor = this.buildWhereHierarchy(this.model, args.cursor); + } + + if (args?.where) { + args.where = this.buildWhereHierarchy(this.model, args.where); + } + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`count\` ${this.getModelName(this.model)}: ${formatObject(args)}`); + } + return super.count(args); + } + + override groupBy(args: any): Promise { + if (!args) { + throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); + } + if (!this.involvesDelegateModel(this.model)) { + return super.groupBy(args); + } + + // check if count select is using fields from base + this.checkAggregationArgs('groupBy', args); + + if (args.by) { + for (const by of enumerate(args.by)) { + const fieldInfo = resolveField(this.options.modelMeta, this.model, by); + if (fieldInfo && fieldInfo.inheritedFrom) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + `groupBy with fields from base type is not supported yet: "${by}"` + ); + } + } + } + + args = deepcopy(args); + + if (args.where) { + args.where = this.buildWhereHierarchy(this.model, args.where); + } + + if (this.options.logPrismaQuery) { + this.logger.info(`[delegate] \`groupBy\` ${this.getModelName(this.model)}: ${formatObject(args)}`); + } + return super.groupBy(args); + } + + private checkAggregationArgs(operation: 'aggregate' | 'count' | 'groupBy', args: any) { + if (!args) { + return; + } + + for (const op of ['_count', '_sum', '_avg', '_min', '_max', 'select', 'having']) { + if (args[op] && typeof args[op] === 'object') { + for (const field of Object.keys(args[op])) { + const fieldInfo = resolveField(this.options.modelMeta, this.model, field); + if (fieldInfo?.inheritedFrom) { + throw prismaClientValidationError( + this.prisma, + this.options.prismaModule, + `${operation} with fields from base type is not supported yet: "${field}"` + ); + } + } + } + } + } + + // #endregion + + // #region utils + + private extractSelectInclude(args: any) { + if (!args) { + return undefined; + } + args = deepcopy(args); + return 'select' in args + ? { select: args['select'] } + : 'include' in args + ? { include: args['include'] } + : undefined; + } + + private makeAuxRelationName(model: ModelInfo) { + return `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(model.name)}`; + } + + private getModelName(model: string) { + const info = getModelInfo(this.options.modelMeta, model, true); + return info.name; + } + + private getIdFields(model: string): FieldInfo[] { + const idFields = getIdFields(this.options.modelMeta, model); + if (idFields && idFields.length > 0) { + return idFields; + } + const base = this.getBaseModel(model); + return base ? this.getIdFields(base.name) : []; + } + + private getModelInfo(model: string) { + return getModelInfo(this.options.modelMeta, model, true); + } + + private getBaseModel(model: string) { + const baseNames = getModelInfo(this.options.modelMeta, model, true).baseTypes; + if (!baseNames) { + return undefined; + } + if (baseNames.length > 1) { + throw new Error('Multi-inheritance is not supported'); + } + return this.options.modelMeta.models[lowerCaseFirst(baseNames[0])]; + } + + private involvesDelegateModel(model: string, visited?: Set): boolean { + if (this.isDelegateOrDescendantOfDelegate(model)) { + return true; + } + + visited = visited ?? new Set(); + if (visited.has(model)) { + return false; + } + visited.add(model); + + const modelInfo = getModelInfo(this.options.modelMeta, model, true); + return Object.values(modelInfo.fields).some( + (field) => field.isDataModel && this.involvesDelegateModel(field.type, visited) + ); + } + + private isDelegateOrDescendantOfDelegate(model: string): boolean { + if (isDelegateModel(this.options.modelMeta, model)) { + return true; + } + const baseTypes = getModelInfo(this.options.modelMeta, model)?.baseTypes; + return !!( + baseTypes && + baseTypes.length > 0 && + baseTypes.some((base) => this.isDelegateOrDescendantOfDelegate(base)) + ); + } + + private assembleHierarchy(model: string, entity: any) { + if (!entity || typeof entity !== 'object') { + return entity; + } + + const upMerged = this.assembleUp(model, entity); + const downMerged = this.assembleDown(model, entity); + + // https://www.npmjs.com/package/deepmerge#arraymerge-example-combine-arrays + const combineMerge = (target: any[], source: any[], options: ArrayMergeOptions) => { + const destination = target.slice(); + source.forEach((item, index) => { + if (typeof destination[index] === 'undefined') { + destination[index] = options.cloneUnlessOtherwiseSpecified(item, options); + } else if (options.isMergeableObject(item)) { + destination[index] = deepmerge(target[index], item, options); + } else if (target.indexOf(item) === -1) { + destination.push(item); + } + }); + return destination; + }; + + const result = deepmerge(upMerged, downMerged, { + arrayMerge: combineMerge, + }); + return result; + } + + private assembleUp(model: string, entity: any) { + const result: any = {}; + const base = this.getBaseModel(model); + + if (base) { + // merge base fields + const baseRelationName = this.makeAuxRelationName(base); + const baseData = entity[baseRelationName]; + if (baseData && typeof baseData === 'object') { + const baseAssembled = this.assembleUp(base.name, baseData); + Object.assign(result, baseAssembled); + } + } + + const modelInfo = getModelInfo(this.options.modelMeta, model, true); + + for (const [key, value] of Object.entries(entity)) { + if (key.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { + continue; + } + + const field = modelInfo.fields[key]; + if (!field) { + // not a field, could be `_count`, `_sum`, etc. + result[key] = value; + continue; + } + + if (field.inheritedFrom) { + // already merged from base + continue; + } + + if (field.isDataModel) { + if (Array.isArray(value)) { + result[field.name] = value.map((item) => this.assembleUp(field.type, item)); + } else { + result[field.name] = this.assembleUp(field.type, value); + } + } else { + result[field.name] = value; + } + } + + return result; + } + + private assembleDown(model: string, entity: any) { + const result: any = {}; + const modelInfo = getModelInfo(this.options.modelMeta, model, true); + + if (modelInfo.discriminator) { + // model is a delegate, merge sub model fields + const subModelName = entity[modelInfo.discriminator]; + if (subModelName) { + const subModel = getModelInfo(this.options.modelMeta, subModelName, true); + const subRelationName = this.makeAuxRelationName(subModel); + const subData = entity[subRelationName]; + if (subData && typeof subData === 'object') { + const subAssembled = this.assembleDown(subModel.name, subData); + Object.assign(result, subAssembled); + } + } + } + + for (const [key, value] of Object.entries(entity)) { + if (key.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { + continue; + } + + const field = modelInfo.fields[key]; + if (!field) { + // not a field, could be `_count`, `_sum`, etc. + result[key] = value; + continue; + } + + if (field.isDataModel) { + if (Array.isArray(value)) { + result[field.name] = value.map((item) => this.assembleDown(field.type, item)); + } else { + result[field.name] = this.assembleDown(field.type, value); + } + } else { + result[field.name] = value; + } + } + + return result; + } + + // #endregion +} diff --git a/packages/runtime/src/enhancements/enhance.ts b/packages/runtime/src/enhancements/enhance.ts deleted file mode 100644 index 42a504bdf..000000000 --- a/packages/runtime/src/enhancements/enhance.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getDefaultModelMeta } from '../loader'; -import { withOmit, WithOmitOptions } from './omit'; -import { withPassword, WithPasswordOptions } from './password'; -import { withPolicy, WithPolicyContext, WithPolicyOptions } from './policy'; - -/** - * Options @see enhance - */ -export type EnhancementOptions = WithPolicyOptions & WithPasswordOptions & WithOmitOptions; - -let hasPassword: boolean | undefined = undefined; -let hasOmit: boolean | undefined = undefined; - -/** - * Gets a Prisma client enhanced with all essential behaviors, including access - * policy, field validation, field omission and password hashing. - * - * It's a shortcut for calling withOmit(withPassword(withPolicy(prisma, options))). - * - * @param prisma The Prisma client to enhance. - * @param context The context to for evaluating access policies. - * @param options Options. - */ -export function enhance( - prisma: DbClient, - context?: WithPolicyContext, - options?: EnhancementOptions -) { - let result = prisma; - - if (hasPassword === undefined || hasOmit === undefined) { - const modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); - const allFields = Object.values(modelMeta.fields).flatMap((modelInfo) => Object.values(modelInfo)); - hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password')); - hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit')); - } - - if (hasPassword) { - // @password proxy - result = withPassword(result, options); - } - - if (hasOmit) { - // @omit proxy - result = withOmit(result, options); - } - - // policy proxy - result = withPolicy(result, context, options); - - return result; -} diff --git a/packages/runtime/src/enhancements/index.ts b/packages/runtime/src/enhancements/index.ts index 25b150a71..3ddeddac0 100644 --- a/packages/runtime/src/enhancements/index.ts +++ b/packages/runtime/src/enhancements/index.ts @@ -1,9 +1,4 @@ export * from '../cross'; -export * from './enhance'; -export * from './omit'; -export * from './password'; -export * from './policy'; -export * from './preset'; +export * from './create-enhancement'; export * from './types'; export * from './utils'; -export * from './where-visitor'; diff --git a/packages/runtime/src/enhancements/policy/logger.ts b/packages/runtime/src/enhancements/logger.ts similarity index 75% rename from packages/runtime/src/enhancements/policy/logger.ts rename to packages/runtime/src/enhancements/logger.ts index dc61c471e..e53551563 100644 --- a/packages/runtime/src/enhancements/policy/logger.ts +++ b/packages/runtime/src/enhancements/logger.ts @@ -13,7 +13,15 @@ export class Logger { const engine = (this.prisma as any)._engine; this.emitter = engine ? (engine.logEmitter as EventEmitter) : undefined; if (this.emitter) { - this.eventNames = this.emitter.eventNames(); + if (typeof this.emitter.eventNames === 'function') { + // Node.js + this.eventNames = this.emitter.eventNames(); + } else if ('events' in this.emitter && this.emitter.events && typeof this.emitter.events === 'object') { + // edge runtime + this.eventNames = Object.keys((this.emitter as any).events); + } else { + this.eventNames = []; + } } } diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index 2df81b40a..fa834166d 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -1,40 +1,28 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { enumerate, getModelFields, resolveField, type ModelMeta } from '../cross'; -import { getDefaultModelMeta } from '../loader'; +import { enumerate, getModelFields, resolveField } from '../cross'; import { DbClientContract } from '../types'; +import { InternalEnhancementOptions } from './create-enhancement'; import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; -import { CommonEnhancementOptions } from './types'; /** - * Options for @see withOmit - */ -export interface WithOmitOptions extends CommonEnhancementOptions { - /** - * Model metadata - */ - modelMeta?: ModelMeta; -} - -/** - * Gets an enhanced Prisma client that supports "@omit" attribute. + * Gets an enhanced Prisma client that supports `@omit` attribute. * - * @deprecated Use {@link enhance} instead + * @private */ -export function withOmit(prisma: DbClient, options?: WithOmitOptions): DbClient { - const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); +export function withOmit(prisma: DbClient, options: InternalEnhancementOptions): DbClient { return makeProxy( prisma, - _modelMeta, - (_prisma, model) => new OmitHandler(_prisma as DbClientContract, model, _modelMeta), + options.modelMeta, + (_prisma, model) => new OmitHandler(_prisma as DbClientContract, model, options), 'omit' ); } class OmitHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, private readonly modelMeta: ModelMeta) { - super(prisma, model); + constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { + super(prisma, model, options); } // base override @@ -49,7 +37,7 @@ class OmitHandler extends DefaultPrismaProxyHandler { private async doPostProcess(entityData: any, model: string) { for (const field of getModelFields(entityData)) { - const fieldInfo = await resolveField(this.modelMeta, model, field); + const fieldInfo = await resolveField(this.options.modelMeta, model, field); if (!fieldInfo) { continue; } diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index c31846298..297613e1b 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -1,42 +1,40 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { hash } from 'bcryptjs'; import { DEFAULT_PASSWORD_SALT_LENGTH } from '../constants'; -import { NestedWriteVisitor, type ModelMeta, type PrismaWriteActionType } from '../cross'; -import { getDefaultModelMeta } from '../loader'; +import { NestedWriteVisitor, type PrismaWriteActionType } from '../cross'; import { DbClientContract } from '../types'; +import { InternalEnhancementOptions } from './create-enhancement'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; -import { CommonEnhancementOptions } from './types'; /** - * Options for @see withPassword - */ -export interface WithPasswordOptions extends CommonEnhancementOptions { - /** - * Model metadata - */ - modelMeta?: ModelMeta; -} - -/** - * Gets an enhanced Prisma client that supports @password attribute. + * Gets an enhanced Prisma client that supports `@password` attribute. * - * @deprecated Use {@link enhance} instead + * @private */ -export function withPassword(prisma: DbClient, options?: WithPasswordOptions): DbClient { - const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); +export function withPassword( + prisma: DbClient, + options: InternalEnhancementOptions +): DbClient { return makeProxy( prisma, - _modelMeta, - (_prisma, model) => new PasswordHandler(_prisma as DbClientContract, model, _modelMeta), + options.modelMeta, + (_prisma, model) => new PasswordHandler(_prisma as DbClientContract, model, options), 'password' ); } +// `bcryptjs.hash` is good for performance but it doesn't work in vercel edge runtime, +// so we fall back to `bcrypt.hash` in that case. + +// eslint-disable-next-line no-var +declare var EdgeRuntime: any; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const hashFunc = typeof EdgeRuntime === 'string' ? require('bcryptjs').hashSync : require('bcryptjs').hash; + class PasswordHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, readonly modelMeta: ModelMeta) { - super(prisma, model); + constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { + super(prisma, model, options); } // base override @@ -49,7 +47,7 @@ class PasswordHandler extends DefaultPrismaProxyHandler { } private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) { - const visitor = new NestedWriteVisitor(this.modelMeta, { + const visitor = new NestedWriteVisitor(this.options.modelMeta, { field: async (field, _action, data, context) => { const pwdAttr = field.attributes?.find((attr) => attr.name === '@password'); if (pwdAttr && field.type === 'String') { @@ -62,7 +60,7 @@ class PasswordHandler extends DefaultPrismaProxyHandler { if (!salt) { salt = DEFAULT_PASSWORD_SALT_LENGTH; } - context.parent[field.name] = await hash(data, salt); + context.parent[field.name] = await hashFunc(data, salt); } }, }); diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 34c74f9dd..d6d893d4e 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -4,7 +4,6 @@ import { lowerCaseFirst } from 'lower-case-first'; import invariant from 'tiny-invariant'; import { upperCaseFirst } from 'upper-case-first'; import { fromZodError } from 'zod-validation-error'; -import type { WithPolicyOptions } from '.'; import { CrudFailureReason } from '../../constants'; import { ModelDataVisitor, @@ -17,13 +16,14 @@ import { type FieldInfo, type ModelMeta, } from '../../cross'; -import { AuthUser, DbClientContract, DbOperations, PolicyOperationKind } from '../../types'; +import { PolicyOperationKind, type CrudContract, type DbClientContract } from '../../types'; +import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; +import { Logger } from '../logger'; +import { createDeferredPromise, createFluentPromise } from '../promise'; import { PrismaProxyHandler } from '../proxy'; -import type { PolicyDef, ZodSchemas } from '../types'; -import { formatObject, prismaClientValidationError } from '../utils'; -import { Logger } from './logger'; +import { QueryUtils } from '../query-utils'; +import { clone, formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils'; import { PolicyUtil } from './policy-utils'; -import { createDeferredPromise } from './promise'; // a record for post-write policy check type PostWriteCheckRecord = { @@ -40,32 +40,25 @@ type FindOperations = 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFi */ export class PolicyProxyHandler implements PrismaProxyHandler { private readonly logger: Logger; - private readonly utils: PolicyUtil; + private readonly policyUtils: PolicyUtil; private readonly model: string; - - private readonly DEFAULT_TX_MAXWAIT = 100000; - private readonly DEFAULT_TX_TIMEOUT = 100000; + private readonly modelMeta: ModelMeta; + private readonly prismaModule: any; + private readonly queryUtils: QueryUtils; constructor( private readonly prisma: DbClient, - private readonly policy: PolicyDef, - private readonly modelMeta: ModelMeta, - private readonly zodSchemas: ZodSchemas | undefined, model: string, - private readonly user: AuthUser | undefined, - private readonly options: WithPolicyOptions | undefined + private readonly options: InternalEnhancementOptions, + private readonly context?: EnhancementContext ) { this.logger = new Logger(prisma); - this.utils = new PolicyUtil( - this.prisma, - this.options, - this.modelMeta, - this.policy, - this.zodSchemas, - this.user, - this.shouldLogQuery - ); this.model = lowerCaseFirst(model); + + ({ modelMeta: this.modelMeta, prismaModule: this.prismaModule } = options); + + this.policyUtils = new PolicyUtil(prisma, options, context, this.shouldLogQuery); + this.queryUtils = new QueryUtils(prisma, options); } private get modelClient() { @@ -78,33 +71,43 @@ export class PolicyProxyHandler implements Pr findUnique(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'where field is required in query argument' + ); } - return this.findWithFluentCallStubs(args, 'findUnique', false, () => null); + + return this.findWithFluent('findUnique', args, () => null); } findUniqueOrThrow(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'where field is required in query argument' + ); } - return this.findWithFluentCallStubs(args, 'findUniqueOrThrow', true, () => { - throw this.utils.notFound(this.model); + + return this.findWithFluent('findUniqueOrThrow', args, () => { + throw this.policyUtils.notFound(this.model); }); } findFirst(args?: any) { - return this.findWithFluentCallStubs(args, 'findFirst', false, () => null); + return this.findWithFluent('findFirst', args, () => null); } findFirstOrThrow(args: any) { - return this.findWithFluentCallStubs(args, 'findFirstOrThrow', true, () => { - throw this.utils.notFound(this.model); + return this.findWithFluent('findFirstOrThrow', args, () => { + throw this.policyUtils.notFound(this.model); }); } @@ -112,177 +115,117 @@ export class PolicyProxyHandler implements Pr return createDeferredPromise(() => this.doFind(args, 'findMany', () => [])); } - // returns a promise for the given find operation, together with function stubs for fluent API calls - private findWithFluentCallStubs( - args: any, - actionName: FindOperations, - resolveRoot: boolean, - handleRejection: () => any - ) { - // create a deferred promise so it's only evaluated when awaited or .then() is called - const result = createDeferredPromise(() => this.doFind(args, actionName, handleRejection)); - this.addFluentFunctions(result, this.model, args?.where, resolveRoot ? result : undefined); - return result; + // make a find query promise with fluent API call stubs installed + private findWithFluent(method: FindOperations, args: any, handleRejection: () => any) { + args = clone(args); + return createFluentPromise( + () => this.doFind(args, method, handleRejection), + args, + this.options.modelMeta, + this.model + ); } - private doFind(args: any, actionName: FindOperations, handleRejection: () => any) { + private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) { const origArgs = args; - const _args = this.utils.clone(args); - if (!this.utils.injectForRead(this.prisma, this.model, _args)) { + const _args = clone(args); + if (!this.policyUtils.injectForRead(this.prisma, this.model, _args)) { + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`${actionName}\` ${this.model}: unconditionally denied`); + } return handleRejection(); } - this.utils.injectReadCheckSelect(this.model, _args); + this.policyUtils.injectReadCheckSelect(this.model, _args); if (this.shouldLogQuery) { this.logger.info(`[policy] \`${actionName}\` ${this.model}:\n${formatObject(_args)}`); } - return new Promise((resolve, reject) => { - this.modelClient[actionName](_args).then( - (value: any) => { - this.utils.postProcessForRead(value, this.model, origArgs); - resolve(value); - }, - (err: any) => reject(err) - ); - }); - } - - // returns a fluent API call function - private fluentCall(filter: any, fieldInfo: FieldInfo, rootPromise?: Promise) { - return (args: any) => { - args = this.utils.clone(args); - - // combine the parent filter with the current one - const backLinkField = this.requireBackLink(fieldInfo); - const condition = backLinkField.isArray - ? { [backLinkField.name]: { some: filter } } - : { [backLinkField.name]: { is: filter } }; - args.where = this.utils.and(args.where, condition); - - const promise = createDeferredPromise(() => { - // Promise for fetching - const fetchFluent = (resolve: (value: unknown) => void, reject: (reason?: any) => void) => { - const handler = this.makeHandler(fieldInfo.type); - if (fieldInfo.isArray) { - // fluent call stops here - handler.findMany(args).then( - (value: any) => resolve(value), - (err: any) => reject(err) - ); - } else { - handler.findFirst(args).then( - (value) => resolve(value), - (err) => reject(err) - ); - } - }; - - return new Promise((resolve, reject) => { - if (rootPromise) { - // if a root promise exists, resolve it before fluent API call, - // so that fluent calls start with `findUniqueOrThrow` and `findFirstOrThrow` - // can throw error properly if the root promise is rejected - rootPromise.then( - () => fetchFluent(resolve, reject), - (err) => reject(err) - ); - } else { - fetchFluent(resolve, reject); - } - }); - }); - - if (!fieldInfo.isArray) { - // prepare for a chained fluent API call - this.addFluentFunctions(promise, fieldInfo.type, args.where, rootPromise); - } - - return promise; - }; - } - - // add fluent API functions to the given promise - private addFluentFunctions(promise: any, model: string, filter: any, rootPromise?: Promise) { - const fields = this.utils.getModelFields(model); - if (fields) { - for (const [field, fieldInfo] of Object.entries(fields)) { - if (fieldInfo.isDataModel) { - promise[field] = this.fluentCall(filter, fieldInfo, rootPromise); - } - } - } + const result = await this.modelClient[actionName](_args); + this.policyUtils.postProcessForRead(result, this.model, origArgs); + return result; } //#endregion //#region Create - async create(args: any) { + create(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'data field is required in query argument' + ); } - this.utils.tryReject(this.prisma, this.model, 'create'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'create'); - const origArgs = args; - args = this.utils.clone(args); + const origArgs = args; + args = clone(args); - // static input policy check for top-level create data - const inputCheck = this.utils.checkInputGuard(this.model, args.data, 'create'); - if (inputCheck === false) { - throw this.utils.deniedByPolicy(this.model, 'create', undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); - } + // static input policy check for top-level create data + const inputCheck = this.policyUtils.checkInputGuard(this.model, args.data, 'create'); + if (inputCheck === false) { + throw this.policyUtils.deniedByPolicy( + this.model, + 'create', + undefined, + CrudFailureReason.ACCESS_POLICY_VIOLATION + ); + } - const hasNestedCreateOrConnect = await this.hasNestedCreateOrConnect(args); + const hasNestedCreateOrConnect = await this.hasNestedCreateOrConnect(args); - const { result, error } = await this.transaction(async (tx) => { - if ( - // MUST check true here since inputCheck can be undefined (meaning static input check not possible) - inputCheck === true && - // simple create: no nested create/connect - !hasNestedCreateOrConnect - ) { - // there's no nested write and we've passed input check, proceed with the create directly + const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { + if ( + // MUST check true here since inputCheck can be undefined (meaning static input check not possible) + inputCheck === true && + // simple create: no nested create/connect + !hasNestedCreateOrConnect + ) { + // there's no nested write and we've passed input check, proceed with the create directly - // validate zod schema if any - args.data = this.validateCreateInputSchema(this.model, args.data); + // validate zod schema if any + args.data = this.validateCreateInputSchema(this.model, args.data); - // make a create args only containing data and ID selection - const createArgs: any = { data: args.data, select: this.utils.makeIdSelection(this.model) }; + // make a create args only containing data and ID selection + const createArgs: any = { data: args.data, select: this.policyUtils.makeIdSelection(this.model) }; - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`create\` ${this.model}: ${formatObject(createArgs)}`); - } - const result = await tx[this.model].create(createArgs); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`create\` ${this.model}: ${formatObject(createArgs)}`); + } + const result = await tx[this.model].create(createArgs); - // filter the read-back data - return this.utils.readBack(tx, this.model, 'create', args, result); - } else { - // proceed with a complex create and collect post-write checks - const { result, postWriteChecks } = await this.doCreate(this.model, args, tx); + // filter the read-back data + return this.policyUtils.readBack(tx, this.model, 'create', args, result); + } else { + // proceed with a complex create and collect post-write checks + const { result, postWriteChecks } = await this.doCreate(this.model, args, tx); - // execute post-write checks - await this.runPostWriteChecks(postWriteChecks, tx); + // execute post-write checks + await this.runPostWriteChecks(postWriteChecks, tx); - // filter the read-back data - return this.utils.readBack(tx, this.model, 'create', origArgs, result); + // filter the read-back data + return this.policyUtils.readBack(tx, this.model, 'create', origArgs, result); + } + }); + + if (error) { + throw error; + } else { + return result; } }); - - if (error) { - throw error; - } else { - return result; - } } // create with nested write - private async doCreate(model: string, args: any, db: Record) { + private async doCreate(model: string, args: any, db: CrudContract) { // record id fields involved in the nesting context const idSelections: Array<{ path: FieldInfo[]; ids: string[] }> = []; const pushIdFields = (model: string, context: NestedWriteVisitorContext) => { @@ -308,7 +251,7 @@ export class PolicyProxyHandler implements Pr create: async (model, args, context) => { const validateResult = this.validateCreateInputSchema(model, args); if (validateResult !== args) { - this.utils.replace(args, validateResult); + this.policyUtils.replace(args, validateResult); } pushIdFields(model, context); }, @@ -317,7 +260,7 @@ export class PolicyProxyHandler implements Pr enumerate(args.data).forEach((item) => { const r = this.validateCreateInputSchema(model, item); if (r !== item) { - this.utils.replace(item, r); + this.policyUtils.replace(item, r); } }); pushIdFields(model, context); @@ -325,14 +268,14 @@ export class PolicyProxyHandler implements Pr connectOrCreate: async (model, args, context) => { if (!args.where) { - throw this.utils.validationError(`'where' field is required for connectOrCreate`); + throw this.policyUtils.validationError(`'where' field is required for connectOrCreate`); } if (args.create) { args.create = this.validateCreateInputSchema(model, args.create); } - const existing = await this.utils.checkExistence(db, model, args.where); + const existing = await this.policyUtils.checkExistence(db, model, args.where); if (existing) { // connect case if (context.field?.backLink) { @@ -340,7 +283,7 @@ export class PolicyProxyHandler implements Pr if (backLinkField?.isRelationOwner) { // the target side of relation owns the relation, // check if it's updatable - await this.utils.checkPolicyForUnique(model, args.where, 'update', db, args); + await this.policyUtils.checkPolicyForUnique(model, args.where, 'update', db, args); } } @@ -364,18 +307,18 @@ export class PolicyProxyHandler implements Pr connect: async (model, args, context) => { if (!args || typeof args !== 'object' || Object.keys(args).length === 0) { - throw this.utils.validationError(`'connect' field must be an non-empty object`); + throw this.policyUtils.validationError(`'connect' field must be an non-empty object`); } if (context.field?.backLink) { const backLinkField = resolveField(this.modelMeta, model, context.field.backLink); if (backLinkField?.isRelationOwner) { // check existence - await this.utils.checkExistence(db, model, args, true); + await this.policyUtils.checkExistence(db, model, args, true); // the target side of relation owns the relation, // check if it's updatable - await this.utils.checkPolicyForUnique(model, args, 'update', db, args); + await this.policyUtils.checkPolicyForUnique(model, args, 'update', db, args); } } }, @@ -415,12 +358,13 @@ export class PolicyProxyHandler implements Pr const key = getEntityKey(model, scalarData); // only check if entity is created, not connected if (!connectedEntities.has(key) && !postCreateChecks.has(key)) { - postCreateChecks.set(key, { model, operation: 'create', uniqueFilter: scalarData }); + const idFields = this.policyUtils.getIdFieldValues(model, scalarData); + postCreateChecks.set(key, { model, operation: 'create', uniqueFilter: idFields }); } }); // return only the ids of the top-level entity - const ids = this.utils.getEntityIds(model, result); + const ids = this.policyUtils.getEntityIds(model, result); return { result: ids, postWriteChecks: [...postCreateChecks.values()] }; } @@ -457,11 +401,11 @@ export class PolicyProxyHandler implements Pr // Validates the given create payload against Zod schema if any private validateCreateInputSchema(model: string, data: any) { - const schema = this.utils.getZodSchema(model, 'create'); + const schema = this.policyUtils.getZodSchema(model, 'create'); if (schema && data) { const parseResult = schema.safeParse(data); if (!parseResult.success) { - throw this.utils.deniedByPolicy( + throw this.policyUtils.deniedByPolicy( model, 'create', `input failed validation: ${fromZodError(parseResult.error)}`, @@ -475,62 +419,64 @@ export class PolicyProxyHandler implements Pr } } - async createMany(args: { data: any; skipDuplicates?: boolean }) { + createMany(args: { data: any; skipDuplicates?: boolean }) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'data field is required in query argument' + ); } - this.utils.tryReject(this.prisma, this.model, 'create'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'create'); - args = this.utils.clone(args); + args = clone(args); - // go through create items, statically check input to determine if post-create - // check is needed, and also validate zod schema - let needPostCreateCheck = false; - for (const item of enumerate(args.data)) { - const validationResult = this.validateCreateInputSchema(this.model, item); - if (validationResult !== item) { - this.utils.replace(item, validationResult); - } + // go through create items, statically check input to determine if post-create + // check is needed, and also validate zod schema + let needPostCreateCheck = false; + for (const item of enumerate(args.data)) { + const validationResult = this.validateCreateInputSchema(this.model, item); + if (validationResult !== item) { + this.policyUtils.replace(item, validationResult); + } - const inputCheck = this.utils.checkInputGuard(this.model, item, 'create'); - if (inputCheck === false) { - // unconditionally deny - throw this.utils.deniedByPolicy( - this.model, - 'create', - undefined, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } else if (inputCheck === true) { - // unconditionally allow - } else if (inputCheck === undefined) { - // static policy check is not possible, need to do post-create check - needPostCreateCheck = true; + const inputCheck = this.policyUtils.checkInputGuard(this.model, item, 'create'); + if (inputCheck === false) { + // unconditionally deny + throw this.policyUtils.deniedByPolicy( + this.model, + 'create', + undefined, + CrudFailureReason.ACCESS_POLICY_VIOLATION + ); + } else if (inputCheck === true) { + // unconditionally allow + } else if (inputCheck === undefined) { + // static policy check is not possible, need to do post-create check + needPostCreateCheck = true; + } } - } - if (!needPostCreateCheck) { - return this.modelClient.createMany(args); - } else { - // create entities in a transaction with post-create checks - return this.transaction(async (tx) => { - const { result, postWriteChecks } = await this.doCreateMany(this.model, args, tx); - // post-create check - await this.runPostWriteChecks(postWriteChecks, tx); - return result; - }); - } + if (!needPostCreateCheck) { + return this.modelClient.createMany(args); + } else { + // create entities in a transaction with post-create checks + return this.queryUtils.transaction(this.prisma, async (tx) => { + const { result, postWriteChecks } = await this.doCreateMany(this.model, args, tx); + // post-create check + await this.runPostWriteChecks(postWriteChecks, tx); + return result; + }); + } + }); } - private async doCreateMany( - model: string, - args: { data: any; skipDuplicates?: boolean }, - db: Record - ) { + private async doCreateMany(model: string, args: { data: any; skipDuplicates?: boolean }, db: CrudContract) { // We can't call the native "createMany" because we can't get back what was created // for post-create checks. Instead, do a "create" for each item and collect the results. @@ -548,7 +494,7 @@ export class PolicyProxyHandler implements Pr if (this.shouldLogQuery) { this.logger.info(`[policy] \`create\` for \`createMany\` ${model}: ${formatObject(item)}`); } - return await db[model].create({ select: this.utils.makeIdSelection(model), data: item }); + return await db[model].create({ select: this.policyUtils.makeIdSelection(model), data: item }); }) ); @@ -565,12 +511,7 @@ export class PolicyProxyHandler implements Pr }; } - private async hasDuplicatedUniqueConstraint( - model: string, - createData: any, - upstreamQuery: any, - db: Record - ) { + private async hasDuplicatedUniqueConstraint(model: string, createData: any, upstreamQuery: any, db: CrudContract) { // check unique constraint conflicts // we can't rely on try/catch/ignore constraint violation error: https://github.com/prisma/prisma/issues/20496 // TODO: for simple cases we should be able to translate it to an `upsert` with empty `update` payload @@ -578,7 +519,7 @@ export class PolicyProxyHandler implements Pr // for each unique constraint, check if the input item has all fields set, and if so, check if // an entity already exists, and ignore accordingly - const uniqueConstraints = this.utils.getUniqueConstraints(model); + const uniqueConstraints = this.policyUtils.getUniqueConstraints(model); for (const constraint of Object.values(uniqueConstraints)) { // the unique filter used to check existence @@ -634,7 +575,7 @@ export class PolicyProxyHandler implements Pr if (remainingConstraintFields.size === 0) { // all constraint fields set, check existence - const existing = await this.utils.checkExistence(db, model, uniqueFilter); + const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter); if (existing) { return true; } @@ -654,38 +595,48 @@ export class PolicyProxyHandler implements Pr // "updateMany" works against a set of entities, entities not passing policy check are silently // ignored - async update(args: any) { + update(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'where field is required in query argument' + ); } if (!args.data) { - throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'data field is required in query argument' + ); } - args = this.utils.clone(args); + return createDeferredPromise(async () => { + args = clone(args); - const { result, error } = await this.transaction(async (tx) => { - // proceed with nested writes and collect post-write checks - const { result, postWriteChecks } = await this.doUpdate(args, tx); + const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { + // proceed with nested writes and collect post-write checks + const { result, postWriteChecks } = await this.doUpdate(args, tx); - // post-write check - await this.runPostWriteChecks(postWriteChecks, tx); + // post-write check + await this.runPostWriteChecks(postWriteChecks, tx); - // filter the read-back data - return this.utils.readBack(tx, this.model, 'update', args, result); - }); + // filter the read-back data + return this.policyUtils.readBack(tx, this.model, 'update', args, result); + }); - if (error) { - throw error; - } else { - return result; - } + if (error) { + throw error; + } else { + return result; + } + }); } - private async doUpdate(args: any, db: Record) { + private async doUpdate(args: any, db: CrudContract) { // collected post-update checks const postWriteChecks: PostWriteCheckRecord[] = []; @@ -696,10 +647,10 @@ export class PolicyProxyHandler implements Pr postUpdateLookupFilter: any ) => { // both "post-update" rules and Zod schemas require a post-update check - if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) { + if (this.policyUtils.hasAuthGuard(model, 'postUpdate') || this.policyUtils.getZodSchema(model)) { // select pre-update field values let preValue: any; - const preValueSelect = this.utils.getPreValueSelect(model); + const preValueSelect = this.policyUtils.getPreValueSelect(model); if (preValueSelect && Object.keys(preValueSelect).length > 0) { preValue = await db[model].findFirst({ where: preUpdateLookupFilter, select: preValueSelect }); } @@ -728,10 +679,10 @@ export class PolicyProxyHandler implements Pr // operations. E.g.: // - safe: { data: { user: { connect: { id: 1 }} } } // - unsafe: { data: { userId: 1 } } - const unsafe = this.isUnsafeMutate(model, args); + const unsafe = isUnsafeMutate(model, args, this.modelMeta); // handles the connection to upstream entity - const reversedQuery = this.utils.buildReversedQuery(context, true, unsafe); + const reversedQuery = this.policyUtils.buildReversedQuery(context, true, unsafe); if ((!unsafe || context.field.isRelationOwner) && reversedQuery[context.field.backLink]) { // if mutation is safe, or current field owns the relation (so the other side has no fk), // and the reverse query contains the back link, then we can build a "connect" with it @@ -766,11 +717,11 @@ export class PolicyProxyHandler implements Pr // for example when it's nested inside a one-to-one update const upstreamQuery = { where: reversedQuery[backLinkField.name], - select: this.utils.makeIdSelection(backLinkField.type), + select: this.policyUtils.makeIdSelection(backLinkField.type), }; // fetch the upstream entity - if (this.logger.enabled('info')) { + if (this.shouldLogQuery) { this.logger.info( `[policy] \`findUniqueOrThrow\` ${model}: looking up upstream entity of ${ backLinkField.type @@ -807,7 +758,7 @@ export class PolicyProxyHandler implements Pr if (args.skipDuplicates) { // get a reversed query to include fields inherited from upstream mutation, // it'll be merged with the create payload for unique constraint checking - const upstreamQuery = this.utils.buildReversedQuery(context); + const upstreamQuery = this.policyUtils.buildReversedQuery(context); if (await this.hasDuplicatedUniqueConstraint(model, item, upstreamQuery, db)) { if (this.shouldLogQuery) { this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`); @@ -821,8 +772,8 @@ export class PolicyProxyHandler implements Pr const _connectDisconnect = async (model: string, args: any, context: NestedWriteVisitorContext) => { if (context.field?.backLink) { - const backLinkField = this.utils.getModelField(model, context.field.backLink); - if (backLinkField.isRelationOwner) { + const backLinkField = this.policyUtils.getModelField(model, context.field.backLink); + if (backLinkField?.isRelationOwner) { // update happens on the related model, require updatable, // translate args to foreign keys so field-level policies can be checked const checkArgs: any = {}; @@ -834,7 +785,7 @@ export class PolicyProxyHandler implements Pr } } } - await this.utils.checkPolicyForUnique(model, args, 'update', db, checkArgs); + await this.policyUtils.checkPolicyForUnique(model, args, 'update', db, checkArgs); // register post-update check await _registerPostUpdateCheck(model, args, args); @@ -846,10 +797,10 @@ export class PolicyProxyHandler implements Pr const visitor = new NestedWriteVisitor(this.modelMeta, { update: async (model, args, context) => { // build a unique query including upstream conditions - const uniqueFilter = this.utils.buildReversedQuery(context); + const uniqueFilter = this.policyUtils.buildReversedQuery(context); // handle not-found - const existing = await this.utils.checkExistence(db, model, uniqueFilter, true); + const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter, true); // check if the update actually writes to this model let thisModelUpdate = false; @@ -857,7 +808,7 @@ export class PolicyProxyHandler implements Pr const validatedPayload = this.validateUpdateInputSchema(model, updatePayload); if (validatedPayload !== updatePayload) { - this.utils.replace(updatePayload, validatedPayload); + this.policyUtils.replace(updatePayload, validatedPayload); } if (updatePayload) { @@ -878,10 +829,10 @@ export class PolicyProxyHandler implements Pr } if (thisModelUpdate) { - this.utils.tryReject(db, this.model, 'update'); + this.policyUtils.tryReject(db, this.model, 'update'); // check pre-update guard - await this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); + await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); // handle the case where id fields are updated const _args: any = args; @@ -895,15 +846,15 @@ export class PolicyProxyHandler implements Pr updateMany: async (model, args, context) => { // prepare for post-update check - if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) { - let select = this.utils.makeIdSelection(model); - const preValueSelect = this.utils.getPreValueSelect(model); + if (this.policyUtils.hasAuthGuard(model, 'postUpdate') || this.policyUtils.getZodSchema(model)) { + let select = this.policyUtils.makeIdSelection(model); + const preValueSelect = this.policyUtils.getPreValueSelect(model); if (preValueSelect) { select = { ...select, ...preValueSelect }; } - const reversedQuery = this.utils.buildReversedQuery(context); + const reversedQuery = this.policyUtils.buildReversedQuery(context); const currentSetQuery = { select, where: reversedQuery }; - this.utils.injectAuthGuardAsWhere(db, currentSetQuery, model, 'read'); + this.policyUtils.injectAuthGuardAsWhere(db, currentSetQuery, model, 'read'); if (this.shouldLogQuery) { this.logger.info( @@ -924,15 +875,15 @@ export class PolicyProxyHandler implements Pr args.data = this.validateUpdateInputSchema(model, args.data); - const updateGuard = this.utils.getAuthGuard(db, model, 'update'); - if (this.utils.isTrue(updateGuard) || this.utils.isFalse(updateGuard)) { + const updateGuard = this.policyUtils.getAuthGuard(db, model, 'update'); + if (this.policyUtils.isTrue(updateGuard) || this.policyUtils.isFalse(updateGuard)) { // injects simple auth guard into where clause - this.utils.injectAuthGuardAsWhere(db, args, model, 'update'); + this.policyUtils.injectAuthGuardAsWhere(db, args, model, 'update'); } else { // we have to process `updateMany` separately because the guard may contain // filters using relation fields which are not allowed in nested `updateMany` - const reversedQuery = this.utils.buildReversedQuery(context); - const updateWhere = this.utils.and(reversedQuery, updateGuard); + const reversedQuery = this.policyUtils.buildReversedQuery(context); + const updateWhere = this.policyUtils.and(reversedQuery, updateGuard); if (this.shouldLogQuery) { this.logger.info( `[policy] \`updateMany\` ${model}:\n${formatObject({ @@ -970,15 +921,15 @@ export class PolicyProxyHandler implements Pr upsert: async (model, args, context) => { // build a unique query including upstream conditions - const uniqueFilter = this.utils.buildReversedQuery(context); + const uniqueFilter = this.policyUtils.buildReversedQuery(context); // branch based on if the update target exists - const existing = await this.utils.checkExistence(db, model, uniqueFilter); + const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter); if (existing) { // update case // check pre-update guard - await this.utils.checkPolicyForUnique(model, existing, 'update', db, args); + await this.policyUtils.checkPolicyForUnique(model, existing, 'update', db, args); // handle the case where id fields are updated const postUpdateIds = this.calculatePostUpdateIds(model, existing, args.update); @@ -1014,7 +965,7 @@ export class PolicyProxyHandler implements Pr connectOrCreate: async (model, args, context) => { // the where condition is already unique, so we can use it to check if the target exists - const existing = await this.utils.checkExistence(db, model, args.where); + const existing = await this.policyUtils.checkExistence(db, model, args.where); if (existing) { // connect await _connectDisconnect(model, args.where, context); @@ -1041,9 +992,9 @@ export class PolicyProxyHandler implements Pr set: async (model, args, context) => { // find the set of items to be replaced - const reversedQuery = this.utils.buildReversedQuery(context); + const reversedQuery = this.policyUtils.buildReversedQuery(context); const findCurrSetArgs = { - select: this.utils.makeIdSelection(model), + select: this.policyUtils.makeIdSelection(model), where: reversedQuery, }; if (this.shouldLogQuery) { @@ -1060,25 +1011,25 @@ export class PolicyProxyHandler implements Pr delete: async (model, args, context) => { // build a unique query including upstream conditions - const uniqueFilter = this.utils.buildReversedQuery(context); + const uniqueFilter = this.policyUtils.buildReversedQuery(context); // handle not-found - await this.utils.checkExistence(db, model, uniqueFilter, true); + await this.policyUtils.checkExistence(db, model, uniqueFilter, true); // check delete guard - await this.utils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, args); + await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, args); }, deleteMany: async (model, args, context) => { - const guard = await this.utils.getAuthGuard(db, model, 'delete'); - if (this.utils.isTrue(guard) || this.utils.isFalse(guard)) { + const guard = await this.policyUtils.getAuthGuard(db, model, 'delete'); + if (this.policyUtils.isTrue(guard) || this.policyUtils.isFalse(guard)) { // inject simple auth guard - context.parent.deleteMany = this.utils.and(args, guard); + context.parent.deleteMany = this.policyUtils.and(args, guard); } else { // we have to process `deleteMany` separately because the guard may contain // filters using relation fields which are not allowed in nested `deleteMany` - const reversedQuery = this.utils.buildReversedQuery(context); - const deleteWhere = this.utils.and(reversedQuery, guard); + const reversedQuery = this.policyUtils.buildReversedQuery(context); + const deleteWhere = this.policyUtils.and(reversedQuery, guard); if (this.shouldLogQuery) { this.logger.info(`[policy] \`deleteMany\` ${model}:\n${formatObject({ where: deleteWhere })}`); } @@ -1097,7 +1048,7 @@ export class PolicyProxyHandler implements Pr const result = await db[this.model].update({ where: args.where, data: args.data, - select: this.utils.makeIdSelection(this.model), + select: this.policyUtils.makeIdSelection(this.model), }); return { result, postWriteChecks }; @@ -1105,7 +1056,7 @@ export class PolicyProxyHandler implements Pr // calculate id fields used for post-update check given an update payload private calculatePostUpdateIds(_model: string, currentIds: any, updatePayload: any) { - const result = this.utils.clone(currentIds); + const result = clone(currentIds); for (const key of Object.keys(currentIds)) { const updateValue = updatePayload[key]; if (typeof updateValue === 'string' || typeof updateValue === 'number' || typeof updateValue === 'bigint') { @@ -1134,7 +1085,7 @@ export class PolicyProxyHandler implements Pr } // deal with compound id fields - const uniqueConstraints = this.utils.getUniqueConstraints(model); + const uniqueConstraints = this.policyUtils.getUniqueConstraints(model); for (const [name, constraint] of Object.entries(uniqueConstraints)) { if (constraint.fields.length > 1) { const target = payload[name]; @@ -1151,7 +1102,7 @@ export class PolicyProxyHandler implements Pr // Validates the given update payload against Zod schema if any private validateUpdateInputSchema(model: string, data: any) { - const schema = this.utils.getZodSchema(model, 'update'); + const schema = this.policyUtils.getZodSchema(model, 'update'); if (schema && data) { // update payload can contain non-literal fields, like: // { x: { increment: 1 } } @@ -1164,7 +1115,7 @@ export class PolicyProxyHandler implements Pr const parseResult = schema.safeParse(literalData); if (!parseResult.success) { - throw this.utils.deniedByPolicy( + throw this.policyUtils.deniedByPolicy( model, 'update', `input failed validation: ${fromZodError(parseResult.error)}`, @@ -1180,133 +1131,136 @@ export class PolicyProxyHandler implements Pr } } - private isUnsafeMutate(model: string, args: any) { - if (!args) { - return false; - } - for (const k of Object.keys(args)) { - const field = resolveField(this.modelMeta, model, k); - if (this.isAutoIncrementIdField(field) || field?.isForeignKey) { - return true; - } - } - return false; - } - - private isAutoIncrementIdField(field: FieldInfo) { - return field.isId && field.isAutoIncrement; - } - - async updateMany(args: any) { + updateMany(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'data field is required in query argument' + ); } - this.utils.tryReject(this.prisma, this.model, 'update'); + return createDeferredPromise(() => { + this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.utils.clone(args); - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); + args = clone(args); + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); - args.data = this.validateUpdateInputSchema(this.model, args.data); + args.data = this.validateUpdateInputSchema(this.model, args.data); - if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) { - // use a transaction to do post-update checks - const postWriteChecks: PostWriteCheckRecord[] = []; - return this.transaction(async (tx) => { - // collect pre-update values - let select = this.utils.makeIdSelection(this.model); - const preValueSelect = this.utils.getPreValueSelect(this.model); - if (preValueSelect) { - select = { ...select, ...preValueSelect }; - } - const currentSetQuery = { select, where: args.where }; - this.utils.injectAuthGuardAsWhere(tx, currentSetQuery, this.model, 'read'); + if (this.policyUtils.hasAuthGuard(this.model, 'postUpdate') || this.policyUtils.getZodSchema(this.model)) { + // use a transaction to do post-update checks + const postWriteChecks: PostWriteCheckRecord[] = []; + return this.queryUtils.transaction(this.prisma, async (tx) => { + // collect pre-update values + let select = this.policyUtils.makeIdSelection(this.model); + const preValueSelect = this.policyUtils.getPreValueSelect(this.model); + if (preValueSelect) { + select = { ...select, ...preValueSelect }; + } + const currentSetQuery = { select, where: args.where }; + this.policyUtils.injectAuthGuardAsWhere(tx, currentSetQuery, this.model, 'read'); - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findMany\` ${this.model}: ${formatObject(currentSetQuery)}`); - } - const currentSet = await tx[this.model].findMany(currentSetQuery); - - postWriteChecks.push( - ...currentSet.map((preValue) => ({ - model: this.model, - operation: 'postUpdate' as PolicyOperationKind, - uniqueFilter: this.utils.getEntityIds(this.model, preValue), - preValue: preValueSelect ? preValue : undefined, - })) - ); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`findMany\` ${this.model}: ${formatObject(currentSetQuery)}`); + } + const currentSet = await tx[this.model].findMany(currentSetQuery); - // proceed with the update - const result = await tx[this.model].updateMany(args); + postWriteChecks.push( + ...currentSet.map((preValue) => ({ + model: this.model, + operation: 'postUpdate' as PolicyOperationKind, + uniqueFilter: this.policyUtils.getEntityIds(this.model, preValue), + preValue: preValueSelect ? preValue : undefined, + })) + ); - // run post-write checks - await this.runPostWriteChecks(postWriteChecks, tx); + // proceed with the update + const result = await tx[this.model].updateMany(args); - return result; - }); - } else { - // proceed without a transaction - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`updateMany\` ${this.model}: ${formatObject(args)}`); + // run post-write checks + await this.runPostWriteChecks(postWriteChecks, tx); + + return result; + }); + } else { + // proceed without a transaction + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`updateMany\` ${this.model}: ${formatObject(args)}`); + } + return this.modelClient.updateMany(args); } - return this.modelClient.updateMany(args); - } + }); } - async upsert(args: any) { + upsert(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'where field is required in query argument' + ); } if (!args.create) { - throw prismaClientValidationError(this.prisma, this.options, 'create field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'create field is required in query argument' + ); } if (!args.update) { - throw prismaClientValidationError(this.prisma, this.options, 'update field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'update field is required in query argument' + ); } - this.utils.tryReject(this.prisma, this.model, 'create'); - this.utils.tryReject(this.prisma, this.model, 'update'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'create'); + this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.utils.clone(args); + args = clone(args); - // We can call the native "upsert" because we can't tell if an entity was created or updated - // for doing post-write check accordingly. Instead, decompose it into create or update. + // We can call the native "upsert" because we can't tell if an entity was created or updated + // for doing post-write check accordingly. Instead, decompose it into create or update. - const { result, error } = await this.transaction(async (tx) => { - const { where, create, update, ...rest } = args; - const existing = await this.utils.checkExistence(tx, this.model, where); + const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { + const { where, create, update, ...rest } = args; + const existing = await this.policyUtils.checkExistence(tx, this.model, where); - if (existing) { - // update case - const { result, postWriteChecks } = await this.doUpdate( - { - where: this.utils.composeCompoundUniqueField(this.model, existing), - data: update, - ...rest, - }, - tx - ); - await this.runPostWriteChecks(postWriteChecks, tx); - return this.utils.readBack(tx, this.model, 'update', args, result); + if (existing) { + // update case + const { result, postWriteChecks } = await this.doUpdate( + { + where: this.policyUtils.composeCompoundUniqueField(this.model, existing), + data: update, + ...rest, + }, + tx + ); + await this.runPostWriteChecks(postWriteChecks, tx); + return this.policyUtils.readBack(tx, this.model, 'update', args, result); + } else { + // create case + const { result, postWriteChecks } = await this.doCreate(this.model, { data: create, ...rest }, tx); + await this.runPostWriteChecks(postWriteChecks, tx); + return this.policyUtils.readBack(tx, this.model, 'create', args, result); + } + }); + + if (error) { + throw error; } else { - // create case - const { result, postWriteChecks } = await this.doCreate(this.model, { data: create, ...rest }, tx); - await this.runPostWriteChecks(postWriteChecks, tx); - return this.utils.readBack(tx, this.model, 'create', args, result); + return result; } }); - - if (error) { - throw error; - } else { - return result; - } } //#endregion @@ -1316,152 +1270,168 @@ export class PolicyProxyHandler implements Pr // "delete" works against a single entity, and is rejected if the entity fails policy check. // "deleteMany" works against a set of entities, entities that fail policy check are filtered out. - async delete(args: any) { + delete(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); + throw prismaClientValidationError( + this.prisma, + this.prismaModule, + 'where field is required in query argument' + ); } - this.utils.tryReject(this.prisma, this.model, 'delete'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'delete'); - const { result, error } = await this.transaction(async (tx) => { - // do a read-back before delete - const r = await this.utils.readBack(tx, this.model, 'delete', args, args.where); - const error = r.error; - const read = r.result; + const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { + // do a read-back before delete + const r = await this.policyUtils.readBack(tx, this.model, 'delete', args, args.where); + const error = r.error; + const read = r.result; - // check existence - await this.utils.checkExistence(tx, this.model, args.where, true); + // check existence + await this.policyUtils.checkExistence(tx, this.model, args.where, true); - // inject delete guard - await this.utils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args); + // inject delete guard + await this.policyUtils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args); - // proceed with the deletion - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`delete\` ${this.model}:\n${formatObject(args)}`); - } - await tx[this.model].delete(args); + // proceed with the deletion + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`delete\` ${this.model}:\n${formatObject(args)}`); + } + await tx[this.model].delete(args); - return { result: read, error }; - }); + return { result: read, error }; + }); - if (error) { - throw error; - } else { - return result; - } + if (error) { + throw error; + } else { + return result; + } + }); } - async deleteMany(args: any) { - this.utils.tryReject(this.prisma, this.model, 'delete'); + deleteMany(args: any) { + return createDeferredPromise(() => { + this.policyUtils.tryReject(this.prisma, this.model, 'delete'); - // inject policy conditions - args = args ?? {}; - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete'); + // inject policy conditions + args = args ?? {}; + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete'); - // conduct the deletion - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`deleteMany\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.deleteMany(args); + // conduct the deletion + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`deleteMany\` ${this.model}:\n${formatObject(args)}`); + } + return this.modelClient.deleteMany(args); + }); } //#endregion //#region Aggregation - async aggregate(args: any) { + aggregate(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } - args = this.utils.clone(args); + return createDeferredPromise(() => { + args = clone(args); - // inject policy conditions - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + // inject policy conditions + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.aggregate(args); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${formatObject(args)}`); + } + return this.modelClient.aggregate(args); + }); } - async groupBy(args: any) { + groupBy(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } - args = this.utils.clone(args); + return createDeferredPromise(() => { + args = clone(args); - // inject policy conditions - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + // inject policy conditions + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.groupBy(args); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${formatObject(args)}`); + } + return this.modelClient.groupBy(args); + }); } - async count(args: any) { - // inject policy conditions - args = args ? this.utils.clone(args) : {}; - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + count(args: any) { + return createDeferredPromise(() => { + // inject policy conditions + args = args ? clone(args) : {}; + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`count\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.count(args); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`count\` ${this.model}:\n${formatObject(args)}`); + } + return this.modelClient.count(args); + }); } //#endregion //#region Subscribe (Prisma Pulse) - async subscribe(args: any) { - const readGuard = this.utils.getAuthGuard(this.prisma, this.model, 'read'); - if (this.utils.isTrue(readGuard)) { - // no need to inject - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`); + subscribe(args: any) { + return createDeferredPromise(() => { + const readGuard = this.policyUtils.getAuthGuard(this.prisma, this.model, 'read'); + if (this.policyUtils.isTrue(readGuard)) { + // no need to inject + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`); + } + return this.modelClient.subscribe(args); } - return this.modelClient.subscribe(args); - } - if (!args) { - // include all - args = { create: {}, update: {}, delete: {} }; - } else { - if (typeof args !== 'object') { - throw prismaClientValidationError(this.prisma, this.options, 'argument must be an object'); - } - if (Object.keys(args).length === 0) { + if (!args) { // include all args = { create: {}, update: {}, delete: {} }; } else { - args = this.utils.clone(args); + if (typeof args !== 'object') { + throw prismaClientValidationError(this.prisma, this.prismaModule, 'argument must be an object'); + } + if (Object.keys(args).length === 0) { + // include all + args = { create: {}, update: {}, delete: {} }; + } else { + args = clone(args); + } } - } - // inject into subscribe conditions + // inject into subscribe conditions - if (args.create) { - args.create.after = this.utils.and(args.create.after, readGuard); - } + if (args.create) { + args.create.after = this.policyUtils.and(args.create.after, readGuard); + } - if (args.update) { - args.update.after = this.utils.and(args.update.after, readGuard); - } + if (args.update) { + args.update.after = this.policyUtils.and(args.update.after, readGuard); + } - if (args.delete) { - args.delete.before = this.utils.and(args.delete.before, readGuard); - } + if (args.delete) { + args.delete.before = this.policyUtils.and(args.delete.before, readGuard); + } - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.subscribe(args); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`); + } + return this.modelClient.subscribe(args); + }); } //#endregion @@ -1472,45 +1442,14 @@ export class PolicyProxyHandler implements Pr return !!this.options?.logPrismaQuery && this.logger.enabled('info'); } - private transaction(action: (tx: Record) => Promise) { - if (this.prisma['$transaction']) { - const txOptions: any = { maxWait: this.DEFAULT_TX_MAXWAIT, timeout: this.DEFAULT_TX_TIMEOUT }; - if (this.options?.transactionMaxWait !== undefined) { - txOptions.maxWait = this.options.transactionMaxWait; - } - if (this.options?.transactionTimeout !== undefined) { - txOptions.timeout = this.options.transactionTimeout; - } - if (this.options?.transactionIsolationLevel !== undefined) { - txOptions.isolationLevel = this.options.transactionIsolationLevel; - } - return this.prisma.$transaction((tx) => action(tx), txOptions); - } else { - // already in transaction, don't nest - return action(this.prisma); - } - } - - private async runPostWriteChecks(postWriteChecks: PostWriteCheckRecord[], db: Record) { + private async runPostWriteChecks(postWriteChecks: PostWriteCheckRecord[], db: CrudContract) { await Promise.all( postWriteChecks.map(async ({ model, operation, uniqueFilter, preValue }) => - this.utils.checkPolicyForUnique(model, uniqueFilter, operation, db, undefined, preValue) + this.policyUtils.checkPolicyForUnique(model, uniqueFilter, operation, db, undefined, preValue) ) ); } - private makeHandler(model: string) { - return new PolicyProxyHandler( - this.prisma, - this.policy, - this.modelMeta, - this.zodSchemas, - model, - this.user, - this.options - ); - } - private requireBackLink(fieldInfo: FieldInfo) { invariant(fieldInfo.backLink, `back link not found for field ${fieldInfo.name}`); return requireField(this.modelMeta, fieldInfo.type, fieldInfo.backLink); diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index d4380d72b..c76812a51 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -1,78 +1,13 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import semver from 'semver'; -import { PRISMA_MINIMUM_VERSION } from '../../constants'; -import { getIdFields, type ModelMeta } from '../../cross'; -import { getDefaultModelMeta, getDefaultPolicy, getDefaultZodSchemas } from '../../loader'; -import { AuthUser, DbClientContract } from '../../types'; +import { getIdFields } from '../../cross'; +import { DbClientContract } from '../../types'; import { hasAllFields } from '../../validation'; -import { ErrorTransformer, makeProxy } from '../proxy'; -import type { CommonEnhancementOptions, PolicyDef, ZodSchemas } from '../types'; +import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; +import { Logger } from '../logger'; +import { makeProxy } from '../proxy'; import { PolicyProxyHandler } from './handler'; -/** - * Context for evaluating access policies - */ -export type WithPolicyContext = { - user?: AuthUser; -}; - -/** - * Transaction isolation levels: https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-isolation-level - */ -export type TransactionIsolationLevel = - | 'ReadUncommitted' - | 'ReadCommitted' - | 'RepeatableRead' - | 'Snapshot' - | 'Serializable'; - -/** - * Options for @see withPolicy - */ -export interface WithPolicyOptions extends CommonEnhancementOptions { - /** - * Policy definition - */ - policy?: PolicyDef; - - /** - * Model metadata - */ - modelMeta?: ModelMeta; - - /** - * Zod schemas for validation - */ - zodSchemas?: ZodSchemas; - - /** - * Whether to log Prisma query - */ - logPrismaQuery?: boolean; - - /** - * Hook for transforming errors before they are thrown to the caller. - */ - errorTransformer?: ErrorTransformer; - - /** - * The `maxWait` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. - */ - transactionMaxWait?: number; - - /** - * The `timeout` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. - */ - transactionTimeout?: number; - - /** - * The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. - */ - transactionIsolationLevel?: TransactionIsolationLevel; -} - /** * Gets an enhanced Prisma client with access policy check. * @@ -81,32 +16,19 @@ export interface WithPolicyOptions extends CommonEnhancementOptions { * @param policy The policy definition, will be loaded from default location if not provided * @param modelMeta The model metadata, will be loaded from default location if not provided * - * @deprecated Use {@link enhance} instead + * @private */ export function withPolicy( prisma: DbClient, - context?: WithPolicyContext, - options?: WithPolicyOptions + options: InternalEnhancementOptions, + context?: EnhancementContext ): DbClient { - if (!prisma) { - throw new Error('Invalid prisma instance'); - } - - const prismaVer = (prisma as any)._clientVersion; - if (prismaVer && semver.lt(prismaVer, PRISMA_MINIMUM_VERSION)) { - console.warn( - `ZenStack requires Prisma version "${PRISMA_MINIMUM_VERSION}" or higher. Detected version is "${prismaVer}".` - ); - } - - const _policy = options?.policy ?? getDefaultPolicy(options?.loadPath); - const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); - const _zodSchemas = options?.zodSchemas ?? getDefaultZodSchemas(options?.loadPath); + const { modelMeta, policy } = options; // validate user context const userContext = context?.user; - if (userContext && _modelMeta.authModel) { - const idFields = getIdFields(_modelMeta, _modelMeta.authModel); + if (userContext && modelMeta.authModel) { + const idFields = getIdFields(modelMeta, modelMeta.authModel); if ( !hasAllFields( context.user, @@ -119,11 +41,12 @@ export function withPolicy( } // validate user context for fields used in policy expressions - const authSelector = _policy.authSelector; + const authSelector = policy.authSelector; if (authSelector) { Object.keys(authSelector).forEach((f) => { if (!(f in userContext)) { - console.warn(`User context does not have field "${f}" used in policy rules`); + const logger = new Logger(prisma); + logger.warn(`User context does not have field "${f}" used in policy rules`); } }); } @@ -131,17 +54,8 @@ export function withPolicy( return makeProxy( prisma, - _modelMeta, - (_prisma, model) => - new PolicyProxyHandler( - _prisma as DbClientContract, - _policy, - _modelMeta, - _zodSchemas, - model, - context?.user, - options - ), + modelMeta, + (_prisma, model) => new PolicyProxyHandler(_prisma as DbClientContract, model, options, context), 'policy', options?.errorTransformer ); diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 4ff3044b8..bcb946877 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -5,7 +5,6 @@ import { lowerCaseFirst } from 'lower-case-first'; import { upperCaseFirst } from 'upper-case-first'; import { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; -import type { EnhancementOptions } from '..'; import { CrudFailureReason, FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX, @@ -17,46 +16,43 @@ import { PRE_UPDATE_VALUE_SELECTOR, PrismaErrorCode, } from '../../constants'; -import { - enumerate, - getFields, - getIdFields, - getModelFields, - resolveField, - zip, - type FieldInfo, - type ModelMeta, - type NestedWriteVisitorContext, -} from '../../cross'; -import { AuthUser, DbClientContract, DbOperations, PolicyOperationKind } from '../../types'; +import { enumerate, getFields, getModelFields, resolveField, zip, type FieldInfo, type ModelMeta } from '../../cross'; +import { AuthUser, CrudContract, DbClientContract, PolicyOperationKind } from '../../types'; import { getVersion } from '../../version'; +import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; +import { Logger } from '../logger'; +import { QueryUtils } from '../query-utils'; import type { InputCheckFunc, PolicyDef, ReadFieldCheckFunc, ZodSchemas } from '../types'; -import { - formatObject, - prismaClientKnownRequestError, - prismaClientUnknownRequestError, - prismaClientValidationError, -} from '../utils'; -import { Logger } from './logger'; +import { formatObject, prismaClientKnownRequestError } from '../utils'; /** * Access policy enforcement utilities */ -export class PolicyUtil { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore +export class PolicyUtil extends QueryUtils { private readonly logger: Logger; + private readonly modelMeta: ModelMeta; + private readonly policy: PolicyDef; + private readonly zodSchemas?: ZodSchemas; + private readonly prismaModule: any; + private readonly user?: AuthUser; constructor( private readonly db: DbClientContract, - private readonly options: EnhancementOptions | undefined, - private readonly modelMeta: ModelMeta, - private readonly policy: PolicyDef, - private readonly zodSchemas: ZodSchemas | undefined, - private readonly user?: AuthUser, + options: InternalEnhancementOptions, + context?: EnhancementContext, private readonly shouldLogQuery = false ) { + super(db, options); + this.logger = new Logger(db); + this.user = context?.user; + + ({ + modelMeta: this.modelMeta, + policy: this.policy, + zodSchemas: this.zodSchemas, + prismaModule: this.prismaModule, + } = options); } //#region Logical operators @@ -234,14 +230,33 @@ export class PolicyUtil { //# Auth guard + private readonly FULLY_OPEN_AUTH_GUARD = { + create: true, + read: true, + update: true, + delete: true, + postUpdate: true, + create_input: true, + update_input: true, + }; + + private getModelAuthGuard(model: string): PolicyDef['guard']['string'] { + if (this.options.kinds && !this.options.kinds.includes('policy')) { + // policy enhancement not enabled, return an fully open guard + return this.FULLY_OPEN_AUTH_GUARD; + } else { + return this.policy.guard[lowerCaseFirst(model)]; + } + } + /** * Gets pregenerated authorization guard object for a given model and operation. * * @returns true if operation is unconditionally allowed, false if unconditionally denied, * otherwise returns a guard object */ - getAuthGuard(db: Record, model: string, operation: PolicyOperationKind, preValue?: any) { - const guard = this.policy.guard[lowerCaseFirst(model)]; + getAuthGuard(db: CrudContract, model: string, operation: PolicyOperationKind, preValue?: any) { + const guard = this.getModelAuthGuard(model); if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); } @@ -261,7 +276,7 @@ export class PolicyUtil { /** * Get field-level read auth guard that overrides the model-level */ - getFieldOverrideReadAuthGuard(db: Record, model: string, field: string) { + getFieldOverrideReadAuthGuard(db: CrudContract, model: string, field: string) { const guard = this.requireGuard(model); const provider = guard[`${FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX}${field}`]; @@ -281,7 +296,7 @@ export class PolicyUtil { /** * Get field-level update auth guard */ - getFieldUpdateAuthGuard(db: Record, model: string, field: string) { + getFieldUpdateAuthGuard(db: CrudContract, model: string, field: string) { const guard = this.requireGuard(model); const provider = guard[`${FIELD_LEVEL_UPDATE_GUARD_PREFIX}${field}`]; @@ -301,7 +316,7 @@ export class PolicyUtil { /** * Get field-level update auth guard that overrides the model-level */ - getFieldOverrideUpdateAuthGuard(db: Record, model: string, field: string) { + getFieldOverrideUpdateAuthGuard(db: CrudContract, model: string, field: string) { const guard = this.requireGuard(model); const provider = guard[`${FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX}${field}`]; @@ -322,7 +337,7 @@ export class PolicyUtil { * Checks if the given model has a policy guard for the given operation. */ hasAuthGuard(model: string, operation: PolicyOperationKind) { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { return false; } @@ -351,7 +366,7 @@ export class PolicyUtil { * @returns boolean if static analysis is enough to determine the result, undefined if not */ checkInputGuard(model: string, args: any, operation: 'create'): boolean | undefined { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { return undefined; } @@ -372,7 +387,7 @@ export class PolicyUtil { /** * Injects model auth guard as where clause. */ - injectAuthGuardAsWhere(db: Record, args: any, model: string, operation: PolicyOperationKind) { + injectAuthGuardAsWhere(db: CrudContract, args: any, model: string, operation: PolicyOperationKind) { let guard = this.getAuthGuard(db, model, operation); if (operation === 'update' && args) { @@ -420,7 +435,7 @@ export class PolicyUtil { } private injectGuardForRelationFields( - db: Record, + db: CrudContract, model: string, payload: any, operation: PolicyOperationKind @@ -444,7 +459,7 @@ export class PolicyUtil { } private injectGuardForToManyField( - db: Record, + db: CrudContract, fieldInfo: FieldInfo, payload: { some?: any; every?: any; none?: any }, operation: PolicyOperationKind @@ -478,7 +493,7 @@ export class PolicyUtil { } private injectGuardForToOneField( - db: Record, + db: CrudContract, fieldInfo: FieldInfo, payload: { is?: any; isNot?: any } & Record, operation: PolicyOperationKind @@ -508,7 +523,7 @@ export class PolicyUtil { /** * Injects auth guard for read operations. */ - injectForRead(db: Record, model: string, args: any) { + injectForRead(db: CrudContract, model: string, args: any) { // make select and include visible to the injection const injected: any = { select: args.select, include: args.include }; if (!this.injectAuthGuardAsWhere(db, injected, model, 'read')) { @@ -546,141 +561,14 @@ export class PolicyUtil { return true; } - // flatten unique constraint filters - private flattenGeneratedUniqueField(model: string, args: any) { - // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } - const uniqueConstraints = this.modelMeta.uniqueConstraints?.[lowerCaseFirst(model)]; - if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) { - for (const [field, value] of Object.entries(args)) { - if ( - uniqueConstraints[field] && - uniqueConstraints[field].fields.length > 1 && - typeof value === 'object' - ) { - // multi-field unique constraint, flatten it - delete args[field]; - if (value) { - for (const [f, v] of Object.entries(value)) { - args[f] = v; - } - } - } - } - } - } - - composeCompoundUniqueField(model: string, fieldData: any) { - const uniqueConstraints = this.modelMeta.uniqueConstraints?.[lowerCaseFirst(model)]; - if (!uniqueConstraints) { - return fieldData; - } - - // e.g.: { a: '1', b: '1' } => { a_b: { a: '1', b: '1' } } - const result: any = this.clone(fieldData); - for (const [name, constraint] of Object.entries(uniqueConstraints)) { - if (constraint.fields.length > 1 && constraint.fields.every((f) => fieldData[f] !== undefined)) { - // multi-field unique constraint, compose it - result[name] = constraint.fields.reduce( - (prev, field) => ({ ...prev, [field]: fieldData[field] }), - {} - ); - constraint.fields.forEach((f) => delete result[f]); - } - } - return result; - } - /** * Gets unique constraints for the given model. */ getUniqueConstraints(model: string) { - return this.modelMeta.uniqueConstraints?.[lowerCaseFirst(model)] ?? {}; - } - - /** - * Builds a reversed query for the given nested path. - */ - buildReversedQuery(context: NestedWriteVisitorContext, forMutationPayload = false, unsafeOperation = false) { - let result, currQuery: any; - let currField: FieldInfo | undefined; - - for (let i = context.nestingPath.length - 1; i >= 0; i--) { - const { field, model, where } = context.nestingPath[i]; - - // never modify the original where because it's shared in the structure - const visitWhere = { ...where }; - if (model && where) { - // make sure composite unique condition is flattened - this.flattenGeneratedUniqueField(model, visitWhere); - } - - if (!result) { - // first segment (bottom), just use its where clause - result = currQuery = { ...visitWhere }; - currField = field; - } else { - if (!currField) { - throw this.unknownError(`missing field in nested path`); - } - if (!currField.backLink) { - throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`); - } - - const backLinkField = this.getModelField(currField.type, currField.backLink); - if (!backLinkField) { - throw this.unknownError(`missing backLink field ${currField.backLink} in ${currField.type}`); - } - - if (backLinkField.isArray && !forMutationPayload) { - // many-side of relationship, wrap with "some" query - currQuery[currField.backLink] = { some: { ...visitWhere } }; - currQuery = currQuery[currField.backLink].some; - } else { - const fkMapping = where && backLinkField.isRelationOwner && backLinkField.foreignKeyMapping; - - // calculate if we should preserve the relation condition (e.g., { user: { id: 1 } }) - const shouldPreserveRelationCondition = - // doing a mutation - forMutationPayload && - // and it's a safe mutate - !unsafeOperation && - // and the current segment is the direct parent (the last one is the mutate itself), - // the relation condition should be preserved and will be converted to a "connect" later - i === context.nestingPath.length - 2; - - if (fkMapping && !shouldPreserveRelationCondition) { - // turn relation condition into foreign key condition, e.g.: - // { user: { id: 1 } } => { userId: 1 } - for (const [r, fk] of Object.entries(fkMapping)) { - currQuery[fk] = visitWhere[r]; - } - - if (i > 0) { - // prepare for the next segment - currQuery[currField.backLink] = {}; - } - } else { - // preserve the original structure - currQuery[currField.backLink] = { ...visitWhere }; - } - - if (forMutationPayload && currQuery[currField.backLink]) { - // reconstruct compound unique field - currQuery[currField.backLink] = this.composeCompoundUniqueField( - backLinkField.type, - currQuery[currField.backLink] - ); - } - - currQuery = currQuery[currField.backLink]; - } - currField = field; - } - } - return result; + return this.modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {}; } - private injectNestedReadConditions(db: Record, model: string, args: any): any[] { + private injectNestedReadConditions(db: CrudContract, model: string, args: any): any[] { const injectTarget = args.select ?? args.include; if (!injectTarget) { return []; @@ -773,7 +661,7 @@ export class PolicyUtil { model: string, uniqueFilter: any, operation: PolicyOperationKind, - db: Record, + db: CrudContract, args: any, preValue?: any ) { @@ -782,7 +670,7 @@ export class PolicyUtil { throw this.deniedByPolicy( model, operation, - `entity ${formatObject(uniqueFilter)} failed policy check`, + `entity ${formatObject(uniqueFilter, false)} failed policy check`, CrudFailureReason.ACCESS_POLICY_VIOLATION ); } @@ -795,7 +683,7 @@ export class PolicyUtil { throw this.deniedByPolicy( model, 'update', - `entity ${formatObject(uniqueFilter)} failed update policy check for field "${ + `entity ${formatObject(uniqueFilter, false)} failed update policy check for field "${ fieldUpdateGuard.rejectedByField }"`, CrudFailureReason.ACCESS_POLICY_VIOLATION @@ -843,7 +731,7 @@ export class PolicyUtil { throw this.deniedByPolicy( model, operation, - `entity ${formatObject(uniqueFilter)} failed policy check`, + `entity ${formatObject(uniqueFilter, false)} failed policy check`, CrudFailureReason.ACCESS_POLICY_VIOLATION ); } @@ -859,7 +747,7 @@ export class PolicyUtil { throw this.deniedByPolicy( model, operation, - `entities ${JSON.stringify(uniqueFilter)} failed validation: [${error}]`, + `entities ${formatObject(uniqueFilter, false)} failed validation: [${error}]`, CrudFailureReason.DATA_VALIDATION_VIOLATION, parseResult.error ); @@ -867,7 +755,7 @@ export class PolicyUtil { } } - private getFieldReadGuards(db: Record, model: string, args: { select?: any; include?: any }) { + private getFieldReadGuards(db: CrudContract, model: string, args: { select?: any; include?: any }) { const allFields = Object.values(getFields(this.modelMeta, model)); // all scalar fields by default @@ -890,7 +778,7 @@ export class PolicyUtil { return this.and(...allFieldGuards); } - private getFieldUpdateGuards(db: Record, model: string, args: any) { + private getFieldUpdateGuards(db: CrudContract, model: string, args: any) { const allFieldGuards = []; const allOverrideFieldGuards = []; @@ -949,7 +837,7 @@ export class PolicyUtil { /** * Tries rejecting a request based on static "false" policy. */ - tryReject(db: Record, model: string, operation: PolicyOperationKind) { + tryReject(db: CrudContract, model: string, operation: PolicyOperationKind) { const guard = this.getAuthGuard(db, model, operation); if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); @@ -959,12 +847,7 @@ export class PolicyUtil { /** * Checks if a model exists given a unique filter. */ - async checkExistence( - db: Record, - model: string, - uniqueFilter: any, - throwIfNotFound = false - ): Promise { + async checkExistence(db: CrudContract, model: string, uniqueFilter: any, throwIfNotFound = false): Promise { uniqueFilter = this.clone(uniqueFilter); this.flattenGeneratedUniqueField(model, uniqueFilter); @@ -985,7 +868,7 @@ export class PolicyUtil { * Returns an entity given a unique filter with read policy checked. Reject if not readable. */ async readBack( - db: Record, + db: CrudContract, model: string, operation: PolicyOperationKind, selectInclude: { select?: any; include?: any }, @@ -1033,16 +916,21 @@ export class PolicyUtil { * @returns */ injectReadCheckSelect(model: string, args: any) { - if (!this.hasFieldLevelPolicy(model)) { - return; + if (this.hasFieldLevelPolicy(model)) { + // recursively inject selection for fields needed for field-level read checks + const readFieldSelect = this.getReadFieldSelect(model); + if (readFieldSelect) { + this.doInjectReadCheckSelect(model, args, { select: readFieldSelect }); + } } - const readFieldSelect = this.getReadFieldSelect(model); - if (!readFieldSelect) { - return; + // recurse into relation fields + for (const [k, v] of Object.entries(args.select ?? args.include ?? {})) { + const field = resolveField(this.modelMeta, model, k); + if (field?.isDataModel && v && typeof v === 'object') { + this.injectReadCheckSelect(field.type, v); + } } - - this.doInjectReadCheckSelect(model, args, { select: readFieldSelect }); } private doInjectReadCheckSelect(model: string, args: any, input: any) { @@ -1096,7 +984,7 @@ export class PolicyUtil { } private makeAllScalarFieldSelect(model: string): any { - const fields = this.modelMeta.fields[lowerCaseFirst(model)]; + const fields = this.getModelFields(model); const result: any = {}; if (fields) { Object.entries(fields).forEach(([k, v]) => { @@ -1130,29 +1018,19 @@ export class PolicyUtil { return prismaClientKnownRequestError( this.db, - this.options, + this.prismaModule, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, args ); } notFound(model: string) { - return prismaClientKnownRequestError(this.db, this.options, `entity not found for model ${model}`, { + return prismaClientKnownRequestError(this.db, this.prismaModule, `entity not found for model ${model}`, { clientVersion: getVersion(), code: 'P2025', }); } - validationError(message: string) { - return prismaClientValidationError(this.db, this.options, message); - } - - unknownError(message: string) { - return prismaClientUnknownRequestError(this.db, this.options, message, { - clientVersion: getVersion(), - }); - } - //#endregion //#region Misc @@ -1161,7 +1039,7 @@ export class PolicyUtil { * Gets field selection for fetching pre-update entity values for the given model. */ getPreValueSelect(model: string): object | undefined { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); } @@ -1169,7 +1047,7 @@ export class PolicyUtil { } private getReadFieldSelect(model: string): object | undefined { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); } @@ -1177,7 +1055,7 @@ export class PolicyUtil { } private checkReadField(model: string, field: string, entity: any) { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); } @@ -1194,7 +1072,7 @@ export class PolicyUtil { } private hasFieldLevelPolicy(model: string) { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); } @@ -1301,22 +1179,6 @@ export class PolicyUtil { } } - /** - * Gets information for all fields of a model. - */ - getModelFields(model: string) { - model = lowerCaseFirst(model); - return this.modelMeta.fields[model]; - } - - /** - * Gets information for a specific model field. - */ - getModelField(model: string, field: string) { - model = lowerCaseFirst(model); - return this.modelMeta.fields[model]?.[field]; - } - /** * Clones an object and makes sure it's not empty. */ @@ -1358,33 +1220,6 @@ export class PolicyUtil { }, {} as any); } - /** - * Gets "id" fields for a given model. - */ - getIdFields(model: string) { - return getIdFields(this.modelMeta, model, true); - } - - /** - * Gets id field values from an entity. - */ - getEntityIds(model: string, entityData: any) { - const idFields = this.getIdFields(model); - const result: Record = {}; - for (const idField of idFields) { - result[idField.name] = entityData[idField.name]; - } - return result; - } - - /** - * Creates a selection object for id fields for the given model. - */ - makeIdSelection(model: string) { - const idFields = this.getIdFields(model); - return Object.assign({}, ...idFields.map((f) => ({ [f.name]: true }))); - } - private mergeWhereClause(where: any, extra: any) { if (!where) { throw new Error('invalid where clause'); @@ -1412,12 +1247,23 @@ export class PolicyUtil { } private requireGuard(model: string) { - const guard = this.policy.guard[lowerCaseFirst(model)]; + const guard = this.getModelAuthGuard(model); if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); } return guard; } + /** + * Given an entity data, returns an object only containing id fields. + */ + getIdFieldValues(model: string, data: any) { + if (!data) { + return undefined; + } + const idFields = this.getIdFields(model); + return Object.fromEntries(idFields.map((f) => [f.name, data[f.name]])); + } + //#endregion } diff --git a/packages/runtime/src/enhancements/policy/promise.ts b/packages/runtime/src/enhancements/policy/promise.ts deleted file mode 100644 index b6d7baff9..000000000 --- a/packages/runtime/src/enhancements/policy/promise.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -/** - * Creates a promise that only executes when it's awaited or .then() is called. - * @see https://github.com/prisma/prisma/blob/main/packages/client/src/runtime/core/request/createPrismaPromise.ts - */ -export function createDeferredPromise(callback: () => Promise): Promise { - let promise: Promise | undefined; - const cb = () => { - try { - return (promise ??= valueToPromise(callback())); - } catch (err) { - // deal with synchronous errors - return Promise.reject(err); - } - }; - - return { - then(onFulfilled, onRejected) { - return cb().then(onFulfilled, onRejected); - }, - catch(onRejected) { - return cb().catch(onRejected); - }, - finally(onFinally) { - return cb().finally(onFinally); - }, - [Symbol.toStringTag]: 'ZenStackPromise', - }; -} - -function valueToPromise(thing: any): Promise { - if (typeof thing === 'object' && typeof thing?.then === 'function') { - return thing; - } else { - return Promise.resolve(thing); - } -} diff --git a/packages/runtime/src/enhancements/preset.ts b/packages/runtime/src/enhancements/preset.ts deleted file mode 100644 index 0123dbe64..000000000 --- a/packages/runtime/src/enhancements/preset.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EnhancementOptions, enhance } from './enhance'; -import { WithPolicyContext } from './policy'; - -/** - * Gets a Prisma client enhanced with all essential behaviors, including access - * policy, field validation, field omission and password hashing. - * - * It's a shortcut for calling withOmit(withPassword(withPolicy(prisma, options))). - * - * @param prisma The Prisma client to enhance. - * @param context The context to for evaluating access policies. - * @param options Options. - * - * @deprecated Use {@link enhance} instead - */ -export function withPresets( - prisma: DbClient, - context?: WithPolicyContext, - options?: EnhancementOptions -) { - return enhance(prisma, context, options); -} diff --git a/packages/runtime/src/enhancements/promise.ts b/packages/runtime/src/enhancements/promise.ts new file mode 100644 index 000000000..28a211146 --- /dev/null +++ b/packages/runtime/src/enhancements/promise.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { getModelInfo, type ModelMeta } from '../cross'; + +/** + * Creates a promise that only executes when it's awaited or .then() is called. + * @see https://github.com/prisma/prisma/blob/main/packages/client/src/runtime/core/request/createPrismaPromise.ts + */ +export function createDeferredPromise(callback: () => Promise): Promise { + let promise: Promise | undefined; + const cb = () => { + try { + return (promise ??= valueToPromise(callback())); + } catch (err) { + // deal with synchronous errors + return Promise.reject(err); + } + }; + + return { + then(onFulfilled, onRejected) { + return cb().then(onFulfilled, onRejected); + }, + catch(onRejected) { + return cb().catch(onRejected); + }, + finally(onFinally) { + return cb().finally(onFinally); + }, + [Symbol.toStringTag]: 'ZenStackPromise', + }; +} + +function valueToPromise(thing: any): Promise { + if (typeof thing === 'object' && typeof thing?.then === 'function') { + return thing; + } else { + return Promise.resolve(thing); + } +} + +/** + * Create a deferred promise with fluent API call stub installed. + * + * @param callback The callback to execute when the promise is awaited. + * @param parentArgs The parent promise's query args. + * @param modelMeta The model metadata. + * @param model The model name. + */ +export function createFluentPromise( + callback: () => Promise, + parentArgs: any, + modelMeta: ModelMeta, + model: string +): Promise { + const promise: any = createDeferredPromise(callback); + + const modelInfo = getModelInfo(modelMeta, model); + if (!modelInfo) { + return promise; + } + + // install fluent call stub for model fields + Object.values(modelInfo.fields) + .filter((field) => field.isDataModel) + .forEach((field) => { + // e.g., `posts` in `db.user.findUnique(...).posts()` + promise[field.name] = (fluentArgs: any) => { + if (field.isArray) { + // an array relation terminates fluent call chain + return createDeferredPromise(async () => { + setFluentSelect(parentArgs, field.name, fluentArgs ?? true); + const parentResult: any = await promise; + return parentResult?.[field.name] ?? null; + }); + } else { + fluentArgs = { ...fluentArgs }; + // create a chained subsequent fluent call promise + return createFluentPromise( + async () => { + setFluentSelect(parentArgs, field.name, fluentArgs); + const parentResult: any = await promise; + return parentResult?.[field.name] ?? null; + }, + fluentArgs, + modelMeta, + field.type + ); + } + }; + }); + + return promise; +} + +function setFluentSelect(args: any, fluentFieldName: any, fluentArgs: any) { + delete args.include; + args.select = { [fluentFieldName]: fluentArgs }; +} diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index c735d595a..e7f55a88c 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import deepcopy from 'deepcopy'; import { PRISMA_PROXY_ENHANCER } from '../constants'; import type { ModelMeta } from '../cross'; import type { DbClientContract } from '../types'; -import { createDeferredPromise } from './policy/promise'; +import type { InternalEnhancementOptions } from './create-enhancement'; +import { createDeferredPromise, createFluentPromise } from './promise'; /** * Prisma batch write operation result @@ -31,7 +33,7 @@ export interface PrismaProxyHandler { create(args: any): Promise; - createMany(args: any, skipDuplicates?: boolean): Promise; + createMany(args: { data: any; skipDuplicates?: boolean }): Promise; update(args: any): Promise; @@ -63,95 +65,97 @@ export type PrismaProxyActions = keyof PrismaProxyHandler; * methods to allow more easily inject custom logic. */ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { - constructor(protected readonly prisma: DbClientContract, protected readonly model: string) {} + constructor( + protected readonly prisma: DbClientContract, + protected readonly model: string, + protected readonly options: InternalEnhancementOptions + ) {} + + protected withFluentCall(method: keyof PrismaProxyHandler, args: any, postProcess = true): Promise { + args = args ? deepcopy(args) : {}; + const promise = createFluentPromise( + async () => { + args = await this.preprocessArgs(method, args); + const r = await this.prisma[this.model][method](args); + return postProcess ? this.processResultEntity(r) : r; + }, + args, + this.options.modelMeta, + this.model + ); + return promise; + } + + protected deferred(method: keyof PrismaProxyHandler, args: any, postProcess = true) { + return createDeferredPromise(async () => { + args = await this.preprocessArgs(method, args); + const r = await this.prisma[this.model][method](args); + return postProcess ? this.processResultEntity(r) : r; + }); + } - async findUnique(args: any): Promise { - args = await this.preprocessArgs('findUnique', args); - const r = await this.prisma[this.model].findUnique(args); - return this.processResultEntity(r); + findUnique(args: any) { + return this.withFluentCall('findUnique', args); } - async findUniqueOrThrow(args: any): Promise { - args = await this.preprocessArgs('findUniqueOrThrow', args); - const r = await this.prisma[this.model].findUniqueOrThrow(args); - return this.processResultEntity(r); + findUniqueOrThrow(args: any) { + return this.withFluentCall('findUniqueOrThrow', args); } - async findFirst(args: any): Promise { - args = await this.preprocessArgs('findFirst', args); - const r = await this.prisma[this.model].findFirst(args); - return this.processResultEntity(r); + findFirst(args: any) { + return this.withFluentCall('findFirst', args); } - async findFirstOrThrow(args: any): Promise { - args = await this.preprocessArgs('findFirstOrThrow', args); - const r = await this.prisma[this.model].findFirstOrThrow(args); - return this.processResultEntity(r); + findFirstOrThrow(args: any) { + return this.withFluentCall('findFirstOrThrow', args); } - async findMany(args: any): Promise { - args = await this.preprocessArgs('findMany', args); - const r = await this.prisma[this.model].findMany(args); - return this.processResultEntity(r); + findMany(args: any) { + return this.deferred('findMany', args); } - async create(args: any): Promise { - args = await this.preprocessArgs('create', args); - const r = await this.prisma[this.model].create(args); - return this.processResultEntity(r); + create(args: any): Promise { + return this.deferred('create', args); } - async createMany(args: any, skipDuplicates?: boolean | undefined): Promise<{ count: number }> { - args = await this.preprocessArgs('createMany', args); - return this.prisma[this.model].createMany(args, skipDuplicates); + createMany(args: { data: any; skipDuplicates?: boolean }) { + return this.deferred<{ count: number }>('createMany', args, false); } - async update(args: any): Promise { - args = await this.preprocessArgs('update', args); - const r = await this.prisma[this.model].update(args); - return this.processResultEntity(r); + update(args: any) { + return this.deferred('update', args); } - async updateMany(args: any): Promise<{ count: number }> { - args = await this.preprocessArgs('updateMany', args); - return this.prisma[this.model].updateMany(args); + updateMany(args: any) { + return this.deferred<{ count: number }>('updateMany', args, false); } - async upsert(args: any): Promise { - args = await this.preprocessArgs('upsert', args); - const r = await this.prisma[this.model].upsert(args); - return this.processResultEntity(r); + upsert(args: any) { + return this.deferred('upsert', args); } - async delete(args: any): Promise { - args = await this.preprocessArgs('delete', args); - const r = await this.prisma[this.model].delete(args); - return this.processResultEntity(r); + delete(args: any) { + return this.deferred('delete', args); } - async deleteMany(args: any): Promise<{ count: number }> { - args = await this.preprocessArgs('deleteMany', args); - return this.prisma[this.model].deleteMany(args); + deleteMany(args: any) { + return this.deferred<{ count: number }>('deleteMany', args, false); } - async aggregate(args: any): Promise { - args = await this.preprocessArgs('aggregate', args); - return this.prisma[this.model].aggregate(args); + aggregate(args: any) { + return this.deferred('aggregate', args, false); } - async groupBy(args: any): Promise { - args = await this.preprocessArgs('groupBy', args); - return this.prisma[this.model].groupBy(args); + groupBy(args: any) { + return this.deferred('groupBy', args, false); } - async count(args: any): Promise { - args = await this.preprocessArgs('count', args); - return this.prisma[this.model].count(args); + count(args: any): Promise { + return this.deferred('count', args, false); } - async subscribe(args: any): Promise { - args = await this.preprocessArgs('subscribe', args); - return this.prisma[this.model].subscribe(args); + subscribe(args: any) { + return this.deferred('subscribe', args, false); } /** @@ -172,6 +176,8 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { // a marker for filtering error stack trace const ERROR_MARKER = '__error_marker__'; +const customInspect = Symbol.for('nodejs.util.inspect.custom'); + /** * Makes a Prisma client proxy. */ @@ -182,7 +188,7 @@ export function makeProxy( name = 'unnamed_enhancer', errorTransformer?: ErrorTransformer ) { - const models = Object.keys(modelMeta.fields).map((k) => k.toLowerCase()); + const models = Object.keys(modelMeta.models).map((k) => k.toLowerCase()); const proxy = new Proxy(prisma, { get: (target: any, prop: string | symbol, receiver: any) => { @@ -191,10 +197,6 @@ export function makeProxy( return name; } - if (prop === 'toString') { - return () => `$zenstack_prisma_${prisma._clientVersion}`; - } - if (prop === '$transaction') { // for interactive transactions, we need to proxy the transaction function so that // when it runs the callback, it provides a proxy to the Prisma client wrapped with @@ -236,10 +238,12 @@ export function makeProxy( return propVal; } - return createHandlerProxy(makeHandler(target, prop), propVal, errorTransformer); + return createHandlerProxy(makeHandler(target, prop), propVal, prop, errorTransformer); }, }); + proxy[customInspect] = `$zenstack_prisma_${prisma._clientVersion}`; + return proxy; } @@ -247,6 +251,7 @@ export function makeProxy( function createHandlerProxy( handler: T, origTarget: any, + model: string, errorTransformer?: ErrorTransformer ): T { return new Proxy(handler, { @@ -277,7 +282,7 @@ function createHandlerProxy( if (capture.stack && err instanceof Error) { // save the original stack and replace it with a clean one (err as any).internalStack = err.stack; - err.stack = cleanCallStack(capture.stack, propKey.toString(), err.message); + err.stack = cleanCallStack(capture.stack, model, propKey.toString(), err.message); } if (errorTransformer) { @@ -303,9 +308,9 @@ function createHandlerProxy( } // Filter out @zenstackhq/runtime stack (generated by proxy) from stack trace -function cleanCallStack(stack: string, method: string, message: string) { +function cleanCallStack(stack: string, model: string, method: string, message: string) { // message line - let resultStack = `Error calling enhanced Prisma method \`${method}\`: ${message}`; + let resultStack = `Error calling enhanced Prisma method \`${model}.${method}\`: ${message}`; const lines = stack.split('\n'); let foundMarker = false; diff --git a/packages/runtime/src/enhancements/query-utils.ts b/packages/runtime/src/enhancements/query-utils.ts new file mode 100644 index 000000000..81c8d1da9 --- /dev/null +++ b/packages/runtime/src/enhancements/query-utils.ts @@ -0,0 +1,209 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + getIdFields, + getModelInfo, + getUniqueConstraints, + resolveField, + type FieldInfo, + type NestedWriteVisitorContext, +} from '../cross'; +import type { CrudContract, DbClientContract } from '../types'; +import { getVersion } from '../version'; +import { InternalEnhancementOptions } from './create-enhancement'; +import { clone, prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; + +export class QueryUtils { + constructor(private readonly prisma: DbClientContract, protected readonly options: InternalEnhancementOptions) {} + + getIdFields(model: string) { + return getIdFields(this.options.modelMeta, model, true); + } + + makeIdSelection(model: string) { + const idFields = this.getIdFields(model); + return Object.assign({}, ...idFields.map((f) => ({ [f.name]: true }))); + } + + getEntityIds(model: string, entityData: any) { + const idFields = this.getIdFields(model); + const result: Record = {}; + for (const idField of idFields) { + result[idField.name] = entityData[idField.name]; + } + return result; + } + + /** + * Initiates a transaction. + */ + transaction(db: CrudContract, action: (tx: CrudContract) => Promise) { + const fullDb = db as DbClientContract; + if (fullDb['$transaction']) { + return fullDb.$transaction( + (tx) => { + (tx as any)[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient$tx'; + return action(tx); + }, + { + maxWait: this.options.transactionMaxWait, + timeout: this.options.transactionTimeout, + isolationLevel: this.options.transactionIsolationLevel, + } + ); + } else { + // already in transaction, don't nest + return action(db); + } + } + + /** + * Builds a reversed query for the given nested path. + */ + buildReversedQuery(context: NestedWriteVisitorContext, forMutationPayload = false, unsafeOperation = false) { + let result, currQuery: any; + let currField: FieldInfo | undefined; + + for (let i = context.nestingPath.length - 1; i >= 0; i--) { + const { field, model, where } = context.nestingPath[i]; + + // never modify the original where because it's shared in the structure + const visitWhere = { ...where }; + if (model && where) { + // make sure composite unique condition is flattened + this.flattenGeneratedUniqueField(model, visitWhere); + } + + if (!result) { + // first segment (bottom), just use its where clause + result = currQuery = { ...visitWhere }; + currField = field; + } else { + if (!currField) { + throw this.unknownError(`missing field in nested path`); + } + if (!currField.backLink) { + throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`); + } + + const backLinkField = this.getModelField(currField.type, currField.backLink); + if (!backLinkField) { + throw this.unknownError(`missing backLink field ${currField.backLink} in ${currField.type}`); + } + + if (backLinkField.isArray && !forMutationPayload) { + // many-side of relationship, wrap with "some" query + currQuery[currField.backLink] = { some: { ...visitWhere } }; + currQuery = currQuery[currField.backLink].some; + } else { + const fkMapping = where && backLinkField.isRelationOwner && backLinkField.foreignKeyMapping; + + // calculate if we should preserve the relation condition (e.g., { user: { id: 1 } }) + const shouldPreserveRelationCondition = + // doing a mutation + forMutationPayload && + // and it's a safe mutate + !unsafeOperation && + // and the current segment is the direct parent (the last one is the mutate itself), + // the relation condition should be preserved and will be converted to a "connect" later + i === context.nestingPath.length - 2; + + if (fkMapping && !shouldPreserveRelationCondition) { + // turn relation condition into foreign key condition, e.g.: + // { user: { id: 1 } } => { userId: 1 } + for (const [r, fk] of Object.entries(fkMapping)) { + currQuery[fk] = visitWhere[r]; + } + + if (i > 0) { + // prepare for the next segment + currQuery[currField.backLink] = {}; + } + } else { + // preserve the original structure + currQuery[currField.backLink] = { ...visitWhere }; + } + + if (forMutationPayload && currQuery[currField.backLink]) { + // reconstruct compound unique field + currQuery[currField.backLink] = this.composeCompoundUniqueField( + backLinkField.type, + currQuery[currField.backLink] + ); + } + + currQuery = currQuery[currField.backLink]; + } + currField = field; + } + } + return result; + } + + /** + * Composes a compound unique field from multiple fields. E.g.: { a: '1', b: '1' } => { a_b: { a: '1', b: '1' } }. + */ + composeCompoundUniqueField(model: string, fieldData: any) { + const uniqueConstraints = getUniqueConstraints(this.options.modelMeta, model); + if (!uniqueConstraints) { + return fieldData; + } + + const result: any = clone(fieldData); + for (const [name, constraint] of Object.entries(uniqueConstraints)) { + if (constraint.fields.length > 1 && constraint.fields.every((f) => fieldData[f] !== undefined)) { + // multi-field unique constraint, compose it + result[name] = constraint.fields.reduce( + (prev, field) => ({ ...prev, [field]: fieldData[field] }), + {} + ); + constraint.fields.forEach((f) => delete result[f]); + } + } + return result; + } + + /** + * Flattens a generated unique field. E.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }. + */ + flattenGeneratedUniqueField(model: string, args: any) { + const uniqueConstraints = getUniqueConstraints(this.options.modelMeta, model); + if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) { + for (const [field, value] of Object.entries(args)) { + if ( + uniqueConstraints[field] && + uniqueConstraints[field].fields.length > 1 && + typeof value === 'object' + ) { + // multi-field unique constraint, flatten it + delete args[field]; + if (value) { + for (const [f, v] of Object.entries(value)) { + args[f] = v; + } + } + } + } + } + } + + validationError(message: string) { + return prismaClientValidationError(this.prisma, this.options.prismaModule, message); + } + + unknownError(message: string) { + return prismaClientUnknownRequestError(this.prisma, this.options.prismaModule, message, { + clientVersion: getVersion(), + }); + } + + getModelFields(model: string) { + return getModelInfo(this.options.modelMeta, model)?.fields; + } + + /** + * Gets information for a specific model field. + */ + getModelField(model: string, field: string) { + return resolveField(this.options.modelMeta, model, field); + } +} diff --git a/packages/runtime/src/enhancements/types.ts b/packages/runtime/src/enhancements/types.ts index dec8f097e..9fecc375e 100644 --- a/packages/runtime/src/enhancements/types.ts +++ b/packages/runtime/src/enhancements/types.ts @@ -9,7 +9,7 @@ import { HAS_FIELD_LEVEL_POLICY_FLAG, PRE_UPDATE_VALUE_SELECTOR, } from '../constants'; -import type { DbOperations, PolicyOperationKind, QueryContext } from '../types'; +import type { CrudContract, PolicyOperationKind, QueryContext } from '../types'; /** * Common options for PrismaClient enhancements @@ -31,7 +31,7 @@ export interface CommonEnhancementOptions { /** * Function for getting policy guard with a given context */ -export type PolicyFunc = (context: QueryContext, db: Record) => object; +export type PolicyFunc = (context: QueryContext, db: CrudContract) => object; /** * Function for getting policy guard with a given context @@ -83,5 +83,5 @@ export type PolicyDef = { */ export type ZodSchemas = { models: Record; - input: Record>; + input?: Record>; }; diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 72b1e393d..5cd23610e 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -1,91 +1,50 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -import path from 'path'; -import * as util from 'util'; +import deepcopy from 'deepcopy'; +import safeJsonStringify from 'safe-json-stringify'; +import { resolveField, type FieldInfo, type ModelMeta } from '..'; import type { DbClientContract } from '../types'; -import type { EnhancementOptions } from './enhance'; /** * Formats an object for pretty printing. */ -export function formatObject(value: unknown) { - return util.formatWithOptions({ depth: 20 }, value); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function formatObject(value: any, multiLine = true) { + return multiLine ? safeJsonStringify(value, undefined, 2) : safeJsonStringify(value); } -let _PrismaClientValidationError: new (...args: unknown[]) => Error; -let _PrismaClientKnownRequestError: new (...args: unknown[]) => Error; -let _PrismaClientUnknownRequestError: new (...args: unknown[]) => Error; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function prismaClientValidationError(prisma: DbClientContract, prismaModule: any, message: string): Error { + throw new prismaModule.PrismaClientValidationError(message, { clientVersion: prisma._clientVersion }); +} -/* eslint-disable @typescript-eslint/no-explicit-any */ -function loadPrismaModule(prisma: any) { - // https://github.com/prisma/prisma/discussions/17832 - if (prisma._engineConfig?.datamodelPath) { - // try engine path first - const loadPath = path.dirname(prisma._engineConfig.datamodelPath); - try { - const _prisma = require(loadPath).Prisma; - if (typeof _prisma !== 'undefined') { - return _prisma; - } - } catch { - // noop - } - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function prismaClientKnownRequestError(prisma: DbClientContract, prismaModule: any, ...args: unknown[]): Error { + return new prismaModule.PrismaClientKnownRequestError(...args); +} - try { - // Prisma v4 - return require('@prisma/client/runtime'); - } catch { - try { - // Prisma v5 - return require('@prisma/client'); - } catch (err) { - if (process.env.ZENSTACK_TEST === '1') { - // running in test, try cwd - try { - return require(path.join(process.cwd(), 'node_modules/@prisma/client/runtime')); - } catch { - return require(path.join(process.cwd(), 'node_modules/@prisma/client')); - } - } else { - throw err; - } - } - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function prismaClientUnknownRequestError(prismaModule: any, ...args: unknown[]): Error { + throw new prismaModule.PrismaClientUnknownRequestError(...args); } -export function prismaClientValidationError( - prisma: DbClientContract, - options: EnhancementOptions | undefined, - message: string -) { - if (!_PrismaClientValidationError) { - const _prisma = options?.prismaModule ?? loadPrismaModule(prisma); - _PrismaClientValidationError = _prisma.PrismaClientValidationError; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isUnsafeMutate(model: string, args: any, modelMeta: ModelMeta) { + if (!args) { + return false; + } + for (const k of Object.keys(args)) { + const field = resolveField(modelMeta, model, k); + if (field && (isAutoIncrementIdField(field) || field.isForeignKey)) { + return true; + } } - throw new _PrismaClientValidationError(message, { clientVersion: prisma._clientVersion }); + return false; } -export function prismaClientKnownRequestError( - prisma: DbClientContract, - options: EnhancementOptions | undefined, - ...args: unknown[] -) { - if (!_PrismaClientKnownRequestError) { - const _prisma = options?.prismaModule ?? loadPrismaModule(prisma); - _PrismaClientKnownRequestError = _prisma.PrismaClientKnownRequestError; - } - return new _PrismaClientKnownRequestError(...args); +export function isAutoIncrementIdField(field: FieldInfo) { + return field.isId && field.isAutoIncrement; } -export function prismaClientUnknownRequestError( - prisma: DbClientContract, - options: EnhancementOptions | undefined, - ...args: unknown[] -) { - if (!_PrismaClientUnknownRequestError) { - const _prisma = options?.prismaModule ?? loadPrismaModule(prisma); - _PrismaClientUnknownRequestError = _prisma.PrismaClientUnknownRequestError; - } - throw new _PrismaClientUnknownRequestError(...args); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function clone(value: unknown): any { + return value ? deepcopy(value) : {}; } diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 57df37ee4..6a2609156 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,7 +1,7 @@ export * from './constants'; export * from './enhancements'; export * from './error'; -export * from './loader'; export * from './types'; export * from './validation'; export * from './version'; +export * from './enhance'; diff --git a/packages/runtime/src/loader.ts b/packages/runtime/src/loader.ts deleted file mode 100644 index 1c2eef7bd..000000000 --- a/packages/runtime/src/loader.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -import path from 'path'; -import { ModelMeta, PolicyDef, ZodSchemas } from './enhancements'; - -/** - * Load model metadata. - * - * @param loadPath The path to load model metadata from. If not provided, - * will use default load path. - */ -export function getDefaultModelMeta(loadPath: string | undefined): ModelMeta { - try { - if (loadPath) { - const toLoad = path.resolve(loadPath, 'model-meta'); - return require(toLoad).default; - } else { - return require('.zenstack/model-meta').default; - } - } catch { - if (process.env.ZENSTACK_TEST === '1' && !loadPath) { - try { - // special handling for running as tests, try resolving relative to CWD - return require(path.join(process.cwd(), 'node_modules', '.zenstack', 'model-meta')).default; - } catch { - throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.'); - } - } - throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.'); - } -} - -/** - * Load access policies. - * - * @param loadPath The path to load access policies from. If not provided, - * will use default load path. - */ -export function getDefaultPolicy(loadPath: string | undefined): PolicyDef { - try { - if (loadPath) { - const toLoad = path.resolve(loadPath, 'policy'); - return require(toLoad).default; - } else { - return require('.zenstack/policy').default; - } - } catch { - if (process.env.ZENSTACK_TEST === '1' && !loadPath) { - try { - // special handling for running as tests, try resolving relative to CWD - return require(path.join(process.cwd(), 'node_modules', '.zenstack', 'policy')).default; - } catch { - throw new Error( - 'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.' - ); - } - } - throw new Error( - 'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.' - ); - } -} - -/** - * Load zod schemas. - * - * @param loadPath The path to load zod schemas from. If not provided, - * will use default load path. - */ -export function getDefaultZodSchemas(loadPath: string | undefined): ZodSchemas | undefined { - try { - if (loadPath) { - const toLoad = path.resolve(loadPath, 'zod'); - return require(toLoad); - } else { - return require('.zenstack/zod'); - } - } catch { - if (process.env.ZENSTACK_TEST === '1' && !loadPath) { - try { - // special handling for running as tests, try resolving relative to CWD - return require(path.join(process.cwd(), 'node_modules', '.zenstack', 'zod')); - } catch { - return undefined; - } - } - return undefined; - } -} diff --git a/packages/runtime/src/package.json b/packages/runtime/src/package.json new file mode 120000 index 000000000..4e26811d4 --- /dev/null +++ b/packages/runtime/src/package.json @@ -0,0 +1 @@ +../package.json \ No newline at end of file diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index e143cacfa..4bcab85a1 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -56,6 +56,14 @@ export type QueryContext = { preValue?: any; }; -export type DbClientContract = Record & { - $transaction: (action: (tx: Record) => Promise, options?: unknown) => Promise; +/** + * Prisma contract for CRUD operations. + */ +export type CrudContract = Record; + +/** + * Prisma contract for database client. + */ +export type DbClientContract = CrudContract & { + $transaction: (action: (tx: CrudContract) => Promise, options?: unknown) => Promise; }; diff --git a/packages/runtime/src/version.ts b/packages/runtime/src/version.ts index 567ef7a71..b8e941547 100644 --- a/packages/runtime/src/version.ts +++ b/packages/runtime/src/version.ts @@ -1,42 +1,9 @@ -import path from 'path'; - -/* eslint-disable @typescript-eslint/no-var-requires */ -export function getVersion() { - try { - return require('./package.json').version; - } catch { - try { - // dev environment - return require('../package.json').version; - } catch { - return 'unknown'; - } - } -} +import * as pkgJson from './package.json'; /** - * Gets installed Prisma version by first checking "@prisma/client" and if not available, - * "prisma". + * Gets this package's version. + * @returns */ -export function getPrismaVersion(): string | undefined { - if (process.env.ZENSTACK_TEST === '1') { - // test environment - try { - return require(path.resolve('./node_modules/@prisma/client/package.json')).version; - } catch { - return undefined; - } - } - - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - return require('@prisma/client/package.json').version; - } catch { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - return require('prisma/package.json').version; - } catch { - return undefined; - } - } +export function getVersion() { + return pkgJson.version; } diff --git a/packages/schema/package.json b/packages/schema/package.json index a7f201540..62c3fc896 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.12.4", + "version": "2.0.0", "author": { "name": "ZenStack Team" }, @@ -26,7 +26,7 @@ "linkDirectory": true }, "engines": { - "vscode": "^1.56.0" + "vscode": "^1.63.0" }, "categories": [ "Programming Languages" @@ -55,7 +55,17 @@ "scopeName": "source.zmodel", "path": "./bundle/syntaxes/zmodel.tmLanguage.json" } - ] + ], + "configuration": { + "title": "ZenStack", + "properties": { + "zmodel.format.usePrismaStyle": { + "type": "boolean", + "default": true, + "description": "Use Prisma style indentation." + } + } + } }, "activationEvents": [ "onLanguage:zmodel" @@ -66,6 +76,7 @@ "main": "./bundle/extension.js", "scripts": { "vscode:publish": "vsce publish --no-dependencies", + "vscode:prerelease": "vsce publish --no-dependencies --pre-release", "vscode:prepublish": "pnpm bundle", "vscode:package": "pnpm bundle && vsce package --no-dependencies", "clean": "rimraf dist", @@ -79,7 +90,7 @@ }, "dependencies": { "@paralleldrive/cuid2": "^2.2.0", - "@prisma/generator-helper": "^5.0.0", + "@types/node": "^20.12.7", "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", "async-exit-hook": "^2.0.1", @@ -110,8 +121,11 @@ "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, + "peerDependencies": { + "prisma": "5.0.0 - 5.12.x" + }, "devDependencies": { - "@prisma/client": "^4.8.0", + "@prisma/client": "5.12.0", "@types/async-exit-hook": "^2.0.0", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", @@ -123,7 +137,7 @@ "@zenstackhq/runtime": "workspace:*", "dotenv": "^16.0.3", "esbuild": "^0.15.12", - "prisma": "^4.8.0", + "prisma": "5.12.0", "renamer": "^4.0.0", "tmp": "^0.2.1", "tsc-alias": "^1.7.0", diff --git a/packages/schema/src/cli/actions/format.ts b/packages/schema/src/cli/actions/format.ts index 67f2d8a14..c3d2a6bed 100644 --- a/packages/schema/src/cli/actions/format.ts +++ b/packages/schema/src/cli/actions/format.ts @@ -6,7 +6,12 @@ import ora from 'ora'; import { CliError } from '../cli-error'; import { formatDocument, getDefaultSchemaLocation } from '../cli-util'; -export async function format(_projectPath: string, options: { schema: string }) { +type Options = { + schema: string; + prismaStyle?: boolean; +}; + +export async function format(_projectPath: string, options: Options) { const version = getVersion(); console.log(colors.bold(`⌛ī¸ ZenStack CLI v${version}`)); @@ -18,7 +23,7 @@ export async function format(_projectPath: string, options: { schema: string }) const spinner = ora(`Formatting ${schemaFile}`).start(); try { - const formattedDoc = await formatDocument(schemaFile); + const formattedDoc = await formatDocument(schemaFile, options.prismaStyle); await writeFile(schemaFile, formattedDoc); spinner.succeed(); } catch (e) { diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index c8ede65a2..e9db60fb6 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -3,7 +3,7 @@ import { getDataModels, getLiteral, hasAttribute } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'fs'; import getLatestVersion from 'get-latest-version'; -import { AstNode, getDocument, LangiumDocument, LangiumDocuments, Mutable } from 'langium'; +import { getDocument, LangiumDocument, LangiumDocuments, linkContentToContainer } from 'langium'; import { NodeFileSystem } from 'langium/node'; import path from 'path'; import semver from 'semver'; @@ -56,7 +56,7 @@ export async function loadDocument(fileName: string): Promise { const importedDocuments = importedURIs.map((uri) => langiumDocuments.getOrCreateDocument(uri)); - // build the document together with standard library and plugin modules + // build the document together with standard library, plugin modules, and imported documents await services.shared.workspace.DocumentBuilder.build( [stdLib, ...pluginDocuments, document, ...importedDocuments], { @@ -64,34 +64,52 @@ export async function loadDocument(fileName: string): Promise { } ); - const validationErrors = langiumDocuments.all - .flatMap((d) => d.diagnostics ?? []) - .filter((e) => e.severity === 1) + const diagnostics = langiumDocuments.all + .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag }))) + .filter(({ diag }) => diag.severity === 1 || diag.severity === 2) .toArray(); - if (validationErrors.length > 0) { - console.error(colors.red('Validation errors:')); - for (const validationError of validationErrors) { - console.error( - colors.red( - `line ${validationError.range.start.line + 1}: ${ - validationError.message - } [${document.textDocument.getText(validationError.range)}]` - ) - ); + let hasErrors = false; + + if (diagnostics.length > 0) { + for (const { doc, diag } of diagnostics) { + const message = `${path.relative(process.cwd(), doc.uri.fsPath)}:${diag.range.start.line + 1}:${ + diag.range.start.character + 1 + } - ${diag.message}`; + + if (diag.severity === 1) { + console.error(colors.red(message)); + hasErrors = true; + } else { + console.warn(colors.yellow(message)); + } + } + + if (hasErrors) { + throw new CliError('Schema contains validation errors'); } - throw new CliError('schema validation errors'); } const model = document.parseResult.value as Model; - mergeImportsDeclarations(langiumDocuments, model); + // merge all declarations into the main document + const imported = mergeImportsDeclarations(langiumDocuments, model); + + // remove imported documents + await services.shared.workspace.DocumentBuilder.update( + [], + imported.map((m) => m.$document!.uri) + ); validationAfterMerge(model); - mergeBaseModel(model); + // merge fields and attributes from base models + mergeBaseModel(model, services.references.Linker); - return model; + // finally relink all references + const relinkedModel = await relinkAll(model, services); + + return relinkedModel; } // check global unique thing after merge imports @@ -142,15 +160,15 @@ export function mergeImportsDeclarations(documents: LangiumDocuments, model: Mod const importedModels = resolveTransitiveImports(documents, model); const importedDeclarations = importedModels.flatMap((m) => m.declarations); + model.declarations.push(...importedDeclarations); - importedDeclarations.forEach((d) => { - const mutable = d as Mutable; - // The plugin might use $container to access the model - // need to make sure it is always resolved to the main model - mutable.$container = model; - }); + // remove import directives + model.imports = []; - model.declarations.push(...importedDeclarations); + // fix $containerIndex + linkContentToContainer(model); + + return importedModels; } export async function getPluginDocuments(services: ZModelServices, fileName: string): Promise { @@ -255,7 +273,7 @@ export async function checkNewVersion() { } } -export async function formatDocument(fileName: string) { +export async function formatDocument(fileName: string, isPrismaStyle = true) { const services = createZModelServices(NodeFileSystem).ZModel; const extensions = services.LanguageMetaData.fileExtensions; if (!extensions.includes(path.extname(fileName))) { @@ -268,6 +286,8 @@ export async function formatDocument(fileName: string) { const formatter = services.lsp.Formatter as ZModelFormatter; + formatter.setPrismaStyle(isPrismaStyle); + const identifier = { uri: document.uri.toString() }; const options = formatter.getFormatOptions() ?? { insertSpaces: true, @@ -295,3 +315,22 @@ export function getDefaultSchemaLocation() { return path.resolve('schema.zmodel'); } + +async function relinkAll(model: Model, services: ZModelServices) { + const doc = model.$document!; + + // unlink the document + services.references.Linker.unlink(doc); + + // remove current document + await services.shared.workspace.DocumentBuilder.update([], [doc.uri]); + + // recreate and load the document + const newDoc = services.shared.workspace.LangiumDocumentFactory.fromModel(model, doc.uri); + services.shared.workspace.LangiumDocuments.addDocument(newDoc); + + // rebuild the document + await services.shared.workspace.DocumentBuilder.build([newDoc], { validationChecks: 'all' }); + + return newDoc.parseResult.value as Model; +} diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index d96ed1121..f430f7662 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -81,7 +81,6 @@ export function createProgram() { `schema file (with extension ${schemaExtensions}). Defaults to "schema.zmodel" unless specified in package.json.` ); - const configOption = new Option('-c, --config [file]', 'config file').hideHelp(); const pmOption = new Option('-p, --package-manager ', 'package manager to use').choices([ 'npm', 'yarn', @@ -99,7 +98,6 @@ export function createProgram() { program .command('init') .description('Initialize an existing project for ZenStack.') - .addOption(configOption) .addOption(pmOption) .addOption(new Option('--prisma ', 'location of Prisma schema file to bootstrap from')) .addOption(new Option('--tag ', 'the NPM package tag to use when installing dependencies')) @@ -111,10 +109,9 @@ export function createProgram() { .command('generate') .description('Run code generation.') .addOption(schemaOption) - .addOption(new Option('-o, --output ', 'default output directory for built-in plugins')) - .addOption(configOption) + .addOption(new Option('-o, --output ', 'default output directory for core plugins')) .addOption(new Option('--no-default-plugins', 'do not run default plugins')) - .addOption(new Option('--no-compile', 'do not compile the output of built-in plugins')) + .addOption(new Option('--no-compile', 'do not compile the output of core plugins')) .addOption(noVersionCheckOption) .addOption(noDependencyCheck) .action(generateAction); @@ -131,6 +128,7 @@ export function createProgram() { .command('format') .description('Format a ZenStack schema file.') .addOption(schemaOption) + .option('--no-prisma-style', 'do not use prisma style') .action(formatAction); // make sure config is loaded before actions run diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 0609fd4fb..7b725cc28 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -1,31 +1,35 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-var-requires */ -import type { DMMF } from '@prisma/generator-helper'; import { isPlugin, Model, Plugin } from '@zenstackhq/language/ast'; import { + createProject, + emitProject, getDataModels, - getDMMF, getLiteral, getLiteralArray, hasValidationAttributes, PluginError, - PluginFunction, - PluginOptions, resolvePath, + saveProject, + type OptionValue, + type PluginDeclaredOptions, + type PluginFunction, + type PluginResult, } from '@zenstackhq/sdk'; +import { type DMMF } from '@zenstackhq/sdk/prisma'; import colors from 'colors'; -import fs from 'fs'; import ora from 'ora'; import path from 'path'; -import { ensureDefaultOutputFolder } from '../plugins/plugin-utils'; -import { getDefaultPrismaOutputFile } from '../plugins/prisma/schema-generator'; +import type { Project } from 'ts-morph'; +import { CorePlugins, ensureDefaultOutputFolder } from '../plugins/plugin-utils'; import telemetry from '../telemetry'; import { getVersion } from '../utils/version-utils'; type PluginInfo = { name: string; + description?: string; provider: string; - options: PluginOptions; + options: PluginDeclaredOptions; run: PluginFunction; dependencies: string[]; module: any; @@ -46,16 +50,14 @@ export class PluginRunner { /** * Runs a series of nested generators */ - async run(options: PluginRunnerOptions): Promise { + async run(runnerOptions: PluginRunnerOptions): Promise { const version = getVersion(); console.log(colors.bold(`⌛ī¸ ZenStack CLI v${version}, running plugins`)); - ensureDefaultOutputFolder(options); + ensureDefaultOutputFolder(runnerOptions); const plugins: PluginInfo[] = []; - const pluginDecls = options.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - - let prismaOutput = getDefaultPrismaOutputFile(options.schemaPath); + const pluginDecls = runnerOptions.schema.declarations.filter((d): d is Plugin => isPlugin(d)); for (const pluginDecl of pluginDecls) { const pluginProvider = this.getPluginProvider(pluginDecl); @@ -68,7 +70,7 @@ export class PluginRunner { let pluginModule: any; try { - pluginModule = this.loadPluginModule(pluginProvider, options); + pluginModule = this.loadPluginModule(pluginProvider, runnerOptions.schemaPath); } catch (err) { console.error(`Unable to load plugin module ${pluginProvider}: ${err}`); throw new PluginError('', `Unable to load plugin module ${pluginProvider}`); @@ -80,62 +82,37 @@ export class PluginRunner { } const dependencies = this.getPluginDependencies(pluginModule); - const pluginName = this.getPluginName(pluginModule, pluginProvider); - const pluginOptions: PluginOptions = { schemaPath: options.schemaPath, name: pluginName }; + const pluginOptions: PluginDeclaredOptions = { + provider: pluginProvider, + }; pluginDecl.fields.forEach((f) => { const value = getLiteral(f.value) ?? getLiteralArray(f.value); if (value === undefined) { - throw new PluginError(pluginName, `Invalid option value for ${f.name}`); + throw new PluginError(pluginDecl.name, `Invalid option value for ${f.name}`); } pluginOptions[f.name] = value; }); plugins.push({ - name: pluginName, + name: pluginDecl.name, + description: this.getPluginDescription(pluginModule), provider: pluginProvider, dependencies, options: pluginOptions, run: pluginModule.default as PluginFunction, module: pluginModule, }); - - if (pluginProvider === '@core/prisma' && typeof pluginOptions.output === 'string') { - // record custom prisma output path - prismaOutput = resolvePath(pluginOptions.output, pluginOptions); - } } - // get core plugins that need to be enabled - const corePlugins = this.calculateCorePlugins(options, plugins); - - // shift/insert core plugins to the front - for (const corePlugin of corePlugins.reverse()) { - const existingIdx = plugins.findIndex((p) => p.provider === corePlugin.provider); - if (existingIdx >= 0) { - // shift the plugin to the front - const existing = plugins[existingIdx]; - plugins.splice(existingIdx, 1); - plugins.unshift(existing); - } else { - // synthesize a plugin and insert front - const pluginModule = require(this.getPluginModulePath(corePlugin.provider, options)); - const pluginName = this.getPluginName(pluginModule, corePlugin.provider); - plugins.unshift({ - name: pluginName, - provider: corePlugin.provider, - dependencies: [], - options: { schemaPath: options.schemaPath, name: pluginName, ...corePlugin.options }, - run: pluginModule.default, - module: pluginModule, - }); - } - } + // calculate all plugins (including core plugins implicitly enabled) + const { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, plugins); + const allPlugins = [...corePlugins, ...userPlugins]; // check dependencies - for (const plugin of plugins) { + for (const plugin of allPlugins) { for (const dep of plugin.dependencies) { - if (!plugins.find((p) => p.provider === dep)) { + if (!allPlugins.find((p) => p.provider === dep)) { console.error(`Plugin ${plugin.provider} depends on "${dep}" but it's not declared`); throw new PluginError( plugin.name, @@ -145,63 +122,98 @@ export class PluginRunner { } } - if (plugins.length === 0) { + if (allPlugins.length === 0) { console.log(colors.yellow('No plugins configured.')); return; } const warnings: string[] = []; + // run core plugins first let dmmf: DMMF.Document | undefined = undefined; - for (const { name, provider, run, options: pluginOptions } of plugins) { - // const start = Date.now(); - await this.runPlugin(name, run, options, pluginOptions, dmmf, warnings); - // console.log(`✅ Plugin ${colors.bold(name)} (${provider}) completed in ${Date.now() - start}ms`); - if (provider === '@core/prisma') { - // load prisma DMMF - dmmf = await getDMMF({ - datamodel: fs.readFileSync(prismaOutput, { encoding: 'utf-8' }), - }); + let prismaClientPath = '@prisma/client'; + const project = createProject(); + for (const { name, description, run, options: pluginOptions } of corePlugins) { + const options = { ...pluginOptions, prismaClientPath }; + const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, project); + warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility + + if (r.dmmf) { + // use the DMMF returned by the plugin + dmmf = r.dmmf; + } + + if (r.prismaClientPath) { + // use the prisma client path returned by the plugin + prismaClientPath = r.prismaClientPath; } } - console.log(colors.green(colors.bold('\nđŸ‘ģ All plugins completed successfully!'))); - warnings.forEach((w) => console.warn(colors.yellow(w))); + // compile code generated by core plugins + await compileProject(project, runnerOptions); + + // run user plugins + for (const { name, description, run, options: pluginOptions } of userPlugins) { + const options = { ...pluginOptions, prismaClientPath }; + const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, project); + warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility + } + console.log(colors.green(colors.bold('\nđŸ‘ģ All plugins completed successfully!'))); + warnings.forEach((w) => console.warn(colors.yellow(w))); console.log(`Don't forget to restart your dev server to let the changes take effect.`); } - private calculateCorePlugins(options: PluginRunnerOptions, plugins: PluginInfo[]) { - const corePlugins: Array<{ provider: string; options?: Record }> = []; + private calculateAllPlugins(options: PluginRunnerOptions, plugins: PluginInfo[]) { + const corePlugins: PluginInfo[] = []; + let zodImplicitlyAdded = false; - if (options.defaultPlugins) { - corePlugins.push( - { provider: '@core/prisma' }, - { provider: '@core/model-meta' }, - { provider: '@core/access-policy' } - ); - } else if (plugins.length > 0) { - // "@core/prisma" plugin is always enabled if any plugin is configured - corePlugins.push({ provider: '@core/prisma' }); + // 1. @core/prisma + const existingPrisma = plugins.find((p) => p.provider === CorePlugins.Prisma); + if (existingPrisma) { + corePlugins.push(existingPrisma); + plugins.splice(plugins.indexOf(existingPrisma), 1); + } else if (options.defaultPlugins || plugins.some((p) => p.provider !== CorePlugins.Prisma)) { + // "@core/prisma" is enabled as default or if any other plugin is configured + corePlugins.push(this.makeCorePlugin(CorePlugins.Prisma, options.schemaPath, {})); } - // "@core/access-policy" has implicit requirements - let zodImplicitlyAdded = false; - if ([...plugins, ...corePlugins].find((p) => p.provider === '@core/access-policy')) { - // make sure "@core/model-meta" is enabled - if (!corePlugins.find((p) => p.provider === '@core/model-meta')) { - corePlugins.push({ provider: '@core/model-meta' }); - } + const hasValidation = this.hasValidation(options.schema); - // '@core/zod' plugin is auto-enabled by "@core/access-policy" - // if there're validation rules - if (!corePlugins.find((p) => p.provider === '@core/zod') && this.hasValidation(options.schema)) { - zodImplicitlyAdded = true; - corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } }); + // 2. @core/enhancer + const existingEnhancer = plugins.find((p) => p.provider === CorePlugins.Enhancer); + if (existingEnhancer) { + corePlugins.push(existingEnhancer); + plugins.splice(plugins.indexOf(existingEnhancer), 1); + } else { + if (options.defaultPlugins) { + corePlugins.push( + this.makeCorePlugin(CorePlugins.Enhancer, options.schemaPath, { + withZodSchemas: hasValidation, + }) + ); } } - // core plugins introduced by dependencies + // 3. @core/zod + const existingZod = plugins.find((p) => p.provider === CorePlugins.Zod); + if (existingZod && !existingZod.options.output) { + // we can reuse the user-provided zod plugin if it didn't specify a custom output path + plugins.splice(plugins.indexOf(existingZod), 1); + corePlugins.push(existingZod); + } + + if ( + !corePlugins.some((p) => p.provider === CorePlugins.Zod) && + (options.defaultPlugins || corePlugins.some((p) => p.provider === CorePlugins.Enhancer)) && + hasValidation + ) { + // ensure "@core/zod" is enabled if "@core/enhancer" is enabled and there're validation rules + zodImplicitlyAdded = true; + corePlugins.push(this.makeCorePlugin(CorePlugins.Zod, options.schemaPath, { modelOnly: true })); + } + + // collect core plugins introduced by dependencies plugins.forEach((plugin) => { // TODO: generalize this const isTrpcPlugin = @@ -217,7 +229,9 @@ export class PluginRunner { if (existing.provider === '@core/zod') { // Zod plugin can be automatically enabled in `modelOnly` mode, however // other plugin (tRPC) for now requires it to run in full mode - existing.options = {}; + if (existing.options.modelOnly) { + delete existing.options.modelOnly; + } if ( isTrpcPlugin && @@ -229,21 +243,39 @@ export class PluginRunner { } } else { // add core dependency - const toAdd = { provider: dep, options: {} as Record }; + const depOptions: Record = {}; // TODO: generalize this if (dep === '@core/zod' && isTrpcPlugin) { // pass trpc plugin's `generateModels` option down to zod plugin - toAdd.options.generateModels = plugin.options.generateModels; + depOptions.generateModels = plugin.options.generateModels; } - corePlugins.push(toAdd); + corePlugins.push(this.makeCorePlugin(dep, options.schemaPath, depOptions)); } } } }); - return corePlugins; + return { corePlugins, userPlugins: plugins }; + } + + private makeCorePlugin( + provider: string, + schemaPath: string, + options: Record + ): PluginInfo { + const pluginModule = require(this.getPluginModulePath(provider, schemaPath)); + const pluginName = this.getPluginName(pluginModule, provider); + return { + name: pluginName, + description: this.getPluginDescription(pluginModule), + provider: provider, + dependencies: [], + options: { ...options, provider }, + run: pluginModule.default, + module: pluginModule, + }; } private hasValidation(schema: Model) { @@ -251,10 +283,15 @@ export class PluginRunner { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getPluginName(pluginModule: any, pluginProvider: string): string { + private getPluginName(pluginModule: any, pluginProvider: string) { return typeof pluginModule.name === 'string' ? (pluginModule.name as string) : pluginProvider; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getPluginDescription(pluginModule: any) { + return typeof pluginModule.description === 'string' ? (pluginModule.description as string) : undefined; + } + private getPluginDependencies(pluginModule: any) { return Array.isArray(pluginModule.dependencies) ? (pluginModule.dependencies as string[]) : []; } @@ -266,15 +303,17 @@ export class PluginRunner { private async runPlugin( name: string, + description: string | undefined, run: PluginFunction, runnerOptions: PluginRunnerOptions, - options: PluginOptions, + options: PluginDeclaredOptions, dmmf: DMMF.Document | undefined, - warnings: string[] + project: Project ) { - const spinner = ora(`Running plugin ${colors.cyan(name)}`).start(); + const title = description ?? `Running plugin ${colors.cyan(name)}`; + const spinner = ora(title).start(); try { - await telemetry.trackSpan( + const r = await telemetry.trackSpan( 'cli:plugin:start', 'cli:plugin:complete', 'cli:plugin:error', @@ -283,26 +322,27 @@ export class PluginRunner { options, }, async () => { - let result = run(runnerOptions.schema, options, dmmf, { + return await run(runnerOptions.schema, { ...options, schemaPath: runnerOptions.schemaPath }, dmmf, { output: runnerOptions.output, compile: runnerOptions.compile, + tsProject: project, }); - if (result instanceof Promise) { - result = await result; - } - if (Array.isArray(result)) { - warnings.push(...result); - } } ); spinner.succeed(); + + if (typeof r === 'object') { + return r; + } else { + return { warnings: [] }; + } } catch (err) { spinner.fail(); throw err; } } - private getPluginModulePath(provider: string, options: Pick) { + private getPluginModulePath(provider: string, schemaPath: string) { let pluginModulePath = provider; if (provider.startsWith('@core/')) { pluginModulePath = provider.replace(/^@core/, path.join(__dirname, '../plugins')); @@ -312,14 +352,24 @@ export class PluginRunner { require.resolve(pluginModulePath); } catch { // relative - pluginModulePath = resolvePath(provider, options); + pluginModulePath = resolvePath(provider, { schemaPath }); } } return pluginModulePath; } - private loadPluginModule(provider: string, options: Pick) { - const pluginModulePath = this.getPluginModulePath(provider, options); + private loadPluginModule(provider: string, schemaPath: string) { + const pluginModulePath = this.getPluginModulePath(provider, schemaPath); return require(pluginModulePath); } } + +async function compileProject(project: Project, runnerOptions: PluginRunnerOptions) { + if (runnerOptions.compile !== false) { + // emit + await emitProject(project); + } else { + // otherwise save ts files + await saveProject(project); + } +} diff --git a/packages/schema/src/extension.ts b/packages/schema/src/extension.ts index d28f7dd87..a3e19d7f8 100644 --- a/packages/schema/src/extension.ts +++ b/packages/schema/src/extension.ts @@ -56,6 +56,6 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { const client = new LanguageClient('zmodel', 'ZenStack Model', serverOptions, clientOptions); // Start the client. This will also launch the server - client.start(); + void client.start(); return client; } diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 09af0971c..0baf5ace3 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -7,8 +7,9 @@ import { ReferenceExpr, isEnum, } from '@zenstackhq/language/ast'; -import { getLiteral, getModelIdFields, getModelUniqueFields } from '@zenstackhq/sdk'; +import { getLiteral, getModelIdFields, getModelUniqueFields, isDelegateModel } from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium'; +import { getModelFieldsWithBases } from '../../utils/ast-utils'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; import { getUniqueFields } from '../utils'; @@ -21,16 +22,16 @@ import { validateDuplicatedDeclarations } from './utils'; export default class DataModelValidator implements AstValidator { validate(dm: DataModel, accept: ValidationAcceptor): void { this.validateBaseAbstractModel(dm, accept); - validateDuplicatedDeclarations(dm.$resolvedFields, accept); + this.validateBaseDelegateModel(dm, accept); + validateDuplicatedDeclarations(dm, getModelFieldsWithBases(dm), accept); this.validateAttributes(dm, accept); this.validateFields(dm, accept); } private validateFields(dm: DataModel, accept: ValidationAcceptor) { - const idFields = dm.$resolvedFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@id')); - const uniqueFields = dm.$resolvedFields.filter((f) => - f.attributes.find((attr) => attr.decl.ref?.name === '@unique') - ); + const allFields = getModelFieldsWithBases(dm); + const idFields = allFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@id')); + const uniqueFields = allFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@unique')); const modelLevelIds = getModelIdFields(dm); const modelUniqueFields = getModelUniqueFields(dm); @@ -76,10 +77,10 @@ export default class DataModelValidator implements AstValidator { dm.fields.forEach((field) => this.validateField(field, accept)); if (!dm.isAbstract) { - dm.$resolvedFields + allFields .filter((x) => isDataModel(x.type.reference?.ref)) .forEach((y) => { - this.validateRelationField(y, accept); + this.validateRelationField(dm, y, accept); }); } } @@ -207,7 +208,7 @@ export default class DataModelValidator implements AstValidator { // points back const oppositeModel = field.type.reference?.ref as DataModel; if (oppositeModel) { - const oppositeModelFields = oppositeModel.$resolvedFields as DataModelField[]; + const oppositeModelFields = getModelFieldsWithBases(oppositeModel); for (const oppositeField of oppositeModelFields) { // find the opposite relation with the matching name const relAttr = oppositeField.attributes.find((a) => a.decl.ref?.name === '@relation'); @@ -226,18 +227,23 @@ export default class DataModelValidator implements AstValidator { return false; } - private validateRelationField(field: DataModelField, accept: ValidationAcceptor) { + private validateRelationField(contextModel: DataModel, field: DataModelField, accept: ValidationAcceptor) { const thisRelation = this.parseRelation(field, accept); if (!thisRelation.valid) { return; } + if (field.$container !== contextModel && isDelegateModel(field.$container as DataModel)) { + // relation fields inherited from delegate model don't need opposite relation + return; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const oppositeModel = field.type.reference!.ref! as DataModel; // Use name because the current document might be updated - let oppositeFields = oppositeModel.$resolvedFields.filter( - (f) => f.type.reference?.ref?.name === field.$container.name + let oppositeFields = getModelFieldsWithBases(oppositeModel, false).filter( + (f) => f.type.reference?.ref?.name === contextModel.name ); oppositeFields = oppositeFields.filter((f) => { const fieldRel = this.parseRelation(f); @@ -245,13 +251,13 @@ export default class DataModelValidator implements AstValidator { }); if (oppositeFields.length === 0) { - const node = field.$isInherited ? field.$container : field; - const info: DiagnosticInfo = { node, code: IssueCodes.MissingOppositeRelation }; + const info: DiagnosticInfo = { + node: field, + code: IssueCodes.MissingOppositeRelation, + }; info.property = 'name'; - // use cstNode because the field might be inherited from parent model - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const container = field.$cstNode!.element.$container as DataModel; + const container = field.$container; const relationFieldDocUri = getDocument(container).textDocument.uri; const relationDataModelName = container.name; @@ -260,20 +266,20 @@ export default class DataModelValidator implements AstValidator { relationFieldName: field.name, relationDataModelName, relationFieldDocUri, - dataModelName: field.$container.name, + dataModelName: contextModel.name, }; info.data = data; accept( 'error', - `The relation field "${field.name}" on model "${field.$container.name}" is missing an opposite relation field on model "${oppositeModel.name}"`, + `The relation field "${field.name}" on model "${contextModel.name}" is missing an opposite relation field on model "${oppositeModel.name}"`, info ); return; } else if (oppositeFields.length > 1) { oppositeFields - .filter((x) => !x.$isInherited) + .filter((f) => f.$container !== contextModel) .forEach((f) => { if (this.isSelfRelation(f)) { // self relations are partial @@ -376,14 +382,30 @@ export default class DataModelValidator implements AstValidator { private validateBaseAbstractModel(model: DataModel, accept: ValidationAcceptor) { model.superTypes.forEach((superType, index) => { - if (!superType.ref?.isAbstract) - accept('error', `Model ${superType.$refText} cannot be extended because it's not abstract`, { - node: model, - property: 'superTypes', - index, - }); + if ( + !superType.ref?.isAbstract && + !superType.ref?.attributes.some((attr) => attr.decl.ref?.name === '@@delegate') + ) + accept( + 'error', + `Model ${superType.$refText} cannot be extended because it's neither abstract nor marked as "@@delegate"`, + { + node: model, + property: 'superTypes', + index, + } + ); }); } + + private validateBaseDelegateModel(model: DataModel, accept: ValidationAcceptor) { + if (model.superTypes.filter((base) => base.ref && isDelegateModel(base.ref)).length > 1) { + accept('error', 'Extending from multiple delegate models is not supported', { + node: model, + property: 'superTypes', + }); + } + } } export interface MissingOppositeRelationData { diff --git a/packages/schema/src/language-server/validator/datasource-validator.ts b/packages/schema/src/language-server/validator/datasource-validator.ts index f24fed08b..d102e409f 100644 --- a/packages/schema/src/language-server/validator/datasource-validator.ts +++ b/packages/schema/src/language-server/validator/datasource-validator.ts @@ -9,7 +9,7 @@ import { SUPPORTED_PROVIDERS } from '../constants'; */ export default class DataSourceValidator implements AstValidator { validate(ds: DataSource, accept: ValidationAcceptor): void { - validateDuplicatedDeclarations(ds.fields, accept); + validateDuplicatedDeclarations(ds, ds.fields, accept); this.validateProvider(ds, accept); this.validateUrl(ds, accept); this.validateRelationMode(ds, accept); diff --git a/packages/schema/src/language-server/validator/enum-validator.ts b/packages/schema/src/language-server/validator/enum-validator.ts index 4223d8a2b..5780d91fb 100644 --- a/packages/schema/src/language-server/validator/enum-validator.ts +++ b/packages/schema/src/language-server/validator/enum-validator.ts @@ -10,7 +10,7 @@ import { validateDuplicatedDeclarations } from './utils'; export default class EnumValidator implements AstValidator { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types validate(_enum: Enum, accept: ValidationAcceptor) { - validateDuplicatedDeclarations(_enum.fields, accept); + validateDuplicatedDeclarations(_enum, _enum.fields, accept); this.validateAttributes(_enum, accept); _enum.fields.forEach((field) => { this.validateField(field, accept); diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 7d8c4dd95..cb42e4cb1 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -1,4 +1,5 @@ import { + AstNode, BinaryExpr, Expression, ExpressionType, @@ -9,11 +10,12 @@ import { isLiteralExpr, isMemberAccessExpr, isNullExpr, + isReferenceExpr, isThisExpr, } from '@zenstackhq/language/ast'; -import { isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; -import { AstNode, ValidationAcceptor } from 'langium'; -import { findUpAst, getContainingDataModel, isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils'; +import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; +import { ValidationAcceptor, streamAst } from 'langium'; +import { findUpAst, getContainingDataModel } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { typeAssignable } from './utils'; @@ -31,12 +33,22 @@ export default class ExpressionValidator implements AstValidator { 'auth() cannot be resolved because no model marked wth "@@auth()" or named "User" is found', { node: expr } ); - } else if (isCollectionPredicate(expr)) { - accept('error', 'collection predicate can only be used on an array of model type', { node: expr }); } else { - accept('error', 'expression cannot be resolved', { - node: expr, + const hasReferenceResolutionError = streamAst(expr).some((node) => { + if (isMemberAccessExpr(node)) { + return !!node.member.error; + } + if (isReferenceExpr(node)) { + return !!node.target.error; + } + return false; }); + if (!hasReferenceResolutionError) { + // report silent errors not involving linker errors + accept('error', 'Expression cannot be resolved', { + node: expr, + }); + } } } @@ -220,6 +232,29 @@ export default class ExpressionValidator implements AstValidator { } break; } + + case '?': + case '!': + case '^': + this.validateCollectionPredicate(expr, accept); + break; + } + } + + private validateCollectionPredicate(expr: BinaryExpr, accept: ValidationAcceptor) { + if (!expr.$resolvedType) { + accept('error', 'collection predicate can only be used on an array of model type', { node: expr }); + return; + } + + // TODO: revisit this when we implement lambda inside collection predicate + const thisExpr = streamAst(expr).find(isThisExpr); + if (thisExpr) { + accept( + 'error', + 'using `this` in collection predicate is not supported. To compare entity identity, use id field comparison instead.', + { node: thisExpr } + ); } } diff --git a/packages/schema/src/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts index d8c3df900..a6af730f2 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -11,10 +11,15 @@ import { isDataModelFieldAttribute, isLiteralExpr, } from '@zenstackhq/language/ast'; -import { ExpressionContext, getFunctionExpressionContext, isEnumFieldReference, isFromStdlib } from '@zenstackhq/sdk'; +import { + ExpressionContext, + getDataModelFieldReference, + getFunctionExpressionContext, + isEnumFieldReference, + isFromStdlib, +} from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; import { P, match } from 'ts-pattern'; -import { getDataModelFieldReference } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { typeAssignable } from './utils'; diff --git a/packages/schema/src/language-server/validator/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts index 6f868c614..d071324c1 100644 --- a/packages/schema/src/language-server/validator/schema-validator.ts +++ b/packages/schema/src/language-server/validator/schema-validator.ts @@ -1,7 +1,7 @@ import { Model, isDataModel, isDataSource } from '@zenstackhq/language/ast'; import { hasAttribute } from '@zenstackhq/sdk'; import { LangiumDocuments, ValidationAcceptor } from 'langium'; -import { getAllDeclarationsFromImports, resolveImport, resolveTransitiveImports } from '../../utils/ast-utils'; +import { getAllDeclarationsIncludingImports, resolveImport, resolveTransitiveImports } from '../../utils/ast-utils'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../constants'; import { AstValidator } from '../types'; import { validateDuplicatedDeclarations } from './utils'; @@ -13,7 +13,7 @@ export default class SchemaValidator implements AstValidator { constructor(protected readonly documents: LangiumDocuments) {} validate(model: Model, accept: ValidationAcceptor): void { this.validateImports(model, accept); - validateDuplicatedDeclarations(model.declarations, accept); + validateDuplicatedDeclarations(model, model.declarations, accept); const importedModels = resolveTransitiveImports(this.documents, model); @@ -43,7 +43,7 @@ export default class SchemaValidator implements AstValidator { } private validateDataSources(model: Model, accept: ValidationAcceptor) { - const dataSources = getAllDeclarationsFromImports(this.documents, model).filter((d) => isDataSource(d)); + const dataSources = getAllDeclarationsIncludingImports(this.documents, model).filter((d) => isDataSource(d)); if (dataSources.length > 1) { accept('error', 'Multiple datasource declarations are not allowed', { node: dataSources[1] }); } diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 50e2263d7..6a1a44336 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -3,7 +3,6 @@ import { AttributeParam, BuiltinType, DataModelAttribute, - DataModelField, DataModelFieldAttribute, Expression, ExpressionType, @@ -21,6 +20,7 @@ import { AstNode, ValidationAcceptor } from 'langium'; * Checks if the given declarations have duplicated names */ export function validateDuplicatedDeclarations( + container: AstNode, decls: Array, accept: ValidationAcceptor ): void { @@ -33,8 +33,8 @@ export function validateDuplicatedDeclarations( for (const [name, decls] of Object.entries(groupByName)) { if (decls.length > 1) { let errorField = decls[1]; - if (decls[0].$type === 'DataModelField') { - const nonInheritedFields = decls.filter((x) => !(x as DataModelField).$isInherited); + if (isDataModelField(decls[0])) { + const nonInheritedFields = decls.filter((x) => !(isDataModelField(x) && x.$container !== container)); if (nonInheritedFields.length > 0) { errorField = nonInheritedFields.slice(-1)[0]; } diff --git a/packages/schema/src/language-server/zmodel-code-action.ts b/packages/schema/src/language-server/zmodel-code-action.ts index aace4d0fe..5b6a6c95a 100644 --- a/packages/schema/src/language-server/zmodel-code-action.ts +++ b/packages/schema/src/language-server/zmodel-code-action.ts @@ -2,18 +2,19 @@ import { DataModel, DataModelField, Model, isDataModel } from '@zenstackhq/langu import { AstReflection, CodeActionProvider, - getDocument, IndexManager, LangiumDocument, LangiumDocuments, LangiumServices, MaybePromise, + getDocument, } from 'langium'; import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver'; +import { getModelFieldsWithBases } from '../utils/ast-utils'; import { IssueCodes } from './constants'; -import { ZModelFormatter } from './zmodel-formatter'; import { MissingOppositeRelationData } from './validator/datamodel-validator'; +import { ZModelFormatter } from './zmodel-formatter'; export class ZModelCodeActionProvider implements CodeActionProvider { protected readonly reflection: AstReflection; @@ -92,8 +93,8 @@ export class ZModelCodeActionProvider implements CodeActionProvider { let newText = ''; if (fieldAstNode.type.array) { - //post Post[] - const idField = container.$resolvedFields.find((f) => + // post Post[] + const idField = getModelFieldsWithBases(container).find((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@id') ) as DataModelField; @@ -111,7 +112,7 @@ export class ZModelCodeActionProvider implements CodeActionProvider { const idFieldName = idField.name; const referenceIdFieldName = fieldName + this.upperCaseFirstLetter(idFieldName); - if (!oppositeModel.$resolvedFields.find((f) => f.name === referenceIdFieldName)) { + if (!getModelFieldsWithBases(oppositeModel).find((f) => f.name === referenceIdFieldName)) { referenceField = '\n' + indent + `${referenceIdFieldName} ${idField.type.type}`; } diff --git a/packages/schema/src/language-server/zmodel-completion-provider.ts b/packages/schema/src/language-server/zmodel-completion-provider.ts index 70400db64..cd6dae0ca 100644 --- a/packages/schema/src/language-server/zmodel-completion-provider.ts +++ b/packages/schema/src/language-server/zmodel-completion-provider.ts @@ -159,7 +159,7 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { acceptor(context, item); }; - super.completionForCrossReference(context, crossRef, customAcceptor); + return super.completionForCrossReference(context, crossRef, customAcceptor); } override completionForKeyword( @@ -174,7 +174,7 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { } acceptor(context, item); }; - super.completionForKeyword(context, keyword, customAcceptor); + return super.completionForKeyword(context, keyword, customAcceptor); } private filterKeywordForContext(context: CompletionContext, keyword: string) { diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index 3e3d1f018..93dd1b704 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -1,16 +1,52 @@ -import { AbstractFormatter, AstNode, Formatting, LangiumDocument } from 'langium'; +import { + AbstractFormatter, + AstNode, + ConfigurationProvider, + Formatting, + LangiumDocument, + LangiumServices, + MaybePromise, +} from 'langium'; import * as ast from '@zenstackhq/language/ast'; -import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver'; +import { DocumentFormattingParams, FormattingOptions, TextEdit } from 'vscode-languageserver'; +import { ZModelLanguageMetaData } from '@zenstackhq/language/generated/module'; export class ZModelFormatter extends AbstractFormatter { private formatOptions?: FormattingOptions; + private isPrismaStyle = true; + + protected readonly configurationProvider: ConfigurationProvider; + + constructor(services: LangiumServices) { + super(); + this.configurationProvider = services.shared.workspace.ConfigurationProvider; + } + protected format(node: AstNode): void { const formatter = this.getNodeFormatter(node); + if (ast.isDataModelField(node)) { - formatter.property('type').prepend(Formatting.oneSpace()); - if (node.attributes.length > 0) { - formatter.properties('attributes').prepend(Formatting.oneSpace()); + if (this.isPrismaStyle && ast.isDataModel(node.$container)) { + const dataModel = node.$container; + + const compareFn = (a: number, b: number) => b - a; + const maxNameLength = dataModel.fields.map((x) => x.name.length).sort(compareFn)[0]; + const maxTypeLength = dataModel.fields.map(this.getFieldTypeLength).sort(compareFn)[0]; + + formatter.property('type').prepend(Formatting.spaces(maxNameLength - node.name.length + 1)); + if (node.attributes.length > 0) { + formatter + .node(node.attributes[0]) + .prepend(Formatting.spaces(maxTypeLength - this.getFieldTypeLength(node) + 1)); + + formatter.nodes(...node.attributes.slice(1)).prepend(Formatting.oneSpace()); + } + } else { + formatter.property('type').prepend(Formatting.oneSpace()); + if (node.attributes.length > 0) { + formatter.properties('attributes').prepend(Formatting.oneSpace()); + } } } else if (ast.isDataModelFieldAttribute(node)) { formatter.keyword('(').surround(Formatting.noSpace()); @@ -36,13 +72,24 @@ export class ZModelFormatter extends AbstractFormatter { } } - protected override doDocumentFormat( + override formatDocument( document: LangiumDocument, - options: FormattingOptions, - range?: Range | undefined - ): TextEdit[] { - this.formatOptions = options; - return super.doDocumentFormat(document, options, range); + params: DocumentFormattingParams + ): MaybePromise { + this.formatOptions = params.options; + + this.configurationProvider.getConfiguration(ZModelLanguageMetaData.languageId, 'format').then((config) => { + // in the CLI case, the config is undefined + if (config) { + if (config.usePrismaStyle === false) { + this.setPrismaStyle(false); + } else { + this.setPrismaStyle(true); + } + } + }); + + return super.formatDocument(document, params); } public getFormatOptions(): FormattingOptions | undefined { @@ -52,4 +99,22 @@ export class ZModelFormatter extends AbstractFormatter { public getIndent() { return 1; } + + public setPrismaStyle(isPrismaStyle: boolean) { + this.isPrismaStyle = isPrismaStyle; + } + + private getFieldTypeLength(field: ast.DataModelField) { + let length = (field.type.type || field.type.reference?.$refText)!.length; + + if (field.type.optional) { + length += 1; + } + + if (field.type.array) { + length += 2; + } + + return length; + } } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 69fdf67c2..5a15f9336 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -35,7 +35,13 @@ import { isReferenceExpr, isStringLiteral, } from '@zenstackhq/language/ast'; -import { getContainingModel, hasAttribute, isFromStdlib } from '@zenstackhq/sdk'; +import { + getAuthModel, + getContainingModel, + getModelFieldsWithBases, + isAuthInvocation, + isFutureExpr, +} from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -52,12 +58,7 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { - getAllDeclarationsFromImports, - getContainingDataModel, - isAuthInvocation, - isCollectionPredicate, -} from '../utils/ast-utils'; +import { getAllDataModelsIncludingImports, getContainingDataModel } from '../utils/ast-utils'; import { mapBuiltinTypeToExpressionType } from './validator/utils'; interface DefaultReference extends Reference { @@ -96,8 +97,7 @@ export class ZModelLinker extends DefaultLinker { container: AstNode, property: string, document: LangiumDocument, - extraScopes: ScopeProvider[], - onlyFromExtraScopes = false + extraScopes: ScopeProvider[] ) { if (this.resolveFromScopeProviders(container, property, document, extraScopes)) { return; @@ -105,17 +105,7 @@ export class ZModelLinker extends DefaultLinker { // eslint-disable-next-line @typescript-eslint/no-explicit-any const reference: DefaultReference = (container as any)[property]; - - if (onlyFromExtraScopes) { - // if reference is not resolved from explicit scope providers and automatic linking is not allowed, - // we should explicitly create a linking error - reference._ref = this.createLinkingError({ reference, container, property }); - - // Add the reference to the document's array of references - document.references.push(reference); - } else { - this.doLink({ reference, container, property }, document); - } + this.doLink({ reference, container, property }, document); } //#endregion @@ -261,26 +251,9 @@ export class ZModelLinker extends DefaultLinker { } private resolveReference(node: ReferenceExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { - this.linkReference(node, 'target', document, extraScopes); - node.args.forEach((arg) => this.resolve(arg, document, extraScopes)); + this.resolveDefault(node, document, extraScopes); if (node.target.ref) { - // if the reference is inside the RHS of a collection predicate, it cannot be resolve to a field - // not belonging to the collection's model type - - const collectionPredicateContext = this.getCollectionPredicateContextDataModel(node); - if ( - // inside a collection predicate RHS - collectionPredicateContext && - // current ref expr is resolved to a field - isDataModelField(node.target.ref) && - // the resolved field doesn't belong to the collection predicate's operand's type - node.target.ref.$container !== collectionPredicateContext - ) { - this.unresolvableRefExpr(node); - return; - } - // resolve type if (node.target.ref.$type === EnumField) { this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); @@ -290,26 +263,6 @@ export class ZModelLinker extends DefaultLinker { } } - private getCollectionPredicateContextDataModel(node: ReferenceExpr) { - let curr: AstNode | undefined = node; - while (curr) { - if ( - curr.$container && - // parent is a collection predicate - isCollectionPredicate(curr.$container) && - // the collection predicate's LHS is resolved to a DataModel - isDataModel(curr.$container.left.$resolvedType?.decl) && - // current node is the RHS - curr.$containerProperty === 'right' - ) { - // return the resolved type of LHS - return curr.$container.left.$resolvedType?.decl; - } - curr = curr.$container; - } - return undefined; - } - private resolveArray(node: ArrayExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { node.items.forEach((item) => this.resolve(item, document, extraScopes)); @@ -329,24 +282,18 @@ export class ZModelLinker extends DefaultLinker { if (node.function.ref) { // eslint-disable-next-line @typescript-eslint/ban-types const funcDecl = node.function.ref as FunctionDecl; - if (funcDecl.name === 'auth' && isFromStdlib(funcDecl)) { + if (isAuthInvocation(node)) { // auth() function is resolved to User model in the current document const model = getContainingModel(node); if (model) { - let authModel = getAllDeclarationsFromImports(this.langiumDocuments(), model).find((d) => { - return isDataModel(d) && hasAttribute(d, '@@auth'); - }); - if (!authModel) { - authModel = getAllDeclarationsFromImports(this.langiumDocuments(), model).find((d) => { - return isDataModel(d) && d.name === 'User'; - }); - } + const allDataModels = getAllDataModelsIncludingImports(this.langiumDocuments(), model); + const authModel = getAuthModel(allDataModels); if (authModel) { node.$resolvedType = { decl: authModel, nullable: true }; } } - } else if (funcDecl.name === 'future' && isFromStdlib(funcDecl)) { + } else if (isFutureExpr(node)) { // future() function is resolved to current model node.$resolvedType = { decl: getContainingDataModel(node) }; } else { @@ -372,14 +319,11 @@ export class ZModelLinker extends DefaultLinker { document: LangiumDocument, extraScopes: ScopeProvider[] ) { - this.resolve(node.operand, document, extraScopes); + this.resolveDefault(node, document, extraScopes); const operandResolved = node.operand.$resolvedType; if (operandResolved && !operandResolved.array && isDataModel(operandResolved.decl)) { - const modelDecl = operandResolved.decl as DataModel; - const provider = (name: string) => modelDecl.$resolvedFields.find((f) => f.name === name); // member access is resolved only in the context of the operand type - this.linkReference(node, 'member', document, [provider], true); if (node.member.ref) { this.resolveToDeclaredType(node, node.member.ref.type); @@ -393,20 +337,10 @@ export class ZModelLinker extends DefaultLinker { } private resolveCollectionPredicate(node: BinaryExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { - this.resolve(node.left, document, extraScopes); + this.resolveDefault(node, document, extraScopes); const resolvedType = node.left.$resolvedType; if (resolvedType && isDataModel(resolvedType.decl) && resolvedType.array) { - const dataModelDecl = resolvedType.decl; - const provider = (name: string) => { - if (name === 'this') { - return dataModelDecl; - } else { - return dataModelDecl.$resolvedFields.find((f) => f.name === name); - } - }; - extraScopes = [provider, ...extraScopes]; - this.resolve(node.right, document, extraScopes); this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); } else { // error is reported in validation pass @@ -460,10 +394,11 @@ export class ZModelLinker extends DefaultLinker { // // In model B, the attribute argument "myId" is resolved to the field "myId" in model A - const transtiveDataModel = attrAppliedOn.type.reference?.ref as DataModel; - if (transtiveDataModel) { + const transitiveDataModel = attrAppliedOn.type.reference?.ref as DataModel; + if (transitiveDataModel) { // resolve references in the context of the transitive data model - const scopeProvider = (name: string) => transtiveDataModel.$resolvedFields.find((f) => f.name === name); + const scopeProvider = (name: string) => + getModelFieldsWithBases(transitiveDataModel).find((f) => f.name === name); if (isArrayExpr(node.value)) { node.value.items.forEach((item) => { if (isReferenceExpr(item)) { @@ -518,18 +453,6 @@ export class ZModelLinker extends DefaultLinker { } private resolveDataModel(node: DataModel, document: LangiumDocument, extraScopes: ScopeProvider[]) { - if (node.superTypes.length > 0) { - const superTypeProviders: ScopeProvider[] = []; - // build scope providers for super types recursively with breadth-first search - const queue = node.superTypes.map((t) => t.ref!); - while (queue.length > 0) { - const superType = queue.shift()!; - const provider = (name: string) => superType.fields.find((f) => f.name === name); - superTypeProviders.push(provider); - queue.push(...superType.superTypes.map((t) => t.ref!)); - } - extraScopes = [...superTypeProviders, ...extraScopes]; - } return this.resolveDefault(node, document, extraScopes); } diff --git a/packages/schema/src/language-server/zmodel-module.ts b/packages/schema/src/language-server/zmodel-module.ts index c0c66ce43..116d486da 100644 --- a/packages/schema/src/language-server/zmodel-module.ts +++ b/packages/schema/src/language-server/zmodel-module.ts @@ -66,7 +66,7 @@ export const ZModelModule: Module new ZModelValidator(services), }, lsp: { - Formatter: () => new ZModelFormatter(), + Formatter: (services) => new ZModelFormatter(services), CodeActionProvider: (services) => new ZModelCodeActionProvider(services), DefinitionProvider: (services) => new ZModelDefinitionProvider(services), SemanticTokenProvider: (services) => new ZModelSemanticTokenProvider(services), diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 8eda869e8..e48a17621 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -1,7 +1,6 @@ import { - DataModel, + BinaryExpr, MemberAccessExpr, - Model, isDataModel, isDataModelField, isEnumField, @@ -9,8 +8,9 @@ import { isMemberAccessExpr, isModel, isReferenceExpr, + isThisExpr, } from '@zenstackhq/language/ast'; -import { getAuthModel, getDataModels } from '@zenstackhq/sdk'; +import { getAuthModel, getModelFieldsWithBases, getRecursiveBases, isAuthInvocation } from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -19,7 +19,6 @@ import { EMPTY_SCOPE, LangiumDocument, LangiumServices, - Mutable, PrecomputedScopes, ReferenceInfo, Scope, @@ -30,8 +29,14 @@ import { stream, streamAllContents, } from 'langium'; +import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { resolveImportUri } from '../utils/ast-utils'; +import { + getAllDataModelsIncludingImports, + isCollectionPredicate, + isFutureInvocation, + resolveImportUri, +} from '../utils/ast-utils'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; /** @@ -66,54 +71,23 @@ export class ZModelScopeComputation extends DefaultScopeComputation { return result; } - override computeLocalScopes( - document: LangiumDocument, - cancelToken?: CancellationToken | undefined - ): Promise { - const result = super.computeLocalScopes(document, cancelToken); - - //the $resolvedFields would be used in Linking stage for all the documents - //so we need to set it at the end of the scope computation - this.resolveBaseModels(document); - return result; - } - - private resolveBaseModels(document: LangiumDocument) { - const model = document.parseResult.value as Model; - - model.declarations.forEach((decl) => { - if (decl.$type === 'DataModel') { - const dataModel = decl as DataModel; - dataModel.$resolvedFields = [...dataModel.fields]; - this.getRecursiveSuperTypes(dataModel).forEach((superType) => { - superType.fields.forEach((field) => { - const cloneField = Object.assign({}, field); - cloneField.$isInherited = true; - const mutable = cloneField as Mutable; - // update container - mutable.$container = dataModel; - dataModel.$resolvedFields.push(cloneField); - }); - }); - } - }); - } + override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes) { + super.processNode(node, document, scopes); - private getRecursiveSuperTypes(dataModel: DataModel): DataModel[] { - const result: DataModel[] = []; - dataModel.superTypes.forEach((superType) => { - const superTypeDecl = superType.ref; - if (superTypeDecl) { - result.push(superTypeDecl); - result.push(...this.getRecursiveSuperTypes(superTypeDecl)); + if (isDataModel(node) && !node.$baseMerged) { + // add base fields to the scope recursively + const bases = getRecursiveBases(node); + for (const base of bases) { + for (const field of base.fields) { + scopes.add(node, this.descriptions.createDescription(field, this.nameProvider.getName(field))); + } } - }); - return result; + } } } export class ZModelScopeProvider extends DefaultScopeProvider { - constructor(services: LangiumServices) { + constructor(private readonly services: LangiumServices) { super(services); } @@ -133,57 +107,140 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // allow plugin models des.documentUri.path.endsWith(PLUGIN_MODULE_NAME) || // allow imported documents - importedUris.some((importedUri) => (des.documentUri, importedUri)) + importedUris.some((importedUri) => equalURI(des.documentUri, importedUri)) ); return new StreamScope(importedElements); } override getScope(context: ReferenceInfo): Scope { if (isMemberAccessExpr(context.container) && context.container.operand && context.property === 'member') { - return this.getMemberAccessScope(context.container); + return this.getMemberAccessScope(context); + } + + if (isReferenceExpr(context.container) && context.property === 'target') { + // when reference expression is resolved inside a collection predicate, the scope is the collection + const containerCollectionPredicate = getCollectionPredicateContext(context.container); + if (containerCollectionPredicate) { + return this.getCollectionPredicateScope(context, containerCollectionPredicate); + } } + return super.getScope(context); } - private getMemberAccessScope(node: MemberAccessExpr) { - if (isReferenceExpr(node.operand)) { - // scope to target model's fields - const ref = node.operand.target.ref; - if (isDataModelField(ref)) { - const targetModel = ref.type.reference?.ref; - if (isDataModel(targetModel)) { - return this.createScopeForNodes(targetModel.fields); + private getMemberAccessScope(context: ReferenceInfo) { + const referenceType = this.reflection.getReferenceType(context); + const globalScope = this.getGlobalScope(referenceType, context); + const node = context.container as MemberAccessExpr; + + return match(node.operand) + .when(isReferenceExpr, (operand) => { + // operand is a reference, it can only be a model field + const ref = operand.target.ref; + if (isDataModelField(ref)) { + const targetModel = ref.type.reference?.ref; + return this.createScopeForModel(targetModel, globalScope); } - } - } else if (isMemberAccessExpr(node.operand)) { - // scope to target model's fields - const ref = node.operand.member.ref; - if (isDataModelField(ref)) { - const targetModel = ref.type.reference?.ref; - if (isDataModel(targetModel)) { - return this.createScopeForNodes(targetModel.fields); + return EMPTY_SCOPE; + }) + .when(isMemberAccessExpr, (operand) => { + // operand is a member access, it must be resolved to a non-array data model type + const ref = operand.member.ref; + if (isDataModelField(ref) && !ref.type.array) { + const targetModel = ref.type.reference?.ref; + return this.createScopeForModel(targetModel, globalScope); } - } - } else if (isInvocationExpr(node.operand)) { - // deal with member access from `auth()` and `future() - const funcName = node.operand.function.$refText; - if (funcName === 'auth') { - // resolve to `User` or `@@auth` model - const model = getContainerOfType(node, isModel); - if (model) { - const authModel = getAuthModel(getDataModels(model)); - if (authModel) { - return this.createScopeForNodes(authModel.fields); - } + return EMPTY_SCOPE; + }) + .when(isThisExpr, () => { + // operand is `this`, resolve to the containing model + return this.createScopeForContainingModel(node, globalScope); + }) + .when(isInvocationExpr, (operand) => { + // deal with member access from `auth()` and `future() + if (isAuthInvocation(operand)) { + // resolve to `User` or `@@auth` model + return this.createScopeForAuthModel(node, globalScope); } - } - if (funcName === 'future') { - const thisModel = getContainerOfType(node, isDataModel); - if (thisModel) { - return this.createScopeForNodes(thisModel.fields); + if (isFutureInvocation(operand)) { + // resolve `future()` to the containing model + return this.createScopeForContainingModel(node, globalScope); + } + return EMPTY_SCOPE; + }) + .otherwise(() => EMPTY_SCOPE); + } + + private getCollectionPredicateScope(context: ReferenceInfo, collectionPredicate: BinaryExpr) { + const referenceType = this.reflection.getReferenceType(context); + const globalScope = this.getGlobalScope(referenceType, context); + const collection = collectionPredicate.left; + + return match(collection) + .when(isReferenceExpr, (expr) => { + // collection is a reference, it can only be a model field + const ref = expr.target.ref; + if (isDataModelField(ref)) { + const targetModel = ref.type.reference?.ref; + return this.createScopeForModel(targetModel, globalScope); } + return EMPTY_SCOPE; + }) + .when(isMemberAccessExpr, (expr) => { + // collection is a member access, it can only be resolved to a model field + const ref = expr.member.ref; + if (isDataModelField(ref)) { + const targetModel = ref.type.reference?.ref; + return this.createScopeForModel(targetModel, globalScope); + } + return EMPTY_SCOPE; + }) + .when(isAuthInvocation, (expr) => { + return this.createScopeForAuthModel(expr, globalScope); + }) + .otherwise(() => EMPTY_SCOPE); + } + + private createScopeForContainingModel(node: AstNode, globalScope: Scope) { + const model = getContainerOfType(node, isDataModel); + if (model) { + return this.createScopeForNodes(model.fields, globalScope); + } else { + return EMPTY_SCOPE; + } + } + + private createScopeForModel(node: AstNode | undefined, globalScope: Scope) { + if (isDataModel(node)) { + return this.createScopeForNodes(getModelFieldsWithBases(node), globalScope); + } else { + return EMPTY_SCOPE; + } + } + + private createScopeForAuthModel(node: AstNode, globalScope: Scope) { + const model = getContainerOfType(node, isModel); + if (model) { + const allDataModels = getAllDataModelsIncludingImports( + this.services.shared.workspace.LangiumDocuments, + model + ); + const authModel = getAuthModel(allDataModels); + if (authModel) { + return this.createScopeForModel(authModel, globalScope); } } return EMPTY_SCOPE; } } + +function getCollectionPredicateContext(node: AstNode) { + let curr: AstNode | undefined = node; + while (curr) { + if (curr.$container && isCollectionPredicate(curr.$container) && curr.$containerProperty === 'right') { + return curr.$container; + } + curr = curr.$container; + } + return undefined; +} diff --git a/packages/schema/src/plugins/access-policy/index.ts b/packages/schema/src/plugins/access-policy/index.ts deleted file mode 100644 index cbdcbd64f..000000000 --- a/packages/schema/src/plugins/access-policy/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PluginFunction } from '@zenstackhq/sdk'; -import PolicyGenerator from './policy-guard-generator'; - -export const name = 'Access Policy'; - -const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { - return new PolicyGenerator().generate(model, options, globalOptions); -}; - -export default run; diff --git a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts new file mode 100644 index 000000000..18bbd8c72 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts @@ -0,0 +1,151 @@ +import { getIdFields, hasAttribute, isAuthInvocation, isDataModelFieldReference } from '@zenstackhq/sdk'; +import { + DataModel, + DataModelField, + Expression, + isDataModel, + isMemberAccessExpr, + type Model, +} from '@zenstackhq/sdk/ast'; +import { streamAst, type AstNode } from 'langium'; +import { isCollectionPredicate } from '../../../utils/ast-utils'; + +/** + * Generate types for typing the `user` context object passed to the `enhance` call, based + * on the fields (potentially deeply) access through `auth()`. + */ +export function generateAuthType(model: Model, authModel: DataModel) { + const types = new Map< + string, + { + // scalar fields to directly pick from Prisma-generated type + pickFields: string[]; + + // relation fields to include + addFields: { name: string; type: string }[]; + } + >(); + + types.set(authModel.name, { pickFields: getIdFields(authModel).map((f) => f.name), addFields: [] }); + + const ensureType = (model: string) => { + if (!types.has(model)) { + types.set(model, { pickFields: [], addFields: [] }); + } + }; + + const addPickField = (model: string, field: string) => { + let fields = types.get(model); + if (!fields) { + fields = { pickFields: [], addFields: [] }; + types.set(model, fields); + } + if (!fields.pickFields.includes(field)) { + fields.pickFields.push(field); + } + }; + + const addAddField = (model: string, name: string, type: string, array: boolean) => { + let fields = types.get(model); + if (!fields) { + fields = { pickFields: [], addFields: [] }; + types.set(model, fields); + } + if (!fields.addFields.find((f) => f.name === name)) { + fields.addFields.push({ name, type: array ? `${type}[]` : type }); + } + }; + + // get all policy expressions involving `auth()` + const authInvolvedExprs = streamAst(model).filter(isAuthAccess); + + // traverse the expressions and collect types and fields involved + authInvolvedExprs.forEach((expr) => { + streamAst(expr).forEach((node) => { + if (isMemberAccessExpr(node)) { + const exprType = node.operand.$resolvedType?.decl; + if (isDataModel(exprType)) { + const memberDecl = node.member.ref; + if (isDataModel(memberDecl?.type.reference?.ref)) { + // member is a relation + const fieldType = memberDecl.type.reference.ref.name; + ensureType(fieldType); + addAddField(exprType.name, memberDecl.name, fieldType, memberDecl.type.array); + } else { + // member is a scalar + if (!isIgnoredField(node.member.ref)) { + addPickField(exprType.name, node.member.$refText); + } + } + } + } + + if (isDataModelFieldReference(node)) { + // this can happen inside collection predicates + const fieldDecl = node.target.ref as DataModelField; + const fieldType = fieldDecl.type.reference?.ref; + if (isDataModel(fieldType)) { + // field is a relation + ensureType(fieldType.name); + addAddField(fieldDecl.$container.name, node.target.$refText, fieldType.name, fieldDecl.type.array); + } else { + if (!isIgnoredField(fieldDecl)) { + // field is a scalar + addPickField(fieldDecl.$container.name, node.target.$refText); + } + } + } + }); + }); + + // generate: + // ` + // namespace auth { + // export type User = WithRequired, 'id'> & { profile: Profile; } & Record; + // export type Profile = WithRequired, 'age'> & Record; + // } + // ` + + return `namespace auth { + type WithRequired = T & { [P in K]-?: T[P] }; +${Array.from(types.entries()) + .map(([model, fields]) => { + let result = `Partial<_P.${model}>`; + + if (fields.pickFields.length > 0) { + result = `WithRequired<${result}, ${fields.pickFields + .map((f) => `'${f}'`) + .join('|')}> & Record`; + } + + if (fields.addFields.length > 0) { + result = `${result} & { ${fields.addFields.map(({ name, type }) => `${name}: ${type}`).join('; ')} }`; + } + + return ` export type ${model} = ${result};`; + }) + .join('\n')} +}`; +} + +function isAuthAccess(node: AstNode): node is Expression { + if (isAuthInvocation(node)) { + return true; + } + + if (isMemberAccessExpr(node) && isAuthAccess(node.operand)) { + return true; + } + + if (isCollectionPredicate(node)) { + if (isAuthAccess(node.left)) { + return true; + } + } + + return false; +} + +function isIgnoredField(field: DataModelField | undefined) { + return !!(field && hasAttribute(field, '@ignore')); +} diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts new file mode 100644 index 000000000..680bf11dd --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -0,0 +1,584 @@ +import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; +import { + PluginError, + getAttribute, + getAttributeArg, + getAuthModel, + getDataModels, + isDelegateModel, + type PluginOptions, +} from '@zenstackhq/sdk'; +import { + DataModel, + DataModelField, + ReferenceExpr, + isArrayExpr, + isDataModel, + isReferenceExpr, + type Model, +} from '@zenstackhq/sdk/ast'; +import { getDMMF, getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; +import fs from 'fs'; +import path from 'path'; +import { + FunctionDeclarationStructure, + InterfaceDeclaration, + ModuleDeclaration, + Node, + Project, + SourceFile, + SyntaxKind, + TypeAliasDeclaration, + VariableStatement, +} from 'ts-morph'; +import { upperCaseFirst } from 'upper-case-first'; +import { name } from '..'; +import { execPackage } from '../../../utils/exec-utils'; +import { trackPrismaSchemaError } from '../../prisma'; +import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; +import { isDefaultWithAuth } from '../enhancer-utils'; +import { generateAuthType } from './auth-type-generator'; + +// information of delegate models and their sub models +type DelegateInfo = [DataModel, DataModel[]][]; + +export class EnhancerGenerator { + constructor( + private readonly model: Model, + private readonly options: PluginOptions, + private readonly project: Project, + private readonly outDir: string + ) {} + + async generate() { + let logicalPrismaClientDir: string | undefined; + let dmmf: DMMF.Document | undefined; + + const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); + + if (this.needsLogicalClient()) { + // schema contains delegate models, need to generate a logical prisma schema + const result = await this.generateLogicalPrisma(); + + logicalPrismaClientDir = './.logical-prisma-client'; + dmmf = result.dmmf; + + // create a reexport of the logical prisma client + const prismaDts = this.project.createSourceFile( + path.join(this.outDir, 'models.d.ts'), + `export type * from '${logicalPrismaClientDir}/index-fixed';`, + { overwrite: true } + ); + await prismaDts.save(); + } else { + // just reexport the prisma client + const prismaDts = this.project.createSourceFile( + path.join(this.outDir, 'models.d.ts'), + `export type * from '${prismaImport}';`, + { overwrite: true } + ); + await prismaDts.save(); + } + + const authModel = getAuthModel(getDataModels(this.model)); + const authTypes = authModel ? generateAuthType(this.model, authModel) : ''; + const authTypeParam = authModel ? `auth.${authModel.name}` : 'AuthUser'; + + const enhanceTs = this.project.createSourceFile( + path.join(this.outDir, 'enhance.ts'), + `import { type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; +import { createEnhancement } from '@zenstackhq/runtime/enhancements'; +import modelMeta from './model-meta'; +import policy from './policy'; +${this.options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} + +${ + logicalPrismaClientDir + ? this.createLogicalPrismaImports(prismaImport, logicalPrismaClientDir) + : this.createSimplePrismaImports(prismaImport) +} + +${authTypes} + +${ + logicalPrismaClientDir + ? this.createLogicalPrismaEnhanceFunction(authTypeParam) + : this.createSimplePrismaEnhanceFunction(authTypeParam) +} + `, + { overwrite: true } + ); + + await this.saveSourceFile(enhanceTs); + + return { dmmf }; + } + + private createSimplePrismaImports(prismaImport: string) { + return `import { Prisma } from '${prismaImport}'; +import type * as _P from '${prismaImport}'; + `; + } + + private createSimplePrismaEnhanceFunction(authTypeParam: string) { + return ` +export function enhance(prisma: DbClient, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions) { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: Prisma, + ...options + }, context); +} + `; + } + + private createLogicalPrismaImports(prismaImport: string, logicalPrismaClientDir: string) { + return `import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaImport}'; +import type { + InternalArgs, + TypeMapDef, + TypeMapCbDef, + DynamicClientExtensionThis, +} from '${prismaImport}/runtime/library'; +import type * as _P from '${logicalPrismaClientDir}/index-fixed'; +import type { Prisma, PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; +`; + } + + private createLogicalPrismaEnhanceFunction(authTypeParam: string) { + return ` +// overload for plain PrismaClient +export function enhance & InternalArgs>( + prisma: _PrismaClient, + context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): PrismaClient; + +// overload for extended PrismaClient +export function enhance & InternalArgs>( + prisma: DynamicClientExtensionThis, + context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): DynamicClientExtensionThis; + +export function enhance(prisma: any, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): any { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: _Prisma, + ...options + }, context); +} +`; + } + + private needsLogicalClient() { + return this.hasDelegateModel(this.model) || this.hasAuthInDefault(this.model); + } + + private hasDelegateModel(model: Model) { + const dataModels = getDataModels(model); + return dataModels.some( + (dm) => isDelegateModel(dm) && dataModels.some((sub) => sub.superTypes.some((base) => base.ref === dm)) + ); + } + + private hasAuthInDefault(model: Model) { + return getDataModels(model).some((dm) => + dm.fields.some((f) => f.attributes.some((attr) => isDefaultWithAuth(attr))) + ); + } + + private async generateLogicalPrisma() { + const prismaGenerator = new PrismaSchemaGenerator(this.model); + const prismaClientOutDir = './.logical-prisma-client'; + const logicalPrismaFile = path.join(this.outDir, 'logical.prisma'); + await prismaGenerator.generate({ + provider: '@internal', // doesn't matter + schemaPath: this.options.schemaPath, + output: logicalPrismaFile, + overrideClientGenerationPath: prismaClientOutDir, + mode: 'logical', + }); + + // generate the prisma client + const generateCmd = `prisma generate --schema "${logicalPrismaFile}" --no-engine`; + try { + // run 'prisma generate' + await execPackage(generateCmd, { stdio: 'ignore' }); + } catch { + await trackPrismaSchemaError(logicalPrismaFile); + try { + // run 'prisma generate' again with output to the console + await execPackage(generateCmd); + } catch { + // noop + } + throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`); + } + + // make a bunch of typing fixes to the generated prisma client + await this.processClientTypes(path.join(this.outDir, prismaClientOutDir)); + + return { + prismaSchema: logicalPrismaFile, + // load the dmmf of the logical prisma schema + dmmf: await getDMMF({ datamodel: fs.readFileSync(logicalPrismaFile, { encoding: 'utf-8' }) }), + }; + } + + private async processClientTypes(prismaClientDir: string) { + // make necessary updates to the generated `index.d.ts` file and save it as `index-fixed.d.ts` + const project = new Project(); + const sf = project.addSourceFileAtPath(path.join(prismaClientDir, 'index.d.ts')); + + // build a map of delegate models and their sub models + const delegateInfo: DelegateInfo = []; + this.model.declarations + .filter((d): d is DataModel => isDelegateModel(d)) + .forEach((dm) => { + delegateInfo.push([ + dm, + this.model.declarations.filter( + (d): d is DataModel => isDataModel(d) && d.superTypes.some((s) => s.ref === dm) + ), + ]); + }); + + // transform index.d.ts and save it into a new file (better perf than in-line editing) + + const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { + overwrite: true, + }); + + if (delegateInfo.length > 0) { + // transform types for delegated models + this.transformDelegate(sf, sfNew, delegateInfo); + sfNew.formatText(); + } else { + // just copy + sfNew.replaceWithText(sf.getFullText()); + } + await sfNew.save(); + } + + private transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { + // copy toplevel imports + sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); + + // copy toplevel import equals + sfNew.addStatements(sf.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText())); + + // copy toplevel exports + sfNew.addExportAssignments(sf.getExportAssignments().map((n) => n.getStructure())); + + // copy toplevel type aliases + sfNew.addTypeAliases(sf.getTypeAliases().map((n) => n.getStructure())); + + // copy toplevel classes + sfNew.addClasses(sf.getClasses().map((n) => n.getStructure())); + + // copy toplevel variables + sfNew.addVariableStatements(sf.getVariableStatements().map((n) => n.getStructure())); + + // copy toplevel namespaces except for `Prisma` + sfNew.addModules( + sf + .getModules() + .filter((n) => n.getName() !== 'Prisma') + .map((n) => n.getStructure()) + ); + + // transform the `Prisma` namespace + const prismaModule = sf.getModuleOrThrow('Prisma'); + const newPrismaModule = sfNew.addModule({ name: 'Prisma', isExported: true }); + this.transformPrismaModule(prismaModule, newPrismaModule, delegateInfo); + } + + private transformPrismaModule( + prismaModule: ModuleDeclaration, + newPrismaModule: ModuleDeclaration, + delegateInfo: DelegateInfo + ) { + // module block is the direct container of declarations inside a namespace + const moduleBlock = prismaModule.getFirstChildByKindOrThrow(SyntaxKind.ModuleBlock); + + // most of the toplevel constructs should be copied over + // here we use ts-morph batch operations for optimal performance + + // copy imports + newPrismaModule.addStatements( + moduleBlock.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText()) + ); + + // copy classes + newPrismaModule.addClasses(moduleBlock.getClasses().map((n) => n.getStructure())); + + // copy functions + newPrismaModule.addFunctions( + moduleBlock.getFunctions().map((n) => n.getStructure() as FunctionDeclarationStructure) + ); + + // copy nested namespaces + newPrismaModule.addModules(moduleBlock.getModules().map((n) => n.getStructure())); + + // transform variables + const newVariables = moduleBlock + .getVariableStatements() + .map((variable) => this.transformVariableStatement(variable)); + newPrismaModule.addVariableStatements(newVariables); + + // transform interfaces + const newInterfaces = moduleBlock.getInterfaces().map((iface) => this.transformInterface(iface, delegateInfo)); + newPrismaModule.addInterfaces(newInterfaces); + + // transform type aliases + const newTypeAliases = moduleBlock + .getTypeAliases() + .map((typeAlias) => this.transformTypeAlias(typeAlias, delegateInfo)); + newPrismaModule.addTypeAliases(newTypeAliases); + } + + private transformVariableStatement(variable: VariableStatement) { + const structure = variable.getStructure(); + + // remove `delegate_aux_*` fields from the variable's typing + const auxFields = this.findAuxDecls(variable); + if (auxFields.length > 0) { + structure.declarations.forEach((variable) => { + let source = variable.type?.toString(); + auxFields.forEach((f) => { + source = source?.replace(f.getText(), ''); + }); + variable.type = source; + }); + } + + return structure; + } + + private transformInterface(iface: InterfaceDeclaration, delegateInfo: DelegateInfo) { + const structure = iface.getStructure(); + + // filter out aux fields + structure.properties = structure.properties?.filter((p) => !p.name.startsWith(DELEGATE_AUX_RELATION_PREFIX)); + + // filter out aux methods + structure.methods = structure.methods?.filter((m) => !m.name.startsWith(DELEGATE_AUX_RELATION_PREFIX)); + + if (delegateInfo.some(([delegate]) => `${delegate.name}Delegate` === iface.getName())) { + // delegate models cannot be created directly, remove create/createMany/upsert + structure.methods = structure.methods?.filter((m) => !['create', 'createMany', 'upsert'].includes(m.name)); + } + + return structure; + } + + private transformTypeAlias(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo) { + const structure = typeAlias.getStructure(); + let source = structure.type as string; + + // remove aux fields + source = this.removeAuxFieldsFromTypeAlias(typeAlias, source); + + // remove discriminator field from concrete input types + source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); + + // remove create/connectOrCreate/upsert fields from delegate's input types + source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); + + // remove delegate fields from nested mutation input types + source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); + + // fix delegate payload union type + source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); + + structure.type = source; + return structure; + } + + private fixDelegatePayloadType(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) { + // change the type of `$Payload` type of delegate model to a union of concrete types + const typeName = typeAlias.getName(); + const payloadRecord = delegateInfo.find(([delegate]) => `$${delegate.name}Payload` === typeName); + if (payloadRecord) { + const discriminatorDecl = this.getDiscriminatorField(payloadRecord[0]); + if (discriminatorDecl) { + source = `${payloadRecord[1] + .map( + (concrete) => + `($${concrete.name}Payload & { scalars: { ${discriminatorDecl.name}: '${concrete.name}' } })` + ) + .join(' | ')}`; + } + } + return source; + } + + private removeCreateFromDelegateInput( + typeAlias: TypeAliasDeclaration, + delegateModels: DelegateInfo, + source: string + ) { + // remove create/connectOrCreate/upsert fields from delegate's input types because + // delegate models cannot be created directly + const typeName = typeAlias.getName(); + const delegateModelNames = delegateModels.map(([delegate]) => delegate.name); + const delegateCreateUpdateInputRegex = new RegExp( + `\\${delegateModelNames.join('|')}(Unchecked)?(Create|Update).*Input` + ); + if (delegateCreateUpdateInputRegex.test(typeName)) { + const toRemove = typeAlias + .getDescendantsOfKind(SyntaxKind.PropertySignature) + .filter((p) => ['create', 'connectOrCreate', 'upsert'].includes(p.getName())); + toRemove.forEach((r) => { + source = source.replace(r.getText(), ''); + }); + } + return source; + } + + private removeDiscriminatorFromConcreteInput( + typeAlias: TypeAliasDeclaration, + delegateInfo: DelegateInfo, + source: string + ) { + // remove discriminator field from the create/update input of concrete models because + // discriminator cannot be set directly + const typeName = typeAlias.getName(); + const concreteModelNames = delegateInfo.map(([, concretes]) => concretes.map((c) => c.name)).flatMap((c) => c); + const concreteCreateUpdateInputRegex = new RegExp( + `(${concreteModelNames.join('|')})(Unchecked)?(Create|Update).*Input` + ); + + const match = typeName.match(concreteCreateUpdateInputRegex); + if (match) { + const modelName = match[1]; + const record = delegateInfo.find(([, concretes]) => concretes.some((c) => c.name === modelName)); + if (record) { + // remove all discriminator fields recursively + const delegateOfConcrete = record[0]; + const discriminators = this.getDiscriminatorFieldsRecursively(delegateOfConcrete); + discriminators.forEach((discriminatorDecl) => { + const discriminatorNode = this.findNamedProperty(typeAlias, discriminatorDecl.name); + if (discriminatorNode) { + source = source.replace(discriminatorNode.getText(), ''); + } + }); + } + } + return source; + } + + private removeAuxFieldsFromTypeAlias(typeAlias: TypeAliasDeclaration, source: string) { + // remove `delegate_aux_*` fields from the type alias + const auxDecls = this.findAuxDecls(typeAlias); + if (auxDecls.length > 0) { + auxDecls.forEach((d) => { + source = source.replace(d.getText(), ''); + }); + } + return source; + } + + private removeDelegateFieldsFromNestedMutationInput( + typeAlias: TypeAliasDeclaration, + _delegateInfo: DelegateInfo, + source: string + ) { + const name = typeAlias.getName(); + + // remove delegate model fields (and corresponding fk fields) from + // create/update input types nested inside concrete models + + const regex = new RegExp(`(.+)(Create|Update)Without${upperCaseFirst(DELEGATE_AUX_RELATION_PREFIX)}_(.+)Input`); + const match = name.match(regex); + if (!match) { + return source; + } + + const nameTuple = match[3]; // [modelName]_[relationFieldName]_[concreteModelName] + const [modelName, relationFieldName, _] = nameTuple.split('_'); + + const fieldDef = this.findNamedProperty(typeAlias, relationFieldName); + if (fieldDef) { + // remove relation field of delegate type, e.g., `asset` + source = source.replace(fieldDef.getText(), ''); + } + + // remove fk fields related to the delegate type relation, e.g., `assetId` + + const relationModel = this.model.declarations.find( + (d): d is DataModel => isDataModel(d) && d.name === modelName + ); + + if (!relationModel) { + return source; + } + + const relationField = relationModel.fields.find((f) => f.name === relationFieldName); + if (!relationField) { + return source; + } + + const relAttr = getAttribute(relationField, '@relation'); + if (!relAttr) { + return source; + } + + const fieldsArg = getAttributeArg(relAttr, 'fields'); + let fkFields: string[] = []; + if (isArrayExpr(fieldsArg)) { + fkFields = fieldsArg.items.map((e) => (e as ReferenceExpr).target.$refText); + } + + fkFields.forEach((fkField) => { + const fieldDef = this.findNamedProperty(typeAlias, fkField); + if (fieldDef) { + source = source.replace(fieldDef.getText(), ''); + } + }); + + return source; + } + + private findNamedProperty(typeAlias: TypeAliasDeclaration, name: string) { + return typeAlias.getFirstDescendant((d) => d.isKind(SyntaxKind.PropertySignature) && d.getName() === name); + } + + private findAuxDecls(node: Node) { + return node + .getDescendantsOfKind(SyntaxKind.PropertySignature) + .filter((n) => n.getName().startsWith(DELEGATE_AUX_RELATION_PREFIX)); + } + + private getDiscriminatorField(delegate: DataModel) { + const delegateAttr = getAttribute(delegate, '@@delegate'); + if (!delegateAttr) { + return undefined; + } + const arg = delegateAttr.args[0]?.value; + return isReferenceExpr(arg) ? (arg.target.ref as DataModelField) : undefined; + } + + private getDiscriminatorFieldsRecursively(delegate: DataModel, result: DataModelField[] = []) { + if (isDelegateModel(delegate)) { + const discriminator = this.getDiscriminatorField(delegate); + if (discriminator) { + result.push(discriminator); + } + + for (const superType of delegate.superTypes) { + if (superType.ref) { + result.push(...this.getDiscriminatorFieldsRecursively(superType.ref, result)); + } + } + } + return result; + } + + private async saveSourceFile(sf: SourceFile) { + if (this.options.preserveTsFiles) { + await sf.save(); + } + } +} diff --git a/packages/schema/src/plugins/enhancer/enhancer-utils.ts b/packages/schema/src/plugins/enhancer/enhancer-utils.ts new file mode 100644 index 000000000..9bb429ca5 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhancer-utils.ts @@ -0,0 +1,20 @@ +import { isAuthInvocation } from '@zenstackhq/sdk'; +import type { DataModelFieldAttribute } from '@zenstackhq/sdk/ast'; +import { streamAst } from 'langium'; + +/** + * Check if the given field attribute is a `@default` with `auth()` invocation + */ +export function isDefaultWithAuth(attr: DataModelFieldAttribute) { + if (attr.decl.ref?.name !== '@default') { + return false; + } + + const expr = attr.args[0]?.value; + if (!expr) { + return false; + } + + // find `auth()` in default value expression + return streamAst(expr).some(isAuthInvocation); +} diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts new file mode 100644 index 000000000..0c82acfba --- /dev/null +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -0,0 +1,41 @@ +import { PluginError, createProject, resolvePath, type PluginFunction } from '@zenstackhq/sdk'; +import path from 'path'; +import { getDefaultOutputFolder } from '../plugin-utils'; +import { EnhancerGenerator } from './enhance'; +import { generate as generateModelMeta } from './model-meta'; +import { generate as generatePolicy } from './policy'; + +export const name = 'Prisma Enhancer'; +export const description = 'Generating PrismaClient enhancer'; + +const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { + let outDir = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); + if (!outDir) { + throw new PluginError(name, `Unable to determine output path, not running plugin`); + } + outDir = resolvePath(outDir, options); + + const project = globalOptions?.tsProject ?? createProject(); + + await generateModelMeta(model, options, project, outDir); + await generatePolicy(model, options, project, outDir); + const { dmmf } = await new EnhancerGenerator(model, options, project, outDir).generate(); + + let prismaClientPath: string | undefined; + if (dmmf) { + // a logical client is generated + if (typeof options.output === 'string') { + // get the absolute path of the prisma client types + const prismaClientPathAbs = path.resolve(options.output, 'models'); + + // resolve it relative to the schema path + prismaClientPath = path.relative(path.dirname(options.schemaPath), prismaClientPathAbs); + } else { + prismaClientPath = `.zenstack/models`; + } + } + + return { dmmf, warnings: [], prismaClientPath }; +}; + +export default run; diff --git a/packages/schema/src/plugins/enhancer/model-meta/index.ts b/packages/schema/src/plugins/enhancer/model-meta/index.ts new file mode 100644 index 000000000..9939ae346 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/model-meta/index.ts @@ -0,0 +1,17 @@ +import { generateModelMeta, getDataModels, type PluginOptions } from '@zenstackhq/sdk'; +import type { Model } from '@zenstackhq/sdk/ast'; +import path from 'path'; +import type { Project } from 'ts-morph'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + const outFile = path.join(outDir, 'model-meta.ts'); + const dataModels = getDataModels(model); + + // save ts files if requested explicitly or the user provided + const preserveTsFiles = options.preserveTsFiles === true || !!options.output; + await generateModelMeta(project, dataModels, { + output: outFile, + generateAttributes: true, + preserveTsFiles, + }); +} diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts similarity index 87% rename from packages/schema/src/plugins/access-policy/expression-writer.ts rename to packages/schema/src/plugins/enhancer/policy/expression-writer.ts index a5de026f0..3ce681dad 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -2,9 +2,11 @@ import { BinaryExpr, BooleanLiteral, DataModel, + DataModelField, Expression, InvocationExpr, isDataModel, + isDataModelField, isEnumField, isMemberAccessExpr, isReferenceExpr, @@ -13,25 +15,28 @@ import { MemberAccessExpr, NumberLiteral, ReferenceExpr, + ReferenceTarget, StringLiteral, UnaryExpr, } from '@zenstackhq/language/ast'; +import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { ExpressionContext, getFunctionExpressionContext, + getIdFields, getLiteral, + isAuthInvocation, isDataModelFieldReference, + isDelegateModel, isFutureExpr, PluginError, + TypeScriptExpressionTransformer, + TypeScriptExpressionTransformerError, } from '@zenstackhq/sdk'; import { lowerCaseFirst } from 'lower-case-first'; +import invariant from 'tiny-invariant'; import { CodeBlockWriter } from 'ts-morph'; -import { name } from '.'; -import { getIdFields, isAuthInvocation } from '../../utils/ast-utils'; -import { - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '../../utils/typescript-expression-transformer'; +import { name } from '..'; type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<='; @@ -116,11 +121,44 @@ export class ExpressionWriter { throw new Error('We should never get here'); } else { this.block(() => { - this.writer.write(`${expr.target.ref?.name}: true`); + const ref = expr.target.ref; + invariant(ref); + if (this.isFieldReferenceToDelegateModel(ref)) { + const thisModel = ref.$container as DataModel; + const targetBase = ref.$inheritedFrom; + this.writeBaseHierarchy(thisModel, targetBase, () => this.writer.write(`${ref.name}: true`)); + } else { + this.writer.write(`${ref.name}: true`); + } }); } } + private writeBaseHierarchy(thisModel: DataModel, targetBase: DataModel | undefined, conditionWriter: () => void) { + if (!targetBase || thisModel === targetBase) { + conditionWriter(); + return; + } + + const base = this.getDelegateBase(thisModel); + if (!base) { + throw new PluginError(name, `Failed to resolve delegate base model for "${thisModel.name}"`); + } + + this.writer.write(`${`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(base.name)}`}: `); + this.writer.block(() => { + this.writeBaseHierarchy(base, targetBase, conditionWriter); + }); + } + + private getDelegateBase(model: DataModel) { + return model.superTypes.map((t) => t.ref).filter((t) => t && isDelegateModel(t))?.[0]; + } + + private isFieldReferenceToDelegateModel(ref: ReferenceTarget): ref is DataModelField { + return isDataModelField(ref) && !!ref.$inheritedFrom && isDelegateModel(ref.$inheritedFrom); + } + private writeMemberAccess(expr: MemberAccessExpr) { if (this.isAuthOrAuthMemberAccess(expr)) { // member access of `auth()`, generate plain expression @@ -499,48 +537,67 @@ export class ExpressionWriter { filterOp?: FilterOperators, extraArgs?: Record ) { - let selector: string | undefined; + // let selector: string | undefined; let operand: Expression | undefined; + let fieldWriter: ((conditionWriter: () => void) => void) | undefined; if (isThisExpr(fieldAccess)) { // pass on writeCondition(); return; } else if (isReferenceExpr(fieldAccess)) { - selector = fieldAccess.target.ref?.name; + const ref = fieldAccess.target.ref; + invariant(ref); + if (this.isFieldReferenceToDelegateModel(ref)) { + const thisModel = ref.$container as DataModel; + const targetBase = ref.$inheritedFrom; + fieldWriter = (conditionWriter: () => void) => + this.writeBaseHierarchy(thisModel, targetBase, () => { + this.writer.write(`${ref.name}: `); + conditionWriter(); + }); + } else { + fieldWriter = (conditionWriter: () => void) => { + this.writer.write(`${ref.name}: `); + conditionWriter(); + }; + } } else if (isMemberAccessExpr(fieldAccess)) { - if (isFutureExpr(fieldAccess.operand)) { + if (!isFutureExpr(fieldAccess.operand)) { // future().field should be treated as the "field" - selector = fieldAccess.member.ref?.name; - } else { - selector = fieldAccess.member.ref?.name; operand = fieldAccess.operand; } + fieldWriter = (conditionWriter: () => void) => { + this.writer.write(`${fieldAccess.member.ref?.name}: `); + conditionWriter(); + }; } else { throw new PluginError(name, `Unsupported expression type: ${fieldAccess.$type}`); } - if (!selector) { + if (!fieldWriter) { throw new PluginError(name, `Failed to write FieldAccess expression`); } const writerFilterOutput = () => { - this.writer.write(selector + ': '); - if (filterOp) { - this.block(() => { - this.writer.write(`${filterOp}: `); - writeCondition(); + // this.writer.write(selector + ': '); + fieldWriter!(() => { + if (filterOp) { + this.block(() => { + this.writer.write(`${filterOp}: `); + writeCondition(); - if (extraArgs) { - for (const [k, v] of Object.entries(extraArgs)) { - this.writer.write(`,\n${k}: `); - this.plain(v); + if (extraArgs) { + for (const [k, v] of Object.entries(extraArgs)) { + this.writer.write(`,\n${k}: `); + this.plain(v); + } } - } - }); - } else { - writeCondition(); - } + }); + } else { + writeCondition(); + } + }); }; if (operand) { diff --git a/packages/schema/src/plugins/enhancer/policy/index.ts b/packages/schema/src/plugins/enhancer/policy/index.ts new file mode 100644 index 000000000..8eaf1d00b --- /dev/null +++ b/packages/schema/src/plugins/enhancer/policy/index.ts @@ -0,0 +1,8 @@ +import { type PluginOptions } from '@zenstackhq/sdk'; +import type { Model } from '@zenstackhq/sdk/ast'; +import type { Project } from 'ts-morph'; +import { PolicyGenerator } from './policy-guard-generator'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + return new PolicyGenerator().generate(project, model, options, outDir); +} diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts similarity index 94% rename from packages/schema/src/plugins/access-policy/policy-guard-generator.ts rename to packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index 20893da10..753ef8f19 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -1,8 +1,6 @@ import { DataModel, - DataModelAttribute, DataModelField, - DataModelFieldAttribute, Enum, Expression, Model, @@ -29,59 +27,47 @@ import { import { ExpressionContext, PluginError, - PluginGlobalOptions, PluginOptions, RUNTIME_PACKAGE, + TypeScriptExpressionTransformer, + TypeScriptExpressionTransformerError, analyzePolicies, - createProject, - emitProject, getAttributeArg, getAuthModel, getDataModels, + getIdFields, getLiteral, - getPrismaClientImportSpec, hasAttribute, hasValidationAttributes, + isAuthInvocation, isEnumFieldReference, isForeignKeyField, isFromStdlib, isFutureExpr, - resolvePath, resolved, - saveProject, } from '@zenstackhq/sdk'; +import { getPrismaClientImportSpec } from '@zenstackhq/sdk/prisma'; import { streamAllContents, streamAst, streamContents } from 'langium'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; -import { FunctionDeclaration, SourceFile, VariableDeclarationKind, WriterFunction } from 'ts-morph'; -import { name } from '.'; -import { getIdFields, isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils'; -import { - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '../../utils/typescript-expression-transformer'; -import { ALL_OPERATION_KINDS, getDefaultOutputFolder } from '../plugin-utils'; +import { FunctionDeclaration, Project, SourceFile, VariableDeclarationKind, WriterFunction } from 'ts-morph'; +import { name } from '..'; +import { isCollectionPredicate } from '../../../utils/ast-utils'; +import { ALL_OPERATION_KINDS } from '../../plugin-utils'; import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; /** * Generates source file that contains Prisma query guard objects used for injecting database queries */ -export default class PolicyGenerator { - async generate(model: Model, options: PluginOptions, globalOptions?: PluginGlobalOptions) { - let output = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); - if (!output) { - throw new PluginError(options.name, `Unable to determine output path, not running plugin`); - } - output = resolvePath(output, options); - - const project = createProject(); +export class PolicyGenerator { + async generate(project: Project, model: Model, options: PluginOptions, output: string) { const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); sf.addStatements('/* eslint-disable */'); sf.addImportDeclaration({ namedImports: [ { name: 'type QueryContext' }, - { name: 'type DbOperations' }, + { name: 'type CrudContract' }, { name: 'allFieldsEqual' }, { name: 'type PolicyDef' }, ], @@ -89,7 +75,7 @@ export default class PolicyGenerator { }); // import enums - const prismaImport = getPrismaClientImportSpec(model, output); + const prismaImport = getPrismaClientImportSpec(output, options); for (const e of model.declarations.filter((d) => isEnum(d) && this.isEnumReferenced(model, d))) { sf.addImportDeclaration({ namedImports: [{ name: e.name }], @@ -155,20 +141,10 @@ export default class PolicyGenerator { sf.addStatements('export default policy'); - let shouldCompile = true; - if (typeof options.compile === 'boolean') { - // explicit override - shouldCompile = options.compile; - } else if (globalOptions) { - shouldCompile = globalOptions.compile; - } - - if (!shouldCompile || options.preserveTsFiles === true) { - // save ts files - await saveProject(project); - } - if (shouldCompile) { - await emitProject(project); + // save ts files if requested explicitly or the user provided + const preserveTsFiles = options.preserveTsFiles === true || !!options.output; + if (preserveTsFiles) { + await sf.save(); } } @@ -229,7 +205,7 @@ export default class PolicyGenerator { operation: PolicyOperationKind, override = false ) { - const attributes = target.attributes as (DataModelAttribute | DataModelFieldAttribute)[]; + const attributes = target.attributes; const attrName = isDataModel(target) ? `@@${kind}` : `@${kind}`; const attrs = attributes.filter((attr) => { if (attr.decl.ref?.name !== attrName) { @@ -775,7 +751,7 @@ export default class PolicyGenerator { { // for generating field references used by field comparison in the same model name: 'db', - type: 'Record', + type: 'CrudContract', }, ], statements, diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts deleted file mode 100644 index 8d7454674..000000000 --- a/packages/schema/src/plugins/model-meta/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - createProject, - generateModelMeta, - getDataModels, - PluginError, - PluginFunction, - resolvePath, -} from '@zenstackhq/sdk'; -import path from 'path'; -import { getDefaultOutputFolder } from '../plugin-utils'; - -export const name = 'Model Metadata'; - -const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { - let output = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); - if (!output) { - throw new PluginError(options.name, `Unable to determine output path, not running plugin`); - } - - output = resolvePath(output, options); - const outFile = path.join(output, 'model-meta.ts'); - const dataModels = getDataModels(model); - const project = createProject(); - - let shouldCompile = true; - if (typeof options.compile === 'boolean') { - // explicit override - shouldCompile = options.compile; - } else if (globalOptions) { - // from CLI or config file - shouldCompile = globalOptions.compile; - } - - await generateModelMeta(project, dataModels, { - output: outFile, - compile: shouldCompile, - preserveTsFiles: options.preserveTsFiles === true, - generateAttributes: true, - }); -}; - -export default run; diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index e095de898..9a3da35e6 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -1,5 +1,5 @@ import { DEFAULT_RUNTIME_LOAD_PATH, type PolicyOperationKind } from '@zenstackhq/runtime'; -import { PluginGlobalOptions } from '@zenstackhq/sdk'; +import { PluginGlobalOptions, ensureEmptyDir } from '@zenstackhq/sdk'; import fs from 'fs'; import path from 'path'; import { PluginRunnerOptions } from '../cli/plugin-runner'; @@ -28,20 +28,16 @@ export function getNodeModulesFolder(startPath?: string): string | undefined { */ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { const output = options.output ? path.resolve(options.output) : getDefaultOutputFolder(); - if (output && !fs.existsSync(output)) { - fs.mkdirSync(output, { recursive: true }); + if (output) { + ensureEmptyDir(output); if (!options.output) { const pkgJson = { name: '.zenstack', version: '1.0.0', exports: { - './model-meta': { - types: './model-meta.ts', - default: './model-meta.js', - }, - './policy': { - types: './policy.d.ts', - default: './policy.js', + './enhance': { + types: './enhance.d.ts', + default: './enhance.js', }, './zod': { types: './zod/index.d.ts', @@ -59,8 +55,23 @@ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { types: './zod/objects/index.d.ts', default: './zod/objects/index.js', }, + './model-meta': { + types: './model-meta.d.ts', + default: './model-meta.js', + }, + './models': { + types: './models.d.ts', + }, }, }; + + // create stubs for zod exports to make bundlers that statically + // analyze imports (like Next.js) happy + for (const zodFolder of ['models', 'input', 'objects']) { + fs.mkdirSync(path.join(output, 'zod', zodFolder), { recursive: true }); + fs.writeFileSync(path.join(output, 'zod', zodFolder, 'index.js'), ''); + } + fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkgJson, undefined, 4)); } } @@ -77,21 +88,29 @@ export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { return path.resolve(globalOptions.output); } - // Find the real runtime module path, it might be a symlink in pnpm - let runtimeModulePath = require.resolve('@zenstackhq/runtime'); - + // for testing, use the local node_modules if (process.env.ZENSTACK_TEST === '1') { - // handling the case when running as tests, resolve relative to CWD - runtimeModulePath = path.resolve(path.join(process.cwd(), 'node_modules', '@zenstackhq', 'runtime')); + return path.join(process.cwd(), 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); } - if (runtimeModulePath) { - // start with the parent folder of @zenstackhq, supposed to be a node_modules folder - while (!runtimeModulePath.endsWith('@zenstackhq') && runtimeModulePath !== '/') { - runtimeModulePath = path.join(runtimeModulePath, '..'); - } + // find the real runtime module path, it might be a symlink in pnpm + let runtimeModulePath = require.resolve('@zenstackhq/runtime'); + + // start with the parent folder of @zenstackhq, supposed to be a node_modules folder + while (!runtimeModulePath.endsWith('@zenstackhq') && runtimeModulePath !== '/') { runtimeModulePath = path.join(runtimeModulePath, '..'); } + runtimeModulePath = path.join(runtimeModulePath, '..'); + const modulesFolder = getNodeModulesFolder(runtimeModulePath); return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined; } + +/** + * Core plugin providers + */ +export enum CorePlugins { + Prisma = '@core/prisma', + Zod = '@core/zod', + Enhancer = '@core/enhancer', +} diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 3a96cf40f..b016d17f9 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,10 +1,102 @@ -import { PluginFunction } from '@zenstackhq/sdk'; -import PrismaSchemaGenerator from './schema-generator'; +import { PluginError, PluginFunction, getLiteral, resolvePath } from '@zenstackhq/sdk'; +import { GeneratorDecl, isGeneratorDecl } from '@zenstackhq/sdk/ast'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; +import fs from 'fs'; +import path from 'path'; +import stripColor from 'strip-color'; +import telemetry from '../../telemetry'; +import { execPackage } from '../../utils/exec-utils'; +import { findUp } from '../../utils/pkg-utils'; +import { PrismaSchemaGenerator } from './schema-generator'; export const name = 'Prisma'; +export const description = 'Generating Prisma schema'; const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { - return new PrismaSchemaGenerator().generate(model, options); + // deal with calculation of the default output location + const output = options.output + ? resolvePath(options.output as string, options) + : getDefaultPrismaOutputFile(options.schemaPath); + + const warnings = await new PrismaSchemaGenerator(model).generate({ ...options, output }); + let prismaClientPath = '@prisma/client'; + + if (options.generateClient !== false) { + let generateCmd = `prisma generate --schema "${output}"`; + if (typeof options.generateArgs === 'string') { + generateCmd += ` ${options.generateArgs}`; + } + try { + // run 'prisma generate' + await execPackage(generateCmd, { stdio: 'ignore' }); + } catch { + await trackPrismaSchemaError(output); + try { + // run 'prisma generate' again with output to the console + await execPackage(generateCmd); + } catch { + // noop + } + throw new PluginError(name, `Failed to run "prisma generate"`); + } + + // extract user-provided prisma client output path + const generator = model.declarations.find( + (d): d is GeneratorDecl => + isGeneratorDecl(d) && + d.fields.some((f) => f.name === 'provider' && getLiteral(f.value) === 'prisma-client-js') + ); + const clientOutputField = generator?.fields.find((f) => f.name === 'output'); + const clientOutput = getLiteral(clientOutputField?.value); + + if (clientOutput) { + if (path.isAbsolute(clientOutput)) { + prismaClientPath = clientOutput; + } else { + // first get absolute path based on prisma schema location + const absPath = path.resolve(path.dirname(output), clientOutput); + + // then make it relative to the zmodel schema location + prismaClientPath = path.relative(path.dirname(options.schemaPath), absPath); + } + } + } + + // load the result DMMF + const dmmf = await getDMMF({ + datamodel: fs.readFileSync(output, 'utf-8'), + }); + + return { warnings, dmmf, prismaClientPath }; }; +function getDefaultPrismaOutputFile(schemaPath: string) { + // handle override from package.json + const pkgJsonPath = findUp(['package.json'], path.dirname(schemaPath)); + if (pkgJsonPath) { + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); + if (typeof pkgJson?.zenstack?.prisma === 'string') { + if (path.isAbsolute(pkgJson.zenstack.prisma)) { + return pkgJson.zenstack.prisma; + } else { + // resolve relative to package.json + return path.resolve(path.dirname(pkgJsonPath), pkgJson.zenstack.prisma); + } + } + } + + return resolvePath('./prisma/schema.prisma', { schemaPath }); +} + +export async function trackPrismaSchemaError(schema: string) { + try { + await getDMMF({ datamodel: fs.readFileSync(schema, 'utf-8') }); + } catch (err) { + if (err instanceof Error) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + telemetry.track('prisma:error', { command: 'generate', message: stripColor(err.message) }); + } + } +} + export default run; diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index ea6317504..594913f8c 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -110,10 +110,15 @@ export class Model extends ContainerDeclaration { name: string, type: ModelFieldType | string, attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [] + documentations: string[] = [], + addToFront = false ): ModelField { const field = new ModelField(name, type, attributes, documentations); - this.fields.push(field); + if (addToFront) { + this.fields.unshift(field); + } else { + this.fields.push(field); + } return field; } diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 0eeea55c5..d7e7f5b9a 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -16,6 +16,8 @@ import { GeneratorDecl, InvocationExpr, isArrayExpr, + isDataModel, + isDataSource, isInvocationExpr, isLiteralExpr, isNullExpr, @@ -26,30 +28,36 @@ import { NumberLiteral, StringLiteral, } from '@zenstackhq/language/ast'; -import { match } from 'ts-pattern'; +import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; +import { match, P } from 'ts-pattern'; +import { getIdFields } from '../../utils/ast-utils'; -import { PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; +import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { - getDMMF, + getAttribute, + getAttributeArg, + getAttributeArgLiteral, getLiteral, - getPrismaVersion, + isDelegateModel, + isIdField, PluginError, PluginOptions, resolved, - resolvePath, ZModelCodeGenerator, } from '@zenstackhq/sdk'; import fs from 'fs'; import { writeFile } from 'fs/promises'; +import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import semver from 'semver'; -import stripColor from 'strip-color'; +import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; -import telemetry from '../../telemetry'; import { execPackage } from '../../utils/exec-utils'; -import { findUp } from '../../utils/pkg-utils'; +import { isDefaultWithAuth } from '../enhancer/enhancer-utils'; import { + AttributeArgValue, + ModelField, ModelFieldType, AttributeArg as PrismaAttributeArg, AttributeArgValue as PrismaAttributeArgValue, @@ -69,11 +77,12 @@ import { const MODEL_PASSTHROUGH_ATTR = '@@prisma.passthrough'; const FIELD_PASSTHROUGH_ATTR = '@prisma.passthrough'; +const PROVIDERS_SUPPORTING_NAMED_CONSTRAINTS = ['postgresql', 'mysql', 'cockroachdb']; /** * Generates Prisma schema file */ -export default class PrismaSchemaGenerator { +export class PrismaSchemaGenerator { private zModelGenerator: ZModelCodeGenerator = new ZModelCodeGenerator(); private readonly PRELUDE = `////////////////////////////////////////////////////////////////////////////////////////////// @@ -83,8 +92,20 @@ export default class PrismaSchemaGenerator { `; - async generate(model: Model, options: PluginOptions) { + private mode: 'logical' | 'physical' = 'physical'; + + constructor(private readonly zmodel: Model) {} + + async generate(options: PluginOptions) { + if (!options.output) { + throw new PluginError(name, 'Output file is not specified'); + } + + const outFile = options.output as string; const warnings: string[] = []; + if (options.mode) { + this.mode = options.mode as 'logical' | 'physical'; + } const prismaVersion = getPrismaVersion(); if (prismaVersion && semver.lt(prismaVersion, PRISMA_MINIMUM_VERSION)) { @@ -95,7 +116,7 @@ export default class PrismaSchemaGenerator { const prisma = new PrismaModel(); - for (const decl of model.declarations) { + for (const decl of this.zmodel.declarations) { switch (decl.$type) { case DataSource: this.generateDataSource(prisma, decl as DataSource); @@ -110,65 +131,28 @@ export default class PrismaSchemaGenerator { break; case GeneratorDecl: - this.generateGenerator(prisma, decl as GeneratorDecl); + this.generateGenerator(prisma, decl as GeneratorDecl, options); break; } } - const outFile = options.output - ? resolvePath(options.output as string, options) - : getDefaultPrismaOutputFile(options.schemaPath); - if (!fs.existsSync(path.dirname(outFile))) { fs.mkdirSync(path.dirname(outFile), { recursive: true }); } await writeFile(outFile, this.PRELUDE + prisma.toString()); - if (options.format === true) { + if (options.format !== false) { try { // run 'prisma format' - await execPackage(`prisma format --schema ${outFile}`); + await execPackage(`prisma format --schema ${outFile}`, { stdio: 'ignore' }); } catch { warnings.push(`Failed to format Prisma schema file`); } } - const generateClient = options.generateClient !== false; - - if (generateClient) { - let generateCmd = `prisma generate --schema "${outFile}"`; - if (typeof options.generateArgs === 'string') { - generateCmd += ` ${options.generateArgs}`; - } - try { - // run 'prisma generate' - await execPackage(generateCmd, 'ignore'); - } catch { - await this.trackPrismaSchemaError(outFile); - try { - // run 'prisma generate' again with output to the console - await execPackage(generateCmd); - } catch { - // noop - } - throw new PluginError(name, `Failed to run "prisma generate"`); - } - } - return warnings; } - private async trackPrismaSchemaError(schema: string) { - try { - await getDMMF({ datamodel: fs.readFileSync(schema, 'utf-8') }); - } catch (err) { - if (err instanceof Error) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - telemetry.track('prisma:error', { command: 'generate', message: stripColor(err.message) }); - } - } - } - private generateDataSource(prisma: PrismaModel, dataSource: DataSource) { const fields: SimpleField[] = dataSource.fields.map((f) => ({ name: f.name, @@ -221,7 +205,7 @@ export default class PrismaSchemaGenerator { return new ZModelCodeGenerator({ quote: 'double' }).generate(expr); } - private generateGenerator(prisma: PrismaModel, decl: GeneratorDecl) { + private generateGenerator(prisma: PrismaModel, decl: GeneratorDecl, options: PluginOptions) { const generator = prisma.addGenerator( decl.name, decl.fields.map((f) => ({ name: f.name, text: this.configExprToText(f.value) })) @@ -240,20 +224,6 @@ export default class PrismaSchemaGenerator { throw new PluginError(name, 'option "previewFeatures" must be an array'); } - if (semver.lt(prismaVersion, '5.0.0')) { - // extendedWhereUnique feature is opt-in pre V5 - if (!previewFeatures.includes('extendedWhereUnique')) { - previewFeatures.push('extendedWhereUnique'); - } - } - - if (semver.lt(prismaVersion, '5.0.0')) { - // fieldReference feature is opt-in pre V5 - if (!previewFeatures.includes('fieldReference')) { - previewFeatures.push('fieldReference'); - } - } - if (previewFeatures.length > 0) { const curr = generator.fields.find((f) => f.name === 'previewFeatures'); if (!curr) { @@ -263,13 +233,38 @@ export default class PrismaSchemaGenerator { } } } + + if (typeof options.overrideClientGenerationPath === 'string') { + const output = generator.fields.find((f) => f.name === 'output'); + if (output) { + output.text = JSON.stringify(options.overrideClientGenerationPath); + } else { + generator.fields.push({ + name: 'output', + text: JSON.stringify(options.overrideClientGenerationPath), + }); + } + } } } private generateModel(prisma: PrismaModel, decl: DataModel) { const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name); for (const field of decl.fields) { - this.generateModelField(model, field); + if (field.$inheritedFrom) { + if ( + // abstract inheritance is always kept + field.$inheritedFrom.isAbstract || + // logical schema keeps all inherited fields + this.mode === 'logical' || + // id fields are always kept + isIdField(field) + ) { + this.generateModelField(model, field); + } + } else { + this.generateModelField(model, field); + } } for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { @@ -282,6 +277,264 @@ export default class PrismaSchemaGenerator { // user defined comments pass-through decl.comments.forEach((c) => model.addComment(c)); + + // generate relation fields on base models linking to concrete models + this.generateDelegateRelationForBase(model, decl); + + // generate reverse relation fields on concrete models + this.generateDelegateRelationForConcrete(model, decl); + + // expand relations on other models that reference delegated models to concrete models + this.expandPolymorphicRelations(model, decl); + + // name relations inherited from delegate base models for disambiguation + this.nameRelationsInheritedFromDelegate(model, decl); + } + + private generateDelegateRelationForBase(model: PrismaDataModel, decl: DataModel) { + if (this.mode !== 'physical') { + return; + } + + if (!isDelegateModel(decl)) { + return; + } + + // collect concrete models inheriting this model + const concreteModels = decl.$container.declarations.filter( + (d) => isDataModel(d) && d !== decl && d.superTypes.some((base) => base.ref === decl) + ); + + // generate an optional relation field in delegate base model to each concrete model + concreteModels.forEach((concrete) => { + const auxName = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(concrete.name)}`; + model.addField(auxName, new ModelFieldType(concrete.name, false, true)); + }); + } + + private generateDelegateRelationForConcrete(model: PrismaDataModel, concreteDecl: DataModel) { + if (this.mode !== 'physical') { + return; + } + + // generate a relation field for each delegated base model + + const baseModels = concreteDecl.superTypes + .map((t) => t.ref) + .filter((t): t is DataModel => !!t) + .filter((t) => isDelegateModel(t)); + + baseModels.forEach((base) => { + const idFields = getIdFields(base); + + // add relation fields + const relationField = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(base.name)}`; + model.addField(relationField, base.name, [ + new PrismaFieldAttribute('@relation', [ + new PrismaAttributeArg( + 'fields', + new AttributeArgValue( + 'Array', + idFields.map( + (idField) => + new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) + ) + ) + ), + new PrismaAttributeArg( + 'references', + new AttributeArgValue( + 'Array', + idFields.map( + (idField) => + new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) + ) + ) + ), + new PrismaAttributeArg( + 'onDelete', + new AttributeArgValue('FieldReference', new PrismaFieldReference('Cascade')) + ), + new PrismaAttributeArg( + 'onUpdate', + new AttributeArgValue('FieldReference', new PrismaFieldReference('Cascade')) + ), + ]), + ]); + }); + } + + private expandPolymorphicRelations(model: PrismaDataModel, decl: DataModel) { + if (this.mode !== 'logical') { + return; + } + + // the logical schema needs to expand relations to the delegate models to concrete ones + + // for the given model, find relation fields of delegate model type, find all concrete models + // of the delegate model and generate an auxiliary opposite relation field to each of them + decl.fields.forEach((f) => { + const fieldType = f.type.reference?.ref; + if (!isDataModel(fieldType)) { + return; + } + + // find concrete models that inherit from this field's model type + const concreteModels = decl.$container.declarations.filter( + (d) => isDataModel(d) && isDescendantOf(d, fieldType) + ); + + // aux relation name format: delegate_aux_[model]_[relationField]_[concrete] + // e.g., delegate_aux_User_myAsset_Video + + concreteModels.forEach((concrete) => { + const relationField = model.addField( + `${DELEGATE_AUX_RELATION_PREFIX}_${decl.name}_${f.name}_${concrete.name}`, + new ModelFieldType(concrete.name, f.type.array, f.type.optional) + ); + const relAttr = getAttribute(f, '@relation'); + if (relAttr) { + const fieldsArg = relAttr.args.find((arg) => arg.name === 'fields'); + if (fieldsArg) { + const idFields = getIdFields(fieldType); + + // add fk fields, e.g., delegate_aux_User_myAsset_VideoId + const addedIdFields = idFields.map((idField) => + model.addField(`${relationField.name}${upperCaseFirst(idField.name)}`, idField.type.type!) + ); + + const fieldsArg = new AttributeArgValue( + 'Array', + addedIdFields.map( + (f) => new AttributeArgValue('FieldReference', new PrismaFieldReference(f.name)) + ) + ); + + const referencesArg = new AttributeArgValue( + 'Array', + idFields.map( + (f) => new AttributeArgValue('FieldReference', new PrismaFieldReference(f.name)) + ) + ); + + const addedRel = new PrismaFieldAttribute('@relation', [ + // use field name as relation name for disambiguation + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)), + new PrismaAttributeArg('fields', fieldsArg), + new PrismaAttributeArg('references', referencesArg), + ]); + + if (this.supportNamedConstraints) { + addedRel.args.push( + // generate a `map` argument for foreign key constraint disambiguation + new PrismaAttributeArg( + 'map', + new PrismaAttributeArgValue('String', `${relationField.name}_fk`) + ) + ); + } + + relationField.attributes.push(addedRel); + } else { + relationField.attributes.push(this.makeFieldAttribute(relAttr as DataModelFieldAttribute)); + } + } else { + relationField.attributes.push( + new PrismaFieldAttribute('@relation', [ + // use field name as relation name for disambiguation + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)), + ]) + ); + } + }); + }); + } + + private nameRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) { + if (this.mode !== 'logical') { + return; + } + + // the logical schema needs to name relations inherited from delegate base models for disambiguation + + decl.fields.forEach((f) => { + if (!f.$inheritedFrom || !isDelegateModel(f.$inheritedFrom) || !isDataModel(f.type.reference?.ref)) { + return; + } + + const prismaField = model.fields.find((field) => field.name === f.name); + if (!prismaField) { + return; + } + + // find the base field that this field is inherited from + const baseField = f.$inheritedFrom.fields.find((field) => field.name === f.name); + if (!baseField) { + return; + } + + // find the opposite side of the relation + const oppositeRelationField = this.getOppositeRelationField(f.type.reference.ref, baseField); + if (!oppositeRelationField) { + return; + } + + const fieldType = f.type.reference.ref; + + // relation name format: delegate_aux_[relationType]_[oppositeRelationField]_[concrete] + const relAttr = getAttribute(f, '@relation'); + const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${fieldType.name}_${oppositeRelationField.name}_${decl.name}`; + + if (relAttr) { + const nameArg = getAttributeArg(relAttr, 'name'); + if (!nameArg) { + const prismaRelAttr = prismaField.attributes.find( + (attr) => (attr as PrismaFieldAttribute).name === '@relation' + ) as PrismaFieldAttribute; + if (prismaRelAttr) { + prismaRelAttr.args.unshift( + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)) + ); + } + } + } else { + prismaField.attributes.push( + new PrismaFieldAttribute('@relation', [ + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)), + ]) + ); + } + }); + } + + private getOppositeRelationField(oppositeModel: DataModel, relationField: DataModelField) { + const relName = this.getRelationName(relationField); + return oppositeModel.fields.find( + (f) => f.type.reference?.ref === relationField.$container && this.getRelationName(f) === relName + ); + } + + private getRelationName(field: DataModelField) { + const relAttr = getAttribute(field, '@relation'); + if (!relAttr) { + return undefined; + } + return getAttributeArgLiteral(relAttr, 'name'); + } + + private get supportNamedConstraints() { + const ds = this.zmodel.declarations.find(isDataSource); + if (!ds) { + return false; + } + + const provider = ds.fields.find((f) => f.name === 'provider'); + if (!provider) { + return false; + } + + const value = getStringLiteral(provider.value); + return value && PROVIDERS_SUPPORTING_NAMED_CONSTRAINTS.includes(value); } private isPrismaAttribute(attr: DataModelAttribute | DataModelFieldAttribute) { @@ -310,7 +563,7 @@ export default class PrismaSchemaGenerator { } } - private generateModelField(model: PrismaDataModel, field: DataModelField) { + private generateModelField(model: PrismaDataModel, field: DataModelField, addToFront = false) { const fieldType = field.type.type || field.type.reference?.ref?.name || this.getUnsupportedFieldType(field.type); if (!fieldType) { @@ -321,18 +574,59 @@ export default class PrismaSchemaGenerator { const attributes = field.attributes .filter((attr) => this.isPrismaAttribute(attr)) + // `@default` with `auth()` is handled outside Prisma + .filter((attr) => !isDefaultWithAuth(attr)) + .filter( + (attr) => + // when building physical schema, exclude `@default` for id fields inherited from delegate base + !( + this.mode === 'physical' && + isIdField(field) && + this.isInheritedFromDelegate(field) && + attr.decl.$refText === '@default' + ) + ) .map((attr) => this.makeFieldAttribute(attr)); const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr)); - const result = model.addField(field.name, type, attributes, documentations); + const result = model.addField(field.name, type, attributes, documentations, addToFront); + + if (this.mode === 'logical') { + if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { + // field has `@default` with `auth()`, turn it into a dummy default value, and the + // real default value setting is handled outside Prisma + this.setDummyDefault(result, field); + } + } // user defined comments pass-through field.comments.forEach((c) => result.addComment(c)); } + private setDummyDefault(result: ModelField, field: DataModelField) { + const dummyDefaultValue = match(field.type.type) + .with('String', () => new AttributeArgValue('String', '')) + .with(P.union('Int', 'BigInt', 'Float', 'Decimal'), () => new AttributeArgValue('Number', '0')) + .with('Boolean', () => new AttributeArgValue('Boolean', 'false')) + .with('DateTime', () => new AttributeArgValue('FunctionCall', new PrismaFunctionCall('now'))) + .with('Json', () => new AttributeArgValue('String', '{}')) + .with('Bytes', () => new AttributeArgValue('String', '')) + .otherwise(() => { + throw new PluginError(name, `Unsupported field type with default value: ${field.type.type}`); + }); + + result.attributes.push( + new PrismaFieldAttribute('@default', [new PrismaAttributeArg(undefined, dummyDefaultValue)]) + ); + } + + private isInheritedFromDelegate(field: DataModelField) { + return field.$inheritedFrom && isDelegateModel(field.$inheritedFrom); + } + private makeFieldAttribute(attr: DataModelFieldAttribute) { const attrName = resolved(attr.decl).name; if (attrName === FIELD_PASSTHROUGH_ATTR) { @@ -448,20 +742,6 @@ export default class PrismaSchemaGenerator { } } -export function getDefaultPrismaOutputFile(schemaPath: string) { - // handle override from package.json - const pkgJsonPath = findUp(['package.json'], path.dirname(schemaPath)); - if (pkgJsonPath) { - const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); - if (typeof pkgJson?.zenstack?.prisma === 'string') { - if (path.isAbsolute(pkgJson.zenstack.prisma)) { - return pkgJson.zenstack.prisma; - } else { - // resolve relative to package.json - return path.resolve(path.dirname(pkgJsonPath), pkgJson.zenstack.prisma); - } - } - } - - return resolvePath('./prisma/schema.prisma', { schemaPath }); +function isDescendantOf(model: DataModel, superModel: DataModel): boolean { + return model.superTypes.some((s) => s.ref === superModel || isDescendantOf(s.ref!, superModel)); } diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index d1af70882..f2f628b30 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,507 +1,583 @@ -import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { PluginGlobalOptions, PluginOptions, - createProject, - emitProject, + ensureEmptyDir, getDataModels, - getLiteral, - getPrismaClientImportSpec, hasAttribute, isEnumFieldReference, isForeignKeyField, isFromStdlib, parseOptionAsStrings, resolvePath, - saveProject, } from '@zenstackhq/sdk'; -import { DataModel, DataSource, EnumField, Model, isDataModel, isDataSource, isEnum } from '@zenstackhq/sdk/ast'; +import { DataModel, EnumField, Model, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; -import { promises as fs } from 'fs'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { streamAllContents } from 'langium'; import path from 'path'; -import { Project } from 'ts-morph'; +import type { SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; import { getDefaultOutputFolder } from '../plugin-utils'; import Transformer from './transformer'; -import removeDir from './utils/removeDir'; -import { makeFieldSchema, makeValidationRefinements, getFieldSchemaDefault } from './utils/schema-gen'; - -export async function generate( - model: Model, - options: PluginOptions, - dmmf: DMMF.Document, - globalOptions?: PluginGlobalOptions -) { - let output = options.output as string; - if (!output) { - const defaultOutputFolder = getDefaultOutputFolder(globalOptions); - if (defaultOutputFolder) { - output = path.join(defaultOutputFolder, 'zod'); - } else { - output = './generated/zod'; +import { makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; + +export class ZodSchemaGenerator { + private readonly sourceFiles: SourceFile[] = []; + private readonly globalOptions: PluginGlobalOptions; + + constructor( + private readonly model: Model, + private readonly options: PluginOptions, + private readonly dmmf: DMMF.Document, + globalOptions: PluginGlobalOptions | undefined + ) { + if (!globalOptions) { + throw new Error('Global options are required'); } + this.globalOptions = globalOptions; } - output = resolvePath(output, options); - await handleGeneratorOutputValue(output); - - // calculate the models to be excluded - const excludeModels = getExcludedModels(model, options); - - const prismaClientDmmf = dmmf; - const modelOperations = prismaClientDmmf.mappings.modelOperations.filter( - (o) => !excludeModels.find((e) => e === o.model) - ); + async generate() { + let output = this.options.output as string; + if (!output) { + const defaultOutputFolder = getDefaultOutputFolder(this.globalOptions); + if (defaultOutputFolder) { + output = path.join(defaultOutputFolder, 'zod'); + } else { + output = './generated/zod'; + } + } + output = resolvePath(output, this.options); + ensureEmptyDir(output); + Transformer.setOutputPath(output); - // TODO: better way of filtering than string startsWith? - const inputObjectTypes = prismaClientDmmf.schema.inputObjectTypes.prisma.filter( - (type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase())) - ); - const outputObjectTypes = prismaClientDmmf.schema.outputObjectTypes.prisma.filter( - (type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLowerCase())) - ); + // calculate the models to be excluded + const excludeModels = this.getExcludedModels(); - const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter( - (m) => !excludeModels.find((e) => e === m.name) - ); + const prismaClientDmmf = this.dmmf; - // whether Prisma's Unchecked* series of input types should be generated - const generateUnchecked = options.noUncheckedInput !== true; + const modelOperations = prismaClientDmmf.mappings.modelOperations.filter( + (o) => !excludeModels.find((e) => e === o.model) + ); - const project = createProject(); + // TODO: better way of filtering than string startsWith? + const inputObjectTypes = prismaClientDmmf.schema.inputObjectTypes.prisma.filter( + (type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase())) + ); + const outputObjectTypes = prismaClientDmmf.schema.outputObjectTypes.prisma.filter( + (type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLowerCase())) + ); - // common schemas - await generateCommonSchemas(project, output); + const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter( + (m) => !excludeModels.find((e) => e === m.name) + ); - // enums - await generateEnumSchemas( - prismaClientDmmf.schema.enumTypes.prisma, - prismaClientDmmf.schema.enumTypes.model ?? [], - project, - model - ); + // common schemas + await this.generateCommonSchemas(output); - const dataSource = model.declarations.find((d): d is DataSource => isDataSource(d)); + // enums + await this.generateEnumSchemas( + prismaClientDmmf.schema.enumTypes.prisma, + prismaClientDmmf.schema.enumTypes.model ?? [] + ); - const dataSourceProvider = getLiteral( - dataSource?.fields.find((f) => f.name === 'provider')?.value - ) as ConnectorType; + await this.generateModelSchemas(output, excludeModels); - await generateModelSchemas(project, model, output, excludeModels); + if (this.options.modelOnly) { + // generate stub for object and input schemas, so the exports from '@zenstackhq/runtime/zod' are available + this.sourceFiles.push( + this.project.createSourceFile(path.join(output, 'objects', 'index.ts'), '', { overwrite: true }) + ); + this.sourceFiles.push( + this.project.createSourceFile(path.join(output, 'input', 'index.ts'), '', { overwrite: true }) + ); + } else { + // detailed object schemas referenced from input schemas + addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models); + const aggregateOperationSupport = resolveAggregateOperationSupport(inputObjectTypes); + await this.generateObjectSchemas(inputObjectTypes, output); + + // input schemas + const transformer = new Transformer({ + models, + modelOperations, + aggregateOperationSupport, + project: this.project, + inputObjectTypes, + }); + await transformer.generateInputSchemas(this.options); + this.sourceFiles.push(...transformer.sourceFiles); + } - if (options.modelOnly !== true) { - // detailed object schemas referenced from input schemas - Transformer.provider = dataSourceProvider; - addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models); - const aggregateOperationSupport = resolveAggregateOperationSupport(inputObjectTypes); - await generateObjectSchemas(inputObjectTypes, project, output, model, generateUnchecked); + // create barrel file + const exports = [`export * as models from './models'`, `export * as enums from './enums'`]; + if (this.options.modelOnly !== true) { + exports.push(`export * as input from './input'`, `export * as objects from './objects'`); + } + this.sourceFiles.push( + this.project.createSourceFile(path.join(output, 'index.ts'), exports.join(';\n'), { overwrite: true }) + ); - // input schemas - const transformer = new Transformer({ - models, - modelOperations, - aggregateOperationSupport, - project, - zmodel: model, - inputObjectTypes, - }); - await transformer.generateInputSchemas(generateUnchecked); + if (this.options.preserveTsFiles === true || this.options.output) { + // if preserveTsFiles is true or the user provided a custom output directory, + // save the generated files + await Promise.all( + this.sourceFiles.map(async (sf) => { + await sf.formatText(); + await sf.save(); + }) + ); + } } - // create barrel file - const exports = [`export * as models from './models'`, `export * as enums from './enums'`]; - if (options.modelOnly !== true) { - exports.push(`export * as input from './input'`, `export * as objects from './objects'`); - } - project.createSourceFile(path.join(output, 'index.ts'), exports.join(';\n'), { overwrite: true }); - - // emit - let shouldCompile = true; - if (typeof options.compile === 'boolean') { - // explicit override - shouldCompile = options.compile; - } else if (globalOptions) { - // from CLI or config file - shouldCompile = globalOptions.compile; + private get project() { + return this.globalOptions.tsProject; } - if (!shouldCompile || options.preserveTsFiles === true) { - // save ts files - await saveProject(project); - } - if (shouldCompile) { - await emitProject(project); - } -} + private getExcludedModels() { + // resolve "generateModels" option + const generateModels = parseOptionAsStrings(this.options, 'generateModels', name); + if (generateModels) { + if (this.options.modelOnly === true) { + // no model reference needs to be considered, directly exclude any model not included + return this.model.declarations + .filter((d) => isDataModel(d) && !generateModels.includes(d.name)) + .map((m) => m.name); + } else { + // calculate a transitive closure of models to be included + const todo = getDataModels(this.model).filter((dm) => generateModels.includes(dm.name)); + const included = new Set(); + while (todo.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const dm = todo.pop()!; + included.add(dm); + + // add referenced models to the todo list + dm.fields + .map((f) => f.type.reference?.ref) + .filter((type): type is DataModel => isDataModel(type)) + .forEach((type) => { + if (!included.has(type)) { + todo.push(type); + } + }); + } -function getExcludedModels(model: Model, options: PluginOptions) { - // resolve "generateModels" option - const generateModels = parseOptionAsStrings(options, 'generateModels', name); - if (generateModels) { - if (options.modelOnly === true) { - // no model reference needs to be considered, directly exclude any model not included - return model.declarations - .filter((d) => isDataModel(d) && !generateModels.includes(d.name)) - .map((m) => m.name); - } else { - // calculate a transitive closure of models to be included - const todo = getDataModels(model).filter((dm) => generateModels.includes(dm.name)); - const included = new Set(); - while (todo.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const dm = todo.pop()!; - included.add(dm); - - // add referenced models to the todo list - dm.fields - .map((f) => f.type.reference?.ref) - .filter((type): type is DataModel => isDataModel(type)) - .forEach((type) => { - if (!included.has(type)) { - todo.push(type); - } - }); + // finally find the models to be excluded + return getDataModels(this.model) + .filter((dm) => !included.has(dm)) + .map((m) => m.name); } - - // finally find the models to be excluded - return getDataModels(model) - .filter((dm) => !included.has(dm)) - .map((m) => m.name); + } else { + return []; } - } else { - return []; } -} -async function handleGeneratorOutputValue(output: string) { - // create the output directory and delete contents that might exist from a previous run - await fs.mkdir(output, { recursive: true }); - const isRemoveContentsOnly = true; - await removeDir(output, isRemoveContentsOnly); - - Transformer.setOutputPath(output); -} + private async generateCommonSchemas(output: string) { + // Decimal + this.sourceFiles.push( + this.project.createSourceFile( + path.join(output, 'common', 'index.ts'), + ` + import { z } from 'zod'; + export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()}).passthrough()]); + `, + { overwrite: true } + ) + ); + } -async function generateCommonSchemas(project: Project, output: string) { - // Decimal - project.createSourceFile( - path.join(output, 'common', 'index.ts'), - ` -import { z } from 'zod'; -export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()}).passthrough()]); -`, - { overwrite: true } - ); -} + private async generateEnumSchemas(prismaSchemaEnum: DMMF.SchemaEnum[], modelSchemaEnum: DMMF.SchemaEnum[]) { + const enumTypes = [...prismaSchemaEnum, ...modelSchemaEnum]; + const enumNames = enumTypes.map((enumItem) => upperCaseFirst(enumItem.name)); + Transformer.enumNames = enumNames ?? []; + const transformer = new Transformer({ + enumTypes, + project: this.project, + inputObjectTypes: [], + }); + await transformer.generateEnumSchemas(); + this.sourceFiles.push(...transformer.sourceFiles); + } -async function generateEnumSchemas( - prismaSchemaEnum: DMMF.SchemaEnum[], - modelSchemaEnum: DMMF.SchemaEnum[], - project: Project, - zmodel: Model -) { - const enumTypes = [...prismaSchemaEnum, ...modelSchemaEnum]; - const enumNames = enumTypes.map((enumItem) => upperCaseFirst(enumItem.name)); - Transformer.enumNames = enumNames ?? []; - const transformer = new Transformer({ - enumTypes, - project, - zmodel, - inputObjectTypes: [], - }); - await transformer.generateEnumSchemas(); -} + private async generateObjectSchemas(inputObjectTypes: DMMF.InputType[], output: string) { + // whether Prisma's Unchecked* series of input types should be generated + const generateUnchecked = this.options.noUncheckedInput !== true; -async function generateObjectSchemas( - inputObjectTypes: DMMF.InputType[], - project: Project, - output: string, - zmodel: Model, - generateUnchecked: boolean -) { - const moduleNames: string[] = []; - for (let i = 0; i < inputObjectTypes.length; i += 1) { - const fields = inputObjectTypes[i]?.fields; - const name = inputObjectTypes[i]?.name; - if (!generateUnchecked && name.includes('Unchecked')) { - continue; + const moduleNames: string[] = []; + for (let i = 0; i < inputObjectTypes.length; i += 1) { + const fields = inputObjectTypes[i]?.fields; + const name = inputObjectTypes[i]?.name; + if (!generateUnchecked && name.includes('Unchecked')) { + continue; + } + const transformer = new Transformer({ + name, + fields, + project: this.project, + inputObjectTypes, + }); + const moduleName = transformer.generateObjectSchema(generateUnchecked, this.options); + moduleNames.push(moduleName); + this.sourceFiles.push(...transformer.sourceFiles); } - const transformer = new Transformer({ name, fields, project, zmodel, inputObjectTypes }); - const moduleName = transformer.generateObjectSchema(generateUnchecked); - moduleNames.push(moduleName); - } - project.createSourceFile( - path.join(output, 'objects/index.ts'), - moduleNames.map((name) => `export * from './${name}';`).join('\n'), - { overwrite: true } - ); -} -async function generateModelSchemas(project: Project, zmodel: Model, output: string, excludedModels: string[]) { - const schemaNames: string[] = []; - for (const dm of getDataModels(zmodel)) { - if (!excludedModels.includes(dm.name)) { - schemaNames.push(await generateModelSchema(dm, project, output)); - } + this.sourceFiles.push( + this.project.createSourceFile( + path.join(output, 'objects/index.ts'), + moduleNames.map((name) => `export * from './${name}';`).join('\n'), + { overwrite: true } + ) + ); } - project.createSourceFile( - path.join(output, 'models', 'index.ts'), - schemaNames.map((name) => `export * from './${name}';`).join('\n'), - { overwrite: true } - ); -} + private async generateModelSchemas(output: string, excludedModels: string[]) { + const schemaNames: string[] = []; + for (const dm of getDataModels(this.model)) { + if (!excludedModels.includes(dm.name)) { + schemaNames.push(await this.generateModelSchema(dm, output)); + } + } -async function generateModelSchema(model: DataModel, project: Project, output: string) { - const schemaName = `${upperCaseFirst(model.name)}.schema`; - const sf = project.createSourceFile(path.join(output, 'models', `${schemaName}.ts`), undefined, { - overwrite: true, - }); - sf.replaceWithText((writer) => { - const scalarFields = model.fields.filter( - (field) => - // regular fields only - !isDataModel(field.type.reference?.ref) && !isForeignKeyField(field) + this.sourceFiles.push( + this.project.createSourceFile( + path.join(output, 'models', 'index.ts'), + schemaNames.map((name) => `export * from './${name}';`).join('\n'), + { overwrite: true } + ) ); + } - const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref)); - const fkFields = model.fields.filter((field) => isForeignKeyField(field)); + private async generateModelSchema(model: DataModel, output: string) { + const schemaName = `${upperCaseFirst(model.name)}.schema`; + const sf = this.project.createSourceFile(path.join(output, 'models', `${schemaName}.ts`), undefined, { + overwrite: true, + }); + this.sourceFiles.push(sf); + sf.replaceWithText((writer) => { + const scalarFields = model.fields.filter( + (field) => + // regular fields only + !isDataModel(field.type.reference?.ref) && !isForeignKeyField(field) + ); + + const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref)); + const fkFields = model.fields.filter((field) => isForeignKeyField(field)); - writer.writeLine('/* eslint-disable */'); - writer.writeLine(`import { z } from 'zod';`); + writer.writeLine('/* eslint-disable */'); + writer.writeLine(`import { z } from 'zod';`); - // import user-defined enums from Prisma as they might be referenced in the expressions - const importEnums = new Set(); - for (const node of streamAllContents(model)) { - if (isEnumFieldReference(node)) { - const field = node.target.ref as EnumField; - if (!isFromStdlib(field.$container)) { - importEnums.add(field.$container.name); + // import user-defined enums from Prisma as they might be referenced in the expressions + const importEnums = new Set(); + for (const node of streamAllContents(model)) { + if (isEnumFieldReference(node)) { + const field = node.target.ref as EnumField; + if (!isFromStdlib(field.$container)) { + importEnums.add(field.$container.name); + } } } - } - if (importEnums.size > 0) { - const prismaImport = getPrismaClientImportSpec(model.$container, path.join(output, 'models')); - writer.writeLine(`import { ${[...importEnums].join(', ')} } from '${prismaImport}';`); - } + if (importEnums.size > 0) { + const prismaImport = getPrismaClientImportSpec(path.join(output, 'models'), this.options); + writer.writeLine(`import { ${[...importEnums].join(', ')} } from '${prismaImport}';`); + } - // import enum schemas - const importedEnumSchemas = new Set(); - for (const field of scalarFields) { - if (field.type.reference?.ref && isEnum(field.type.reference?.ref)) { - const name = upperCaseFirst(field.type.reference?.ref.name); - if (!importedEnumSchemas.has(name)) { - writer.writeLine(`import { ${name}Schema } from '../enums/${name}.schema';`); - importedEnumSchemas.add(name); + // import enum schemas + const importedEnumSchemas = new Set(); + for (const field of scalarFields) { + if (field.type.reference?.ref && isEnum(field.type.reference?.ref)) { + const name = upperCaseFirst(field.type.reference?.ref.name); + if (!importedEnumSchemas.has(name)) { + writer.writeLine(`import { ${name}Schema } from '../enums/${name}.schema';`); + importedEnumSchemas.add(name); + } } } - } - // import Decimal - if (scalarFields.some((field) => field.type.type === 'Decimal')) { - writer.writeLine(`import { DecimalSchema } from '../common';`); - writer.writeLine(`import { Decimal } from 'decimal.js';`); - } - - // base schema - writer.write(`const baseSchema = z.object(`); - writer.inlineBlock(() => { - scalarFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field, true)},`); - }); - }); - writer.writeLine(');'); - - // relation fields - - let relationSchema: string | undefined; - let fkSchema: string | undefined; + // import Decimal + if (scalarFields.some((field) => field.type.type === 'Decimal')) { + writer.writeLine(`import { DecimalSchema } from '../common';`); + writer.writeLine(`import { Decimal } from 'decimal.js';`); + } - if (relations.length > 0 || fkFields.length > 0) { - relationSchema = 'relationSchema'; - writer.write(`const ${relationSchema} = z.object(`); + // base schema - including all scalar fields, with optionality following the schema + writer.write(`const baseSchema = z.object(`); writer.inlineBlock(() => { - [...relations, ...fkFields].forEach((field) => { + scalarFields.forEach((field) => { writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); }); }); writer.writeLine(');'); - } - if (fkFields.length > 0) { - fkSchema = 'fkSchema'; - writer.write(`const ${fkSchema} = z.object(`); - writer.inlineBlock(() => { - fkFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); + // relation fields + + let relationSchema: string | undefined; + let fkSchema: string | undefined; + + if (relations.length > 0) { + relationSchema = 'relationSchema'; + writer.write(`const ${relationSchema} = z.object(`); + writer.inlineBlock(() => { + [...relations].forEach((field) => { + writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); + }); }); - }); - writer.writeLine(');'); - } + writer.writeLine(');'); + } - // compile "@@validate" to ".refine" - const refinements = makeValidationRefinements(model); - let refineFuncName: string | undefined; - if (refinements.length > 0) { - refineFuncName = `refine${upperCaseFirst(model.name)}`; - writer.writeLine( - `export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join( - '\n' - )}; }` - ); - } + if (fkFields.length > 0) { + fkSchema = 'fkSchema'; + writer.write(`const ${fkSchema} = z.object(`); + writer.inlineBlock(() => { + fkFields.forEach((field) => { + writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); + }); + }); + writer.writeLine(');'); + } - //////////////////////////////////////////////// - // 1. Model schema - //////////////////////////////////////////////// - const fieldsWithoutDefault = scalarFields.filter((f) => !getFieldSchemaDefault(f)); - // mark fields without default value as optional - let modelSchema = makePartial( - 'baseSchema', - fieldsWithoutDefault.length < scalarFields.length ? fieldsWithoutDefault.map((f) => f.name) : undefined - ); + // compile "@@validate" to ".refine" + const refinements = makeValidationRefinements(model); + let refineFuncName: string | undefined; + if (refinements.length > 0) { + refineFuncName = `refine${upperCaseFirst(model.name)}`; + writer.writeLine( + ` +/** + * Schema refinement function for applying \`@@validate\` rules. + */ +export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join( + '\n' + )}; +} +` + ); + } - // omit fields - const fieldsToOmit = scalarFields.filter((field) => hasAttribute(field, '@omit')); - if (fieldsToOmit.length > 0) { - modelSchema = makeOmit( - modelSchema, - fieldsToOmit.map((f) => f.name) - ); - } + //////////////////////////////////////////////// + // 1. Model schema + //////////////////////////////////////////////// + + let modelSchema = 'baseSchema'; - if (relationSchema) { - // export schema with only scalar fields + // omit fields + const fieldsToOmit = scalarFields.filter((field) => hasAttribute(field, '@omit')); + if (fieldsToOmit.length > 0) { + modelSchema = this.makeOmit( + modelSchema, + fieldsToOmit.map((f) => f.name) + ); + } + + // export schema with only scalar fields: `[Model]ScalarSchema` const modelScalarSchema = `${upperCaseFirst(model.name)}ScalarSchema`; - writer.writeLine(`export const ${modelScalarSchema} = ${modelSchema};`); + writer.writeLine(` +/** + * \`${model.name}\` schema excluding foreign keys and relations. + */ +export const ${modelScalarSchema} = ${modelSchema}; +`); modelSchema = modelScalarSchema; - // merge relations - modelSchema = makeMerge(modelSchema, makePartial(relationSchema)); - } + // merge fk fields + if (fkSchema) { + modelSchema = this.makeMerge(modelSchema, fkSchema); + } - // refine - if (refineFuncName) { - const noRefineSchema = `${upperCaseFirst(model.name)}WithoutRefineSchema`; - writer.writeLine(`export const ${noRefineSchema} = ${modelSchema};`); - modelSchema = `${refineFuncName}(${noRefineSchema})`; - } - writer.writeLine(`export const ${upperCaseFirst(model.name)}Schema = ${modelSchema};`); + // merge relation fields (all optional) + if (relationSchema) { + modelSchema = this.makeMerge(modelSchema, this.makePartial(relationSchema)); + } - //////////////////////////////////////////////// - // 2. Prisma create & update - //////////////////////////////////////////////// + // refine + if (refineFuncName) { + // export a schema without refinement for extensibility: `[Model]WithoutRefineSchema` + const noRefineSchema = `${upperCaseFirst(model.name)}WithoutRefineSchema`; + writer.writeLine(` +/** + * \`${model.name}\` schema prior to calling \`.refine()\` for extensibility. + */ +export const ${noRefineSchema} = ${modelSchema}; +`); + modelSchema = `${refineFuncName}(${noRefineSchema})`; + } - // schema for validating prisma create input (all fields optional) - let prismaCreateSchema = makePassthrough(makePartial('baseSchema')); - if (refineFuncName) { - prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`; - } - writer.writeLine(`export const ${upperCaseFirst(model.name)}PrismaCreateSchema = ${prismaCreateSchema};`); - - // schema for validating prisma update input (all fields optional) - // note numeric fields can be simple update or atomic operations - let prismaUpdateSchema = `z.object({ - ${scalarFields - .map((field) => { - let fieldSchema = makeFieldSchema(field); - if (field.type.type === 'Int' || field.type.type === 'Float') { - fieldSchema = `z.union([${fieldSchema}, z.record(z.unknown())])`; - } - return `\t${field.name}: ${fieldSchema}`; - }) - .join(',\n')} -})`; - prismaUpdateSchema = makePartial(prismaUpdateSchema); - if (refineFuncName) { - prismaUpdateSchema = `${refineFuncName}(${prismaUpdateSchema})`; - } - writer.writeLine(`export const ${upperCaseFirst(model.name)}PrismaUpdateSchema = ${prismaUpdateSchema};`); - - //////////////////////////////////////////////// - // 3. Create schema - //////////////////////////////////////////////// - let createSchema = 'baseSchema'; - const fieldsWithDefault = scalarFields.filter( - (field) => hasAttribute(field, '@default') || hasAttribute(field, '@updatedAt') || field.type.array - ); - if (fieldsWithDefault.length > 0) { - createSchema = makePartial( - createSchema, - fieldsWithDefault.map((f) => f.name) + // export the final model schema: `[Model]Schema` + writer.writeLine(` +/** + * \`${model.name}\` schema including all fields (scalar, foreign key, and relations) and validations. + */ +export const ${upperCaseFirst(model.name)}Schema = ${modelSchema}; +`); + + //////////////////////////////////////////////// + // 2. Prisma create & update + //////////////////////////////////////////////// + + // schema for validating prisma create input (all fields optional) + let prismaCreateSchema = this.makePassthrough(this.makePartial('baseSchema')); + if (refineFuncName) { + prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`; + } + writer.writeLine(` +/** + * Schema used for validating Prisma create input. For internal use only. + * @private + */ +export const ${upperCaseFirst(model.name)}PrismaCreateSchema = ${prismaCreateSchema}; +`); + + // schema for validating prisma update input (all fields optional) + // note numeric fields can be simple update or atomic operations + let prismaUpdateSchema = `z.object({ + ${scalarFields + .map((field) => { + let fieldSchema = makeFieldSchema(field); + if (field.type.type === 'Int' || field.type.type === 'Float') { + fieldSchema = `z.union([${fieldSchema}, z.record(z.unknown())])`; + } + return `\t${field.name}: ${fieldSchema}`; + }) + .join(',\n')} + })`; + prismaUpdateSchema = this.makePartial(prismaUpdateSchema); + if (refineFuncName) { + prismaUpdateSchema = `${refineFuncName}(${prismaUpdateSchema})`; + } + writer.writeLine( + ` +/** + * Schema used for validating Prisma update input. For internal use only. + * @private + */ +export const ${upperCaseFirst(model.name)}PrismaUpdateSchema = ${prismaUpdateSchema}; +` ); - } - if (fkSchema) { - // export schema with only scalar fields + //////////////////////////////////////////////// + // 3. Create schema + //////////////////////////////////////////////// + + let createSchema = 'baseSchema'; + const fieldsWithDefault = scalarFields.filter( + (field) => hasAttribute(field, '@default') || hasAttribute(field, '@updatedAt') || field.type.array + ); + + // mark fields with default as optional + if (fieldsWithDefault.length > 0) { + createSchema = this.makePartial( + createSchema, + fieldsWithDefault.map((f) => f.name) + ); + } + + // export schema with only scalar fields: `[Model]CreateScalarSchema` const createScalarSchema = `${upperCaseFirst(model.name)}CreateScalarSchema`; - writer.writeLine(`export const ${createScalarSchema} = ${createSchema};`); + writer.writeLine(` +/** + * \`${model.name}\` schema for create operations excluding foreign keys and relations. + */ +export const ${createScalarSchema} = ${createSchema}; +`); + + if (fkSchema) { + // merge fk fields + createSchema = this.makeMerge(createScalarSchema, fkSchema); + } - // merge fk fields - createSchema = makeMerge(createScalarSchema, fkSchema); - } + if (refineFuncName) { + // export a schema without refinement for extensibility: `[Model]CreateWithoutRefineSchema` + const noRefineSchema = `${upperCaseFirst(model.name)}CreateWithoutRefineSchema`; + writer.writeLine(` +/** + * \`${model.name}\` schema for create operations prior to calling \`.refine()\` for extensibility. + */ +export const ${noRefineSchema} = ${createSchema}; +`); + createSchema = `${refineFuncName}(${noRefineSchema})`; + } - if (refineFuncName) { - // export a schema without refinement for extensibility - const noRefineSchema = `${upperCaseFirst(model.name)}CreateWithoutRefineSchema`; - writer.writeLine(`export const ${noRefineSchema} = ${createSchema};`); - createSchema = `${refineFuncName}(${noRefineSchema})`; - } - writer.writeLine(`export const ${upperCaseFirst(model.name)}CreateSchema = ${createSchema};`); + // export the final create schema: `[Model]CreateSchema` + writer.writeLine(` +/** + * \`${model.name}\` schema for create operations including scalar fields, foreign key fields, and validations. + */ +export const ${upperCaseFirst(model.name)}CreateSchema = ${createSchema}; +`); + + //////////////////////////////////////////////// + // 3. Update schema + //////////////////////////////////////////////// - //////////////////////////////////////////////// - // 3. Update schema - //////////////////////////////////////////////// - let updateSchema = makePartial('baseSchema'); + // for update all fields are optional + let updateSchema = this.makePartial('baseSchema'); - if (fkSchema) { - // export schema with only scalar fields + // export schema with only scalar fields: `[Model]UpdateScalarSchema` const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; - writer.writeLine(`export const ${updateScalarSchema} = ${updateSchema};`); + writer.writeLine(` +/** + * \`${model.name}\` schema for update operations excluding foreign keys and relations. + */ +export const ${updateScalarSchema} = ${updateSchema}; +`); + updateSchema = updateScalarSchema; - // merge fk fields - updateSchema = makeMerge(updateSchema, makePartial(fkSchema)); - } + if (fkSchema) { + // merge fk fields + updateSchema = this.makeMerge(updateSchema, this.makePartial(fkSchema)); + } - if (refineFuncName) { - // export a schema without refinement for extensibility - const noRefineSchema = `${upperCaseFirst(model.name)}UpdateWithoutRefineSchema`; - writer.writeLine(`export const ${noRefineSchema} = ${updateSchema};`); - updateSchema = `${refineFuncName}(${noRefineSchema})`; - } - writer.writeLine(`export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema};`); - }); + if (refineFuncName) { + // export a schema without refinement for extensibility: `[Model]UpdateWithoutRefineSchema` + const noRefineSchema = `${upperCaseFirst(model.name)}UpdateWithoutRefineSchema`; + writer.writeLine(` +/** + * \`${model.name}\` schema for update operations prior to calling \`.refine()\` for extensibility. + */ +export const ${noRefineSchema} = ${updateSchema}; +`); + updateSchema = `${refineFuncName}(${noRefineSchema})`; + } - return schemaName; -} + // export the final update schema: `[Model]UpdateSchema` + writer.writeLine(` +/** + * \`${model.name}\` schema for update operations including scalar fields, foreign key fields, and validations. + */ +export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema}; +`); + }); + + return schemaName; + } -function makePartial(schema: string, fields?: string[]) { - if (fields) { - if (fields.length === 0) { - return schema; + private makePartial(schema: string, fields?: string[]) { + if (fields) { + if (fields.length === 0) { + return schema; + } else { + return `${schema}.partial({ + ${fields.map((f) => `${f}: true`).join(', ')} + })`; + } } else { - return `${schema}.partial({ - ${fields.map((f) => `${f}: true`).join(', ')} - })`; + return `${schema}.partial()`; } - } else { - return `${schema}.partial()`; } -} -function makeOmit(schema: string, fields: string[]) { - return `${schema}.omit({ - ${fields.map((f) => `${f}: true`).join(', ')}, - })`; -} + private makeOmit(schema: string, fields: string[]) { + return `${schema}.omit({ + ${fields.map((f) => `${f}: true`).join(', ')}, + })`; + } -function makeMerge(schema1: string, schema2: string): string { - return `${schema1}.merge(${schema2})`; -} + private makeMerge(schema1: string, schema2: string): string { + return `${schema1}.merge(${schema2})`; + } -function makePassthrough(schema: string) { - return `${schema}.passthrough()`; + private makePassthrough(schema: string) { + return `${schema}.passthrough()`; + } } diff --git a/packages/schema/src/plugins/zod/index.ts b/packages/schema/src/plugins/zod/index.ts index b2b43cb40..ffe198378 100644 --- a/packages/schema/src/plugins/zod/index.ts +++ b/packages/schema/src/plugins/zod/index.ts @@ -1,12 +1,14 @@ import { PluginFunction } from '@zenstackhq/sdk'; import invariant from 'tiny-invariant'; -import { generate } from './generator'; +import { ZodSchemaGenerator } from './generator'; export const name = 'Zod'; +export const description = 'Generating Zod schemas'; const run: PluginFunction = async (model, options, dmmf, globalOptions) => { invariant(dmmf); - return generate(model, options, dmmf, globalOptions); + const generator = new ZodSchemaGenerator(model, options, dmmf, globalOptions); + return generator.generate(); }; export default run; diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index e5066c1e4..86829f1ca 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,12 +1,9 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; -import { Model } from '@zenstackhq/language/ast'; -import { getPrismaClientImportSpec, getPrismaVersion } from '@zenstackhq/sdk'; +import { indentString, type PluginOptions } from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; -import { indentString } from '@zenstackhq/sdk/utils'; +import { getPrismaClientImportSpec, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import path from 'path'; -import * as semver from 'semver'; -import { Project } from 'ts-morph'; +import type { Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { AggregateOperationSupport, TransformerParams } from './types'; @@ -22,13 +19,12 @@ export default class Transformer { static enumNames: string[] = []; static rawOpsMap: { [name: string]: string } = {}; - static provider: string; private static outputPath = './generated'; private hasJson = false; private hasDecimal = false; private project: Project; - private zmodel: Model; - private inputObjectTypes: DMMF.InputType[]; + private inputObjectTypes: PrismaDMMF.InputType[]; + public sourceFiles: SourceFile[] = []; constructor(params: TransformerParams) { this.originalName = params.name ?? ''; @@ -39,7 +35,6 @@ export default class Transformer { this.aggregateOperationSupport = params.aggregateOperationSupport ?? {}; this.enumTypes = params.enumTypes ?? []; this.project = params.project; - this.zmodel = params.zmodel; this.inputObjectTypes = params.inputObjectTypes; } @@ -59,12 +54,17 @@ export default class Transformer { `${name}`, `z.enum(${JSON.stringify(enumType.values)})` )}`; - this.project.createSourceFile(filePath, content, { overwrite: true }); + this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); } - this.project.createSourceFile( - path.join(Transformer.outputPath, `enums/index.ts`), - this.enumTypes.map((enumType) => `export * from './${upperCaseFirst(enumType.name)}.schema';`).join('\n'), - { overwrite: true } + + this.sourceFiles.push( + this.project.createSourceFile( + path.join(Transformer.outputPath, `enums/index.ts`), + this.enumTypes + .map((enumType) => `export * from './${upperCaseFirst(enumType.name)}.schema';`) + .join('\n'), + { overwrite: true } + ) ); } @@ -76,13 +76,13 @@ export default class Transformer { return `export const ${name}Schema = ${schema}`; } - generateObjectSchema(generateUnchecked: boolean) { + generateObjectSchema(generateUnchecked: boolean, options: PluginOptions) { const zodObjectSchemaFields = this.generateObjectSchemaFields(generateUnchecked); - const objectSchema = this.prepareObjectSchema(zodObjectSchemaFields); + const objectSchema = this.prepareObjectSchema(zodObjectSchemaFields, options); const filePath = path.join(Transformer.outputPath, `objects/${this.name}.schema.ts`); const content = '/* eslint-disable */\n' + objectSchema; - this.project.createSourceFile(filePath, content, { overwrite: true }); + this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); return `${this.name}.schema`; } @@ -117,6 +117,8 @@ export default class Transformer { return result; } + // TODO: unify the following with `schema-gen.ts` + if (inputType.type === 'String') { result.push(this.wrapWithZodValidators('z.string()', field, inputType)); } else if (inputType.type === 'Int' || inputType.type === 'Float') { @@ -131,7 +133,13 @@ export default class Transformer { } else if (inputType.type === 'DateTime') { result.push(this.wrapWithZodValidators(['z.date()', 'z.string().datetime()'], field, inputType)); } else if (inputType.type === 'Bytes') { - result.push(this.wrapWithZodValidators(`z.instanceof(Uint8Array)`, field, inputType)); + result.push( + this.wrapWithZodValidators( + `z.custom(data => data instanceof Uint8Array)`, + field, + inputType + ) + ); } else if (inputType.type === 'Json') { this.hasJson = true; result.push(this.wrapWithZodValidators('jsonSchema', field, inputType)); @@ -184,7 +192,7 @@ export default class Transformer { wrapWithZodValidators( mainValidators: string | string[], field: PrismaDMMF.SchemaArg, - inputType: PrismaDMMF.SchemaArgInputType + inputType: PrismaDMMF.InputTypeRef ) { let line = ''; @@ -214,11 +222,7 @@ export default class Transformer { this.schemaImports.add(upperCaseFirst(name)); } - generatePrismaStringLine( - field: PrismaDMMF.SchemaArg, - inputType: PrismaDMMF.SchemaArgInputType, - inputsLength: number - ) { + generatePrismaStringLine(field: PrismaDMMF.SchemaArg, inputType: PrismaDMMF.InputTypeRef, inputsLength: number) { const isEnum = inputType.location === 'enumTypes'; const { isModelQueryType, modelName, queryName } = this.checkIsModelQueryType(inputType.type as string); @@ -254,12 +258,12 @@ export default class Transformer { return zodStringWithMainType; } - prepareObjectSchema(zodObjectSchemaFields: string[]) { + prepareObjectSchema(zodObjectSchemaFields: string[], options: PluginOptions) { const objectSchema = `${this.generateExportObjectSchemaStatement( this.addFinalWrappers({ zodStringFields: zodObjectSchemaFields }) )}\n`; - const prismaImportStatement = this.generateImportPrismaStatement(); + const prismaImportStatement = this.generateImportPrismaStatement(options); const json = this.generateJsonSchemaImplementation(); @@ -285,10 +289,10 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; return this.wrapWithZodObject(fields) + '.strict()'; } - generateImportPrismaStatement() { + generateImportPrismaStatement(options: PluginOptions) { const prismaClientImportPath = getPrismaClientImportSpec( - this.zmodel, - path.resolve(Transformer.outputPath, './objects') + path.resolve(Transformer.outputPath, './objects'), + options ); return `import type { Prisma } from '${prismaClientImportPath}';\n\n`; } @@ -384,9 +388,12 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; return wrapped; } - async generateInputSchemas(generateUnchecked: boolean) { + async generateInputSchemas(options: PluginOptions) { const globalExports: string[] = []; + // whether Prisma's Unchecked* series of input types should be generated + const generateUnchecked = options.noUncheckedInput !== true; + for (const modelOperation of this.modelOperations) { const { model: origModelName, @@ -421,7 +428,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; let imports = [ `import { z } from 'zod'`, - this.generateImportPrismaStatement(), + this.generateImportPrismaStatement(options), selectImport, includeImport, ]; @@ -563,10 +570,6 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; const aggregateOperations = []; - // DMMF messed up the model name casing used in the aggregate operations, - // AND the casing behavior varies from version to version -_-|| - const prismaVersion = getPrismaVersion(); - if (this.aggregateOperationSupport[modelName]?.count) { imports.push( `import { ${modelName}CountAggregateInputObjectSchema } from '../objects/${modelName}CountAggregateInput.schema'` @@ -624,12 +627,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; ', ' )} }),`; - // prisma 4 and 5 different typing for "groupBy" and we have to deal with it separately - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - operations.push(['groupBy', origModelName]); - } else { - operations.push(['groupBy', modelName]); - } + operations.push(['groupBy', origModelName]); } // count @@ -666,7 +664,7 @@ ${operations } as ${modelName}InputSchemaType; `; - this.project.createSourceFile(filePath, content, { overwrite: true }); + this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); } const indexFilePath = path.join(Transformer.outputPath, 'input/index.ts'); @@ -674,7 +672,7 @@ ${operations /* eslint-disable */ ${globalExports.join(';\n')} `; - this.project.createSourceFile(indexFilePath, indexContent, { overwrite: true }); + this.sourceFiles.push(this.project.createSourceFile(indexFilePath, indexContent, { overwrite: true })); } generateImportStatements(imports: (string | undefined)[]) { diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts index 72564c7ef..b64995448 100644 --- a/packages/schema/src/plugins/zod/types.ts +++ b/packages/schema/src/plugins/zod/types.ts @@ -1,5 +1,4 @@ -import { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; -import { Model } from '@zenstackhq/language/ast'; +import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import { Project } from 'ts-morph'; export type TransformerParams = { @@ -12,8 +11,7 @@ export type TransformerParams = { isDefaultPrismaClientOutput?: boolean; prismaClientOutputPath?: string; project: Project; - zmodel: Model; - inputObjectTypes: DMMF.InputType[]; + inputObjectTypes: PrismaDMMF.InputType[]; }; export type AggregateOperationSupport = { diff --git a/packages/schema/src/plugins/zod/utils/removeDir.ts b/packages/schema/src/plugins/zod/utils/removeDir.ts deleted file mode 100644 index 03f8d74f5..000000000 --- a/packages/schema/src/plugins/zod/utils/removeDir.ts +++ /dev/null @@ -1,15 +0,0 @@ -import path from 'path'; -import { promises as fs } from 'fs'; - -export default async function removeDir(dirPath: string, onlyContent: boolean) { - const dirEntries = await fs.readdir(dirPath, { withFileTypes: true }); - await Promise.all( - dirEntries.map(async (dirEntry) => { - const fullPath = path.join(dirPath, dirEntry.name); - return dirEntry.isDirectory() ? await removeDir(fullPath, false) : await fs.unlink(fullPath); - }) - ); - if (!onlyContent) { - await fs.rmdir(dirPath); - } -} diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index c660ad606..e6a335221 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -1,6 +1,8 @@ import { ExpressionContext, PluginError, + TypeScriptExpressionTransformer, + TypeScriptExpressionTransformerError, getAttributeArg, getAttributeArgLiteral, getLiteral, @@ -19,12 +21,9 @@ import { } from '@zenstackhq/sdk/ast'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '..'; -import { - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '../../../utils/typescript-expression-transformer'; +import { isDefaultWithAuth } from '../../enhancer/enhancer-utils'; -export function makeFieldSchema(field: DataModelField, respectDefault = false) { +export function makeFieldSchema(field: DataModelField) { if (isDataModel(field.type.reference?.ref)) { if (field.type.array) { // array field is always optional @@ -141,15 +140,20 @@ export function makeFieldSchema(field: DataModelField, respectDefault = false) { } } - if (respectDefault) { + if (field.attributes.some(isDefaultWithAuth)) { + // field uses `auth()` in `@default()`, this was transformed into a pseudo default + // value, while compiling to zod we should turn it into an optional field instead + // of `.default()` + schema += '.nullish()'; + } else { const schemaDefault = getFieldSchemaDefault(field); - if (schemaDefault) { + if (schemaDefault !== undefined) { schema += `.default(${schemaDefault})`; } - } - if (field.type.optional) { - schema += '.nullish()'; + if (field.type.optional) { + schema += '.nullish()'; + } } return schema; @@ -182,7 +186,7 @@ function makeZodSchema(field: DataModelField) { schema = 'z.coerce.date()'; break; case 'Bytes': - schema = 'z.union([z.string(), z.instanceof(Uint8Array)])'; + schema = 'z.union([z.string(), z.custom(data => data instanceof Uint8Array)])'; break; default: schema = 'z.any()'; diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 145ffed60..266b5c517 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -76,7 +76,7 @@ function env(name: String): String { * Gets the current login user. */ function auth(): Any { -} @@@expressionContext([AccessPolicy]) +} @@@expressionContext([DefaultValue, AccessPolicy]) /** * Gets current date-time (as DateTime type). @@ -207,7 +207,7 @@ attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) /** * Defines a default value for a field. - * @param value: An expression (e.g. 5, true, now()). + * @param value: An expression (e.g. 5, true, now(), auth()). */ attribute @default(_ value: ContextType, map: String?) @@@prisma @@ -680,6 +680,11 @@ attribute @prisma.passthrough(_ text: String) */ attribute @@prisma.passthrough(_ text: String) +/** + * Marks a model to be a delegate. Used for implementing polymorphism. + */ +attribute @@delegate(_ discriminator: FieldReference) + /** * Used for specifying operator classes for GIN index. */ diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index 9cd8ba386..9ecbb4672 100644 --- a/packages/schema/src/telemetry.ts +++ b/packages/schema/src/telemetry.ts @@ -1,5 +1,5 @@ import { createId } from '@paralleldrive/cuid2'; -import { getPrismaVersion } from '@zenstackhq/sdk'; +import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; import exitHook from 'async-exit-hook'; import { CommanderError } from 'commander'; import { init, Mixpanel } from 'mixpanel'; @@ -111,18 +111,18 @@ export class Telemetry { } } - async trackSpan( + async trackSpan( startEvent: TelemetryEvents, completeEvent: TelemetryEvents, errorEvent: TelemetryEvents, properties: Record, - action: () => Promise | void + action: () => Promise | T ) { this.track(startEvent, properties); const start = Date.now(); let success = true; try { - await Promise.resolve(action()); + return await action(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { this.track(errorEvent, { diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index c752c837d..3a255228e 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -1,8 +1,10 @@ import { BinaryExpr, DataModel, + DataModelAttribute, DataModelField, Expression, + InheritableNode, isArrayExpr, isBinaryExpr, isDataModel, @@ -15,11 +17,22 @@ import { ModelImport, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { isFromStdlib } from '@zenstackhq/sdk'; -import { AstNode, getDocument, LangiumDocuments, Mutable } from 'langium'; +import { isDelegateModel, isFromStdlib } from '@zenstackhq/sdk'; +import { + AstNode, + copyAstNode, + CstNode, + getContainerOfType, + getDocument, + LangiumDocuments, + linkContentToContainer, + Linker, + Mutable, + Reference, +} from 'langium'; +import { isAbsolute } from 'node:path'; import { URI, Utils } from 'vscode-uri'; import { findNodeModulesFile } from './pkg-utils'; -import {isAbsolute} from 'node:path' export function extractDataModelsWithAllowRules(model: Model): DataModel[] { return model.declarations.filter( @@ -27,39 +40,82 @@ export function extractDataModelsWithAllowRules(model: Model): DataModel[] { ) as DataModel[]; } -export function mergeBaseModel(model: Model) { - model.declarations - .filter((x) => x.$type === 'DataModel') - .forEach((decl) => { - const dataModel = decl as DataModel; +type BuildReference = ( + node: AstNode, + property: string, + refNode: CstNode | undefined, + refText: string +) => Reference; + +export function mergeBaseModel(model: Model, linker: Linker) { + const buildReference = linker.buildReference.bind(linker); - dataModel.fields = dataModel.superTypes + model.declarations.filter(isDataModel).forEach((decl) => { + const dataModel = decl as DataModel; + + const bases = getRecursiveBases(dataModel).reverse(); + if (bases.length > 0) { + dataModel.fields = bases // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .flatMap((superType) => updateContainer(superType.ref!.fields, dataModel)) + .flatMap((base) => base.fields) + // don't inherit skip-level fields + .filter((f) => !f.$inheritedFrom) + .map((f) => cloneAst(f, dataModel, buildReference)) .concat(dataModel.fields); - dataModel.attributes = dataModel.superTypes - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .flatMap((superType) => updateContainer(superType.ref!.attributes, dataModel)) + dataModel.attributes = bases + .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(base, attr))) + .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); - }); + + // fix $containerIndex + linkContentToContainer(dataModel); + } + + dataModel.$baseMerged = true; + }); // remove abstract models - model.declarations = model.declarations.filter((x) => !(x.$type == 'DataModel' && x.isAbstract)); + model.declarations = model.declarations.filter((x) => !(isDataModel(x) && x.isAbstract)); } -function updateContainer(nodes: T[], container: AstNode): Mutable[] { - return nodes.map((node) => { - const cloneField = Object.assign({}, node); - const mutable = cloneField as Mutable; - // update container - mutable.$container = container; - return mutable; - }); +function filterBaseAttribute(base: DataModel, attr: DataModelAttribute) { + if (attr.$inheritedFrom) { + // don't inherit from skip-level base + return false; + } + + // uninheritable attributes for all inheritance + const uninheritableAttributes = ['@@delegate', '@@map']; + + // uninheritable attributes for delegate inheritance (they reference fields from the base) + const uninheritableFromDelegateAttributes = ['@@unique', '@@index', '@@fulltext']; + + if (uninheritableAttributes.includes(attr.decl.$refText)) { + return false; + } + + if (isDelegateModel(base) && uninheritableFromDelegateAttributes.includes(attr.decl.$refText)) { + return false; + } + + return true; +} + +// deep clone an AST, relink references, and set its container +function cloneAst( + node: T, + newContainer: AstNode, + buildReference: BuildReference +): Mutable { + const clone = copyAstNode(node, buildReference) as Mutable; + clone.$container = newContainer; + clone.$inheritedFrom = node.$inheritedFrom ?? getContainerOfType(node, isDataModel); + return clone; } export function getIdFields(dataModel: DataModel) { - const fieldLevelId = dataModel.$resolvedFields.find((f) => + const fieldLevelId = getModelFieldsWithBases(dataModel).find((f) => f.attributes.some((attr) => attr.decl.$refText === '@id') ); if (fieldLevelId) { @@ -69,7 +125,7 @@ export function getIdFields(dataModel: DataModel) { const modelIdAttr = dataModel.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); if (modelIdAttr) { // get fields referenced in the attribute: @@id([field1, field2]]) - if (!isArrayExpr(modelIdAttr.args[0].value)) { + if (!isArrayExpr(modelIdAttr.args[0]?.value)) { return []; } const argValue = modelIdAttr.args[0].value; @@ -85,6 +141,10 @@ export function isAuthInvocation(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); } +export function isFutureInvocation(node: AstNode) { + return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); +} + export function getDataModelFieldReference(expr: Expression): DataModelField | undefined { if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) { return expr.target.ref; @@ -103,8 +163,8 @@ export function resolveImportUri(imp: ModelImport): URI | undefined { } if ( - !imp.path.startsWith('.') // Respect relative paths - && !isAbsolute(imp.path) // Respect Absolute paths + !imp.path.startsWith('.') && // Respect relative paths + !isAbsolute(imp.path) // Respect Absolute paths ) { imp.path = findNodeModulesFile(imp.path) ?? imp.path; } @@ -156,11 +216,15 @@ export function resolveImport(documents: LangiumDocuments, imp: ModelImport): Mo return undefined; } -export function getAllDeclarationsFromImports(documents: LangiumDocuments, model: Model) { +export function getAllDeclarationsIncludingImports(documents: LangiumDocuments, model: Model) { const imports = resolveTransitiveImports(documents, model); return model.declarations.concat(...imports.map((imp) => imp.declarations)); } +export function getAllDataModelsIncludingImports(documents: LangiumDocuments, model: Model) { + return getAllDeclarationsIncludingImports(documents, model).filter(isDataModel); +} + export function isCollectionPredicate(node: AstNode): node is BinaryExpr { return isBinaryExpr(node) && ['?', '!', '^'].includes(node.operator); } @@ -176,6 +240,29 @@ export function getContainingDataModel(node: Expression): DataModel | undefined return undefined; } +export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) { + if (model.$baseMerged) { + return model.fields; + } else { + return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)]; + } +} + +export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): DataModel[] { + const result: DataModel[] = []; + dataModel.superTypes.forEach((superType) => { + const baseDecl = superType.ref; + if (baseDecl) { + if (!includeDelegate && isDelegateModel(baseDecl)) { + return; + } + result.push(baseDecl); + result.push(...getRecursiveBases(baseDecl)); + } + }); + return result; +} + /** * Walk upward from the current AST node to find the first node that satisfies the predicate. */ diff --git a/packages/schema/src/utils/exec-utils.ts b/packages/schema/src/utils/exec-utils.ts index 8f0508dbb..03060a4f9 100644 --- a/packages/schema/src/utils/exec-utils.ts +++ b/packages/schema/src/utils/exec-utils.ts @@ -1,18 +1,18 @@ -import { execSync as _exec, StdioOptions } from 'child_process'; +import { execSync as _exec, ExecSyncOptions } from 'child_process'; /** * Utility for executing command synchronously and prints outputs on current console */ -export function execSync(cmd: string, stdio: StdioOptions = 'inherit', env?: Record): void { - const mergedEnv = { ...process.env, ...env }; - _exec(cmd, { encoding: 'utf-8', stdio, env: mergedEnv }); +export function execSync(cmd: string, options?: Omit & { env?: Record }): void { + const { env, ...restOptions } = options ?? {}; + const mergedEnv = env ? { ...process.env, ...env } : undefined; + _exec(cmd, { encoding: 'utf-8', stdio: options?.stdio ?? 'inherit', env: mergedEnv, ...restOptions }); } /** * Utility for running package commands through npx/bunx */ -export function execPackage(cmd: string, stdio: StdioOptions = 'inherit', env?: Record): void { +export function execPackage(cmd: string, options?: Omit & { env?: Record }): void { const packageManager = process?.versions?.bun ? 'bunx' : 'npx'; - const mergedEnv = { ...process.env, ...env }; - _exec(`${packageManager} ${cmd}`, { encoding: 'utf-8', stdio, env: mergedEnv }); + execSync(`${packageManager} ${cmd}`, options) } \ No newline at end of file diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index 99593dcd3..82b5ef019 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { execSync } from './exec-utils'; +import { match } from 'ts-pattern'; export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; @@ -61,23 +62,22 @@ export function findNodeModulesFile(name: string, cwd: string = process.cwd()) { } } -function getPackageManager(projectPath = '.'): PackageManagers { - const lockFile = findUp(['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'], projectPath); +export function getPackageManager(searchStartPath = '.') { + const lockFile = findUp(['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'], searchStartPath); if (!lockFile) { // default use npm - return 'npm'; + return { packageManager: 'npm', lockFile: undefined, projectRoot: searchStartPath }; } - switch (path.basename(lockFile)) { - case 'yarn.lock': - return 'yarn'; - case 'pnpm-lock.yaml': - return 'pnpm'; - default: - return 'npm'; - } + const packageManager = match(path.basename(lockFile)) + .with('yarn.lock', () => 'yarn') + .with('pnpm-lock.yaml', () => 'pnpm') + .otherwise(() => 'npm'); + + return { packageManager, lockFile, projectRoot: path.dirname(lockFile) }; } + export function installPackage( pkg: string, dev: boolean, diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index 13f69fd89..5820e92a8 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -3,7 +3,7 @@ import { DataModel, Enum, Expression, isDataModel, isEnum } from '@zenstackhq/language/ast'; import * as tmp from 'tmp'; import { Project, VariableDeclarationKind } from 'ts-morph'; -import { ExpressionWriter } from '../../src/plugins/access-policy/expression-writer'; +import { ExpressionWriter } from '../../src/plugins/enhancer/policy/expression-writer'; import { loadModel } from '../utils'; tmp.setGracefulCleanup(); diff --git a/packages/schema/tests/generator/prisma-builder.test.ts b/packages/schema/tests/generator/prisma-builder.test.ts index a3944401c..8144110de 100644 --- a/packages/schema/tests/generator/prisma-builder.test.ts +++ b/packages/schema/tests/generator/prisma-builder.test.ts @@ -1,4 +1,4 @@ -import { getDMMF } from '@zenstackhq/sdk'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; import { AttributeArg, AttributeArgValue, diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 0e8987703..c91034d1d 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -1,16 +1,33 @@ /// -import { getDMMF } from '@zenstackhq/sdk'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; import { loadDocument } from '../../src/cli/cli-util'; -import PrismaSchemaGenerator from '../../src/plugins/prisma/schema-generator'; +import { PrismaSchemaGenerator } from '../../src/plugins/prisma/schema-generator'; +import { execSync } from '../../src/utils/exec-utils'; import { loadModel } from '../utils'; tmp.setGracefulCleanup(); describe('Prisma generator test', () => { + let origDir: string; + + beforeEach(() => { + origDir = process.cwd(); + const r = tmp.dirSync({ unsafeCleanup: true }); + console.log(`Project dir: ${r.name}`); + process.chdir(r.name); + + execSync('npm init -y', { stdio: 'ignore' }); + execSync('npm install prisma'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + it('datasource coverage', async () => { const model = await loadModel(` datasource db { @@ -19,7 +36,7 @@ describe('Prisma generator test', () => { directUrl = env("DATABASE_URL") shadowDatabaseUrl = env("DATABASE_URL") extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")] - schemas = ["auth", "public"] + schemas = ["auth", "public"] } generator client { @@ -27,6 +44,10 @@ describe('Prisma generator test', () => { previewFeatures = ["multiSchema", "postgresqlExtensions"] } + plugin prisma { + provider = '@core/prisma' + } + model User { id String @id @@ -34,15 +55,15 @@ describe('Prisma generator test', () => { } `); - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', - output: name, + output: 'schema.prisma', + format: false, }); - const content = fs.readFileSync(name, 'utf-8'); + const content = fs.readFileSync('schema.prisma', 'utf-8'); expect(content).toContain('provider = "postgresql"'); expect(content).toContain('url = env("DATABASE_URL")'); expect(content).toContain('directUrl = env("DATABASE_URL")'); @@ -76,11 +97,12 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, + format: false, }); const content = fs.readFileSync(name, 'utf-8'); @@ -109,11 +131,12 @@ describe('Prisma generator test', () => { id String @id @default(nanoid(6)) x String @default(nanoid()) y String @default(dbgenerated("gen_random_uuid()")) + z String @default(auth().id) } `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -122,12 +145,12 @@ describe('Prisma generator test', () => { }); const content = fs.readFileSync(name, 'utf-8'); - // "nanoid()" is only available in later versions of Prisma - await getDMMF({ datamodel: content }, '5.0.0'); + await getDMMF({ datamodel: content }); expect(content).toContain('@default(nanoid(6))'); expect(content).toContain('@default(nanoid())'); expect(content).toContain('@default(dbgenerated("gen_random_uuid()"))'); + expect(content).not.toContain('@default(auth().id)'); }); it('triple slash comments', async () => { @@ -146,7 +169,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -178,7 +201,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -214,7 +237,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -254,7 +277,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -305,12 +328,13 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, generateClient: false, + format: false, }); const content = fs.readFileSync(name, 'utf-8'); @@ -341,13 +365,14 @@ describe('Prisma generator test', () => { } `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, generateClient: false, }); + console.log('Generated:', name); const content = fs.readFileSync(name, 'utf-8'); const dmmf = await getDMMF({ datamodel: content }); @@ -356,16 +381,14 @@ describe('Prisma generator test', () => { const post = dmmf.datamodel.models[0]; expect(post.name).toBe('Post'); expect(post.fields.length).toBe(5); - expect(post.fields[0].name).toBe('id'); - expect(post.fields[3].name).toBe('title'); - expect(post.fields[4].name).toBe('published'); + expect(post.fields.map((f) => f.name)).toEqual(expect.arrayContaining(['id', 'title', 'published'])); }); it('abstract multi files', async () => { const model = await loadDocument(path.join(__dirname, './zmodel/schema.zmodel')); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -415,7 +438,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -446,7 +469,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -481,7 +504,7 @@ describe('Prisma generator test', () => { `); const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', diff --git a/packages/schema/tests/schema/abstract.test.ts b/packages/schema/tests/schema/abstract.test.ts index 47d607962..6a4b69e49 100644 --- a/packages/schema/tests/schema/abstract.test.ts +++ b/packages/schema/tests/schema/abstract.test.ts @@ -61,4 +61,24 @@ describe('Abstract Schema Tests', () => { `); }); + + it('multiple id fields from base', async () => { + await loadModel(` + abstract model Base { + id1 String + id2 String + value String + + @@id([id1, id2]) + } + + model Item1 extends Base { + x String + } + + model Item2 extends Base { + y String + } + `); + }); }); diff --git a/packages/schema/tests/schema/cal-com.zmodel b/packages/schema/tests/schema/cal-com.zmodel index c6e874304..a32bd45a6 100644 --- a/packages/schema/tests/schema/cal-com.zmodel +++ b/packages/schema/tests/schema/cal-com.zmodel @@ -11,13 +11,8 @@ generator client { previewFeatures = [] } -plugin meta { - provider = '@core/model-meta' - output = '.zenstack' -} - -plugin policy { - provider = '@core/access-policy' +plugin enhancer { + provider = '@core/enhancer' output = '.zenstack' } diff --git a/packages/schema/tests/schema/formatter.test.ts b/packages/schema/tests/schema/formatter.test.ts index 35b08707d..24917436d 100644 --- a/packages/schema/tests/schema/formatter.test.ts +++ b/packages/schema/tests/schema/formatter.test.ts @@ -25,7 +25,7 @@ plugin swrHooks { output = 'lib/hooks' } model User { - id String @id + id String @id name String? } enum Role { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index eb8a6065b..aca9e2674 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1028,6 +1028,20 @@ describe('Attribute tests', () => { }); it('auth function check', async () => { + await loadModel(` + ${prelude} + + model User { + id String @id + name String + } + model B { + id String @id + userId String @default(auth().id) + userName String @default(auth().name) + } + `); + expect( await loadModelWithError(` ${prelude} @@ -1067,7 +1081,7 @@ describe('Attribute tests', () => { @@allow('all', auth().email != null) } `) - ).toContain(`expression cannot be resolved`); + ).toContain(`Could not resolve reference to DataModelField named 'email'.`); }); it('collection predicate expression check', async () => { @@ -1078,11 +1092,14 @@ describe('Attribute tests', () => { model A { id String @id x Int + b B @relation(references: [id], fields: [bId]) + bId String @unique } model B { id String @id - a A + a A? + aId String @unique @@allow('all', a?[x > 0]) } `) @@ -1144,15 +1161,6 @@ describe('Attribute tests', () => { }); it('incorrect function expression context', async () => { - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id @default(auth()) - } - `) - ).toContain('function "auth" is not allowed in the current context: DefaultValue'); - expect( await loadModelWithError(` ${prelude} diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 78e31204d..0bf12245d 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -10,13 +10,13 @@ describe('Data Model Validation Tests', () => { it('duplicated fields', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { id String @id x Int x String } - `) + `); expect(result).toMatchObject(errorLike('Duplicated declaration name "x"')); }); @@ -78,6 +78,29 @@ describe('Data Model Validation Tests', () => { ).toMatchObject(errorLike('Field of "Unsupported" type cannot be used in expressions')); }); + it('Using `this` in collection predicate', async () => { + expect( + await safelyLoadModel(` + ${prelude} + model User { + id String @id + members User[] + @@allow('all', members?[this == auth()]) + } + `) + ).toMatchObject(errorLike('using `this` in collection predicate is not supported')); + + expect( + await loadModel(` + model User { + id String @id + members User[] + @@allow('all', members?[id == auth().id]) + } + `) + ).toBeTruthy(); + }); + it('mix array and optional', async () => { expect( await safelyLoadModel(` @@ -128,14 +151,14 @@ describe('Data Model Validation Tests', () => { it('should error when there are no unique fields', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int @@allow('all', x > 0) } - `) + `); expect(result).toMatchObject(errorLike(err)); - }) + }); it('should should use @unique when there is no @id', async () => { const result = await safelyLoadModel(` @@ -147,12 +170,12 @@ describe('Data Model Validation Tests', () => { } `); expect(result).toMatchObject({ status: 'fulfilled' }); - }) + }); // @@unique used as id it('should suceed when @@unique used as id', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int @@unique([x]) @@ -160,11 +183,11 @@ describe('Data Model Validation Tests', () => { } `); expect(result).toMatchObject({ status: 'fulfilled' }); - }) + }); it('should succeed when @id is an enum type', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} enum E { A B @@ -174,11 +197,11 @@ describe('Data Model Validation Tests', () => { } `); expect(result).toMatchObject({ status: 'fulfilled' }); - }) + }); it('should succeed when @@id is an enum type', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} enum E { A B @@ -190,133 +213,135 @@ describe('Data Model Validation Tests', () => { } `); expect(result).toMatchObject({ status: 'fulfilled' }); - }) + }); it('should error when there are no id fields, even when denying access', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int @@deny('all', x <= 0) } - `) - + `); + expect(result).toMatchObject(errorLike(err)); - }) + }); it('should error when there are not id fields, without access restrictions', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int @gt(0) } - `) - + `); + expect(result).toMatchObject(errorLike(err)); - }) + }); it('should error when there is more than one field marked as @id', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int @id y Int @id } - `) - expect(result).toMatchObject(errorLike(`Model can include at most one field with @id attribute`)) - }) + `); + expect(result).toMatchObject(errorLike(`Model can include at most one field with @id attribute`)); + }); - it('should error when both @id and @@id are used', async () => { + it('should error when both @id and @@id are used', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int @id y Int @@id([x, y]) } - `) - expect(result).toMatchObject(errorLike(`Model cannot have both field-level @id and model-level @@id attributes`)) - }) + `); + expect(result).toMatchObject( + errorLike(`Model cannot have both field-level @id and model-level @@id attributes`) + ); + }); it('should error when @id used on optional field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int? @id } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)); + }); it('should error when @@id used on optional field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int? @@id([x]) } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)); + }); it('should error when @id used on list field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int[] @id } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); it('should error when @@id used on list field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Int[] @@id([x]) } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); it('should error when @id used on a Json field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Json @id } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); it('should error when @@id used on a Json field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { x Json @@id([x]) } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); it('should error when @id used on a reference field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model Id { id String @id } model M { myId Id @id } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); it('should error when @@id used on a reference field', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model Id { id String @id } @@ -324,9 +349,9 @@ describe('Data Model Validation Tests', () => { myId Id @@id([myId]) } - `) - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) - }) + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); }); it('relation', async () => { @@ -390,7 +415,9 @@ describe('Data Model Validation Tests', () => { id String @id } `) - ).toMatchObject(errorLike(`The relation field "b" on model "A" is missing an opposite relation field on model "B"`)); + ).toMatchObject( + errorLike(`The relation field "b" on model "A" is missing an opposite relation field on model "B"`) + ); // one-to-one ambiguous expect( @@ -492,7 +519,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toMatchObject(errorLike(`Field "aId" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute`)); + ).toMatchObject( + errorLike( + `Field "aId" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute` + ) + ); // missing @relation expect( @@ -508,7 +539,11 @@ describe('Data Model Validation Tests', () => { a A } `) - ).toMatchObject(errorLike(`Field for one side of relation must carry @relation attribute with both "fields" and "references" fields`)); + ).toMatchObject( + errorLike( + `Field for one side of relation must carry @relation attribute with both "fields" and "references" fields` + ) + ); // wrong relation owner field type expect( @@ -672,7 +707,9 @@ describe('Data Model Validation Tests', () => { } `); - expect(errors).toMatchObject(errorLike(`Model A cannot be extended because it's not abstract`)); + expect(errors).toMatchObject( + errorLike(`Model A cannot be extended because it's neither abstract nor marked as "@@delegate"`) + ); // relation incomplete from multiple level inheritance expect( @@ -696,6 +733,32 @@ describe('Data Model Validation Tests', () => { a String } `) - ).toMatchObject(errorLike(`The relation field "user" on model "A" is missing an opposite relation field on model "User"`)); + ).toMatchObject( + errorLike(`The relation field "user" on model "A" is missing an opposite relation field on model "User"`) + ); + }); + + it('delegate base type', async () => { + const errors = await safelyLoadModel(` + ${prelude} + + model Base1 { + id String @id + type String + @@delegate(type) + } + + model Base2 { + id String @id + type String + @@delegate(type) + } + + model A extends Base1,Base2 { + a String + } + `); + + expect(errors).toMatchObject(errorLike(`Extending from multiple delegate models is not supported`)); }); }); diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index 2746a3362..0c92c6ee2 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -32,7 +32,7 @@ export class SchemaLoadingError extends export async function loadModel(content: string, validate = true, verbose = true, mergeBase = true) { const { name: docPath } = tmp.fileSync({ postfix: '.zmodel' }); fs.writeFileSync(docPath, content); - const { shared } = createZModelServices(NodeFileSystem); + const { shared, ZModel } = createZModelServices(NodeFileSystem); const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( URI.file(path.resolve(__dirname, '../../schema/src/res/stdlib.zmodel')) ); @@ -68,7 +68,7 @@ export async function loadModel(content: string, validate = true, verbose = true const model = (await doc.parseResult.value) as Model; if (mergeBase) { - mergeBaseModel(model); + mergeBaseModel(model, ZModel.references.Linker); } return model; @@ -87,13 +87,13 @@ export async function loadModelWithError(content: string, verbose = false) { } export async function safelyLoadModel(content: string, validate = true, verbose = false) { - const [result] = await Promise.allSettled([loadModel(content, validate, verbose)]); + const [ result ] = await Promise.allSettled([ loadModel(content, validate, verbose) ]); - return result; + return result } export const errorLike = (msg: string) => ({ reason: { - message: expect.stringContaining(msg), + message: expect.stringContaining(msg) }, -}); +}) diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/sdk/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 51716a4a6..82aba8ad5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.12.4", + "version": "2.0.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { @@ -18,18 +18,36 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", - "@prisma/internals": "^4.16.0", - "@prisma/internals-v5": "npm:@prisma/internals@^5.0.0", + "@prisma/generator-helper": "5.7.0", + "@prisma/internals": "5.7.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", + "langium": "1.3.1", "lower-case-first": "^2.0.2", - "prettier": "^2.8.3 || 3.x", "semver": "^7.5.2", "ts-morph": "^16.0.0", + "ts-pattern": "^4.3.0", "upper-case-first": "^2.0.2" }, "devDependencies": { "@types/semver": "^7.3.13" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "./ast": { + "types": "./ast.d.ts", + "default": "./ast.js" + }, + "./prisma": { + "types": "./prisma.d.ts", + "default": "./prisma.js" + }, + "./dmmf-helpers": { + "types": "./dmmf-helpers/index.d.ts", + "default": "./dmmf-helpers/index.js" + } } } diff --git a/packages/sdk/src/code-gen.ts b/packages/sdk/src/code-gen.ts index 6039c2f25..3f80e7e4d 100644 --- a/packages/sdk/src/code-gen.ts +++ b/packages/sdk/src/code-gen.ts @@ -1,29 +1,6 @@ -import prettier from 'prettier'; -import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget, SourceFile } from 'ts-morph'; +import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget } from 'ts-morph'; import { PluginError } from './types'; -const formatOptions = { - trailingComma: 'all', - tabWidth: 4, - printWidth: 120, - bracketSpacing: true, - semi: true, - singleQuote: true, - useTabs: false, - parser: 'typescript', -} as const; - -async function formatFile(sourceFile: SourceFile) { - try { - const content = sourceFile.getFullText(); - const formatted = await prettier.format(content, formatOptions); - sourceFile.replaceWithText(formatted); - await sourceFile.save(); - } catch { - /* empty */ - } -} - /** * Creates a TS code generation project */ @@ -46,11 +23,7 @@ export function createProject(options?: CompilerOptions) { * Persists a TS project to disk. */ export async function saveProject(project: Project) { - await Promise.all( - project.getSourceFiles().map(async (sf) => { - await formatFile(sf); - }) - ); + project.getSourceFiles().forEach((sf) => sf.formatText()); await project.save(); } diff --git a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts index 662f0b8a4..bec9632a1 100644 --- a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts @@ -1,5 +1,5 @@ -import type { DMMF } from '@prisma/generator-helper'; import { upperCaseFirst } from 'upper-case-first'; +import type { DMMF } from '../prisma'; import { AggregateOperationSupport } from './types'; const isAggregateOutputType = (name: string) => /(?:Count|Avg|Sum|Min|Max)AggregateOutputType$/.test(name); diff --git a/packages/sdk/src/dmmf-helpers/include-helpers.ts b/packages/sdk/src/dmmf-helpers/include-helpers.ts index 2f9bbf478..c09c72426 100644 --- a/packages/sdk/src/dmmf-helpers/include-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/include-helpers.ts @@ -1,5 +1,5 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { checkIsModelRelationField, checkModelHasModelRelation, checkModelHasManyModelRelation } from './model-helpers'; +import type { DMMF } from '../prisma'; +import { checkIsModelRelationField, checkModelHasManyModelRelation, checkModelHasModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForInclude(inputObjectTypes: DMMF.InputType[], models: DMMF.Model[]) { // generate input object types necessary to support ModelInclude with relation support @@ -52,7 +52,7 @@ function generateModelIncludeInputObjectTypes(models: DMMF.Model[]) { const shouldAddCountField = hasManyRelationToAnotherModel; if (shouldAddCountField) { - const inputTypes: DMMF.SchemaArgInputType[] = [{ isList: false, type: 'Boolean', location: 'scalar' }]; + const inputTypes: DMMF.InputTypeRef[] = [{ isList: false, type: 'Boolean', location: 'scalar' }]; inputTypes.push({ isList: false, type: `${modelName}CountOutputTypeArgs`, diff --git a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts b/packages/sdk/src/dmmf-helpers/missing-types-helper.ts index e88f56db4..dcdff8684 100644 --- a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts +++ b/packages/sdk/src/dmmf-helpers/missing-types-helper.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '@prisma/generator-helper'; +import type { DMMF } from '../prisma'; import { addMissingInputObjectTypesForAggregate } from './aggregate-helpers'; import { addMissingInputObjectTypesForInclude } from './include-helpers'; import { addMissingInputObjectTypesForModelArgs } from './modelArgs-helpers'; diff --git a/packages/sdk/src/dmmf-helpers/model-helpers.ts b/packages/sdk/src/dmmf-helpers/model-helpers.ts index 902bd5401..62bbd9980 100644 --- a/packages/sdk/src/dmmf-helpers/model-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/model-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '@prisma/generator-helper'; +import type { DMMF } from '../prisma'; export function checkModelHasModelRelation(model: DMMF.Model) { const { fields: modelFields } = model; diff --git a/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts b/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts index 549dec3ab..79b3a9f98 100644 --- a/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '@prisma/generator-helper'; +import type { DMMF } from '../prisma'; import { checkModelHasModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForModelArgs(inputObjectTypes: DMMF.InputType[], models: DMMF.Model[]) { diff --git a/packages/sdk/src/dmmf-helpers/select-helpers.ts b/packages/sdk/src/dmmf-helpers/select-helpers.ts index 36403e61a..6037eecd8 100644 --- a/packages/sdk/src/dmmf-helpers/select-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/select-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '@prisma/generator-helper'; +import type { DMMF } from '../prisma'; import { checkIsModelRelationField, checkModelHasManyModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForSelect( @@ -108,7 +108,7 @@ function generateModelSelectInputObjectTypes(models: DMMF.Model[]) { }; if (isRelationField) { - const schemaArgInputType: DMMF.SchemaArgInputType = { + const schemaArgInputType: DMMF.InputTypeRef = { isList: false, type: isList ? `${type}FindManyArgs` : `${type}Args`, location: 'inputObjectTypes', diff --git a/packages/sdk/src/dmmf-helpers/types.ts b/packages/sdk/src/dmmf-helpers/types.ts index a02b9ca7c..a1fbc6c59 100644 --- a/packages/sdk/src/dmmf-helpers/types.ts +++ b/packages/sdk/src/dmmf-helpers/types.ts @@ -1,11 +1,11 @@ -import { DMMF as PrismaDMMF } from '@prisma/generator-helper'; +import type { DMMF } from '../prisma'; export type TransformerParams = { - enumTypes?: PrismaDMMF.SchemaEnum[]; - fields?: PrismaDMMF.SchemaArg[]; + enumTypes?: DMMF.SchemaEnum[]; + fields?: DMMF.SchemaArg[]; name?: string; - models?: PrismaDMMF.Model[]; - modelOperations?: PrismaDMMF.ModelMapping[]; + models?: DMMF.Model[]; + modelOperations?: DMMF.ModelMapping[]; aggregateOperationSupport?: AggregateOperationSupport; isDefaultPrismaClientOutput?: boolean; prismaClientOutputPath?: string; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 64060390e..3c89805d9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,8 +2,8 @@ export * from './code-gen'; export * from './constants'; export { generate as generateModelMeta } from './model-meta-generator'; export * from './policy'; -export * from './prisma'; export * from './types'; +export * from './typescript-expression-transformer'; export * from './utils'; export * from './validation'; export * from './zmodel-code-generator'; diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index bb7cfd4b8..3072ab202 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -13,29 +13,47 @@ import { ReferenceExpr, } from '@zenstackhq/language/ast'; import type { RuntimeAttribute } from '@zenstackhq/runtime'; +import { streamAst } from 'langium'; import { lowerCaseFirst } from 'lower-case-first'; -import { CodeBlockWriter, Project, VariableDeclarationKind } from 'ts-morph'; +import { CodeBlockWriter, Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; import { - emitProject, + ExpressionContext, getAttribute, getAttributeArg, + getAttributeArgLiteral, getAttributeArgs, getAuthModel, getDataModels, getLiteral, hasAttribute, + isDelegateModel, + isAuthInvocation, isEnumFieldReference, isForeignKeyField, isIdField, resolved, - saveProject, + TypeScriptExpressionTransformer, + getRelationField, } from '.'; +/** + * Options for generating model metadata + */ export type ModelMetaGeneratorOptions = { + /** + * Output directory + */ output: string; - compile: boolean; - preserveTsFiles: boolean; + + /** + * Whether to generate all attributes + */ generateAttributes: boolean; + + /** + * Whether to preserve the pre-compilation TypeScript files + */ + preserveTsFiles?: boolean; }; export async function generate(project: Project, models: DataModel[], options: ModelMetaGeneratorOptions) { @@ -43,142 +61,227 @@ export async function generate(project: Project, models: DataModel[], options: M sf.addStatements('/* eslint-disable */'); sf.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, - declarations: [{ name: 'metadata', initializer: (writer) => generateModelMetadata(models, writer, options) }], + declarations: [ + { name: 'metadata', initializer: (writer) => generateModelMetadata(models, sf, writer, options) }, + ], }); sf.addStatements('export default metadata;'); - if (!options.compile || options.preserveTsFiles) { - // save ts files - await saveProject(project); + if (options.preserveTsFiles) { + await sf.save(); } - if (options.compile) { - await emitProject(project); + + return sf; +} + +function generateModelMetadata( + dataModels: DataModel[], + sourceFile: SourceFile, + writer: CodeBlockWriter, + options: ModelMetaGeneratorOptions +) { + writer.block(() => { + writeModels(sourceFile, writer, dataModels, options); + writeDeleteCascade(writer, dataModels); + writeAuthModel(writer, dataModels); + }); +} + +function writeModels( + sourceFile: SourceFile, + writer: CodeBlockWriter, + dataModels: DataModel[], + options: ModelMetaGeneratorOptions +) { + writer.write('models:'); + writer.block(() => { + for (const model of dataModels) { + writer.write(`${lowerCaseFirst(model.name)}:`); + writer.block(() => { + writer.write(`name: '${model.name}',`); + writeBaseTypes(writer, model); + writeFields(sourceFile, writer, model, options); + writeUniqueConstraints(writer, model); + if (options.generateAttributes) { + writeModelAttributes(writer, model); + } + writeDiscriminator(writer, model); + }); + writer.writeLine(','); + } + }); + writer.writeLine(','); +} + +function writeBaseTypes(writer: CodeBlockWriter, model: DataModel) { + if (model.superTypes.length > 0) { + writer.write('baseTypes: ['); + writer.write(model.superTypes.map((t) => `'${t.ref?.name}'`).join(', ')); + writer.write('],'); } } -function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter, options: ModelMetaGeneratorOptions) { +function writeAuthModel(writer: CodeBlockWriter, dataModels: DataModel[]) { + const authModel = getAuthModel(dataModels); + if (authModel) { + writer.writeLine(`authModel: '${authModel.name}'`); + } +} + +function writeDeleteCascade(writer: CodeBlockWriter, dataModels: DataModel[]) { + writer.write('deleteCascade:'); writer.block(() => { - writer.write('fields:'); - writer.block(() => { - for (const model of dataModels) { - writer.write(`${lowerCaseFirst(model.name)}:`); - writer.block(() => { - for (const f of model.fields) { - const backlink = getBackLink(f); - const fkMapping = generateForeignKeyMapping(f); - writer.write(`${f.name}: { - name: "${f.name}", - type: "${ - f.type.reference - ? f.type.reference.$refText - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - f.type.type! - }",`); - - if (isIdField(f)) { - writer.write(` - isId: true,`); - } - - if (isDataModel(f.type.reference?.ref)) { - writer.write(` - isDataModel: true,`); - } - - if (f.type.array) { - writer.write(` - isArray: true,`); - } - - if (f.type.optional) { - writer.write(` - isOptional: true,`); - } - - if (options.generateAttributes) { - const attrs = getFieldAttributes(f); - if (attrs.length > 0) { - writer.write(` - attributes: ${JSON.stringify(attrs)},`); - } - } else { - // only include essential attributes - const attrs = getFieldAttributes(f).filter((attr) => - ['@default', '@updatedAt'].includes(attr.name) - ); - if (attrs.length > 0) { - writer.write(` - attributes: ${JSON.stringify(attrs)},`); - } - } - - if (backlink) { - writer.write(` - backLink: '${backlink.name}',`); - } - - if (isRelationOwner(f, backlink)) { - writer.write(` - isRelationOwner: true,`); - } - - if (isForeignKeyField(f)) { - writer.write(` - isForeignKey: true,`); - } - - if (fkMapping && Object.keys(fkMapping).length > 0) { - writer.write(` - foreignKeyMapping: ${JSON.stringify(fkMapping)},`); - } - - if (isAutoIncrement(f)) { - writer.write(` - isAutoIncrement: true,`); - } - - writer.write(` - },`); - } - }); - writer.write(','); + for (const model of dataModels) { + const cascades = getDeleteCascades(model); + if (cascades.length > 0) { + writer.writeLine(`${lowerCaseFirst(model.name)}: [${cascades.map((n) => `'${n}'`).join(', ')}],`); } - }); - writer.write(','); + } + }); + writer.writeLine(','); +} +function writeUniqueConstraints(writer: CodeBlockWriter, model: DataModel) { + const constraints = getUniqueConstraints(model); + if (constraints.length > 0) { writer.write('uniqueConstraints:'); writer.block(() => { - for (const model of dataModels) { - writer.write(`${lowerCaseFirst(model.name)}:`); - writer.block(() => { - for (const constraint of getUniqueConstraints(model)) { - writer.write(`${constraint.name}: { - name: "${constraint.name}", - fields: ${JSON.stringify(constraint.fields)} - },`); - } - }); - writer.write(','); + for (const constraint of constraints) { + writer.write(`${constraint.name}: { + name: "${constraint.name}", + fields: ${JSON.stringify(constraint.fields)} + },`); } }); writer.write(','); + } +} - writer.write('deleteCascade:'); - writer.block(() => { - for (const model of dataModels) { - const cascades = getDeleteCascades(model); - if (cascades.length > 0) { - writer.writeLine(`${lowerCaseFirst(model.name)}: [${cascades.map((n) => `'${n}'`).join(', ')}],`); +function writeModelAttributes(writer: CodeBlockWriter, model: DataModel) { + const attrs = getAttributes(model); + if (attrs.length > 0) { + writer.write(` +attributes: ${JSON.stringify(attrs)},`); + } +} + +function writeDiscriminator(writer: CodeBlockWriter, model: DataModel) { + const delegateAttr = getAttribute(model, '@@delegate'); + if (!delegateAttr) { + return; + } + const discriminator = getAttributeArg(delegateAttr, 'discriminator') as ReferenceExpr; + if (!discriminator) { + return; + } + if (discriminator) { + writer.write(`discriminator: ${JSON.stringify(discriminator.target.$refText)},`); + } +} + +function writeFields( + sourceFile: SourceFile, + writer: CodeBlockWriter, + model: DataModel, + options: ModelMetaGeneratorOptions +) { + writer.write('fields:'); + writer.block(() => { + for (const f of model.fields) { + const backlink = getBackLink(f); + const fkMapping = generateForeignKeyMapping(f); + writer.write(`${f.name}: {`); + + writer.write(` + name: "${f.name}", + type: "${ + f.type.reference + ? f.type.reference.$refText + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + f.type.type! + }",`); + + if (isIdField(f)) { + writer.write(` + isId: true,`); + } + + if (isDataModel(f.type.reference?.ref)) { + writer.write(` + isDataModel: true,`); + } + + if (f.type.array) { + writer.write(` + isArray: true,`); + } + + if (f.type.optional) { + writer.write(` + isOptional: true,`); + } + + if (options.generateAttributes) { + const attrs = getAttributes(f); + if (attrs.length > 0) { + writer.write(` + attributes: ${JSON.stringify(attrs)},`); + } + } else { + // only include essential attributes + const attrs = getAttributes(f).filter((attr) => ['@default', '@updatedAt'].includes(attr.name)); + if (attrs.length > 0) { + writer.write(` + attributes: ${JSON.stringify(attrs)},`); } } - }); - writer.write(','); - const authModel = getAuthModel(dataModels); - if (authModel) { - writer.writeLine(`authModel: '${authModel.name}'`); + if (backlink) { + writer.write(` + backLink: '${backlink.name}',`); + } + + if (isRelationOwner(f, backlink)) { + writer.write(` + isRelationOwner: true,`); + } + + if (isForeignKeyField(f)) { + writer.write(` + isForeignKey: true,`); + const relationField = getRelationField(f); + if (relationField) { + writer.write(` + relationField: '${relationField.name}',`); + } + } + + if (fkMapping && Object.keys(fkMapping).length > 0) { + writer.write(` + foreignKeyMapping: ${JSON.stringify(fkMapping)},`); + } + + const defaultValueProvider = generateDefaultValueProvider(f, sourceFile); + if (defaultValueProvider) { + writer.write(` + defaultValueProvider: ${defaultValueProvider},`); + } + + if (f.$inheritedFrom && isDelegateModel(f.$inheritedFrom) && !isIdField(f)) { + writer.write(` + inheritedFrom: ${JSON.stringify(f.$inheritedFrom.name)},`); + } + + if (isAutoIncrement(f)) { + writer.write(` + isAutoIncrement: true,`); + } + + writer.write(` + },`); } }); + writer.write(','); } function getBackLink(field: DataModelField) { @@ -207,13 +310,15 @@ function getBackLink(field: DataModelField) { } function getRelationName(field: DataModelField) { - const relAttr = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - const relName = relAttr && relAttr.args?.[0] && getLiteral(relAttr.args?.[0].value); - return relName; + const relAttr = getAttribute(field, '@relation'); + if (!relAttr) { + return undefined; + } + return getAttributeArgLiteral(relAttr, 'name'); } -function getFieldAttributes(field: DataModelField): RuntimeAttribute[] { - return field.attributes +function getAttributes(target: DataModelField | DataModel): RuntimeAttribute[] { + return target.attributes .map((attr) => { const args: Array<{ name?: string; value: unknown }> = []; for (const arg of attr.args) { @@ -338,7 +443,6 @@ function generateForeignKeyMapping(field: DataModelField) { const fieldNames = fields.items.map((item) => (isReferenceExpr(item) ? item.target.$refText : undefined)); const referenceNames = references.items.map((item) => (isReferenceExpr(item) ? item.target.$refText : undefined)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: Record = {}; referenceNames.forEach((name, i) => { if (name) { @@ -374,6 +478,39 @@ function getDeleteCascades(model: DataModel): string[] { .map((m) => m.name); } +function generateDefaultValueProvider(field: DataModelField, sourceFile: SourceFile) { + const defaultAttr = getAttribute(field, '@default'); + if (!defaultAttr) { + return undefined; + } + + const expr = defaultAttr.args[0]?.value; + if (!expr) { + return undefined; + } + + // find `auth()` in default value expression + const hasAuth = streamAst(expr).some(isAuthInvocation); + if (!hasAuth) { + return undefined; + } + + // generates a provider function like: + // function $default$Model$field(user: any) { ... } + const func = sourceFile.addFunction({ + name: `$default$${field.$container.name}$${field.name}`, + parameters: [{ name: 'user', type: 'any' }], + returnType: 'unknown', + statements: (writer) => { + const tsWriter = new TypeScriptExpressionTransformer({ context: ExpressionContext.DefaultValue }); + const code = tsWriter.transform(expr, false); + writer.write(`return ${code};`); + }, + }); + + return func.getName(); +} + function isAutoIncrement(field: DataModelField) { const defaultAttr = getAttribute(field, '@default'); if (!defaultAttr) { diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index 970ce58ba..b45dd7cfb 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -1,93 +1,78 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import type { DMMF } from '@prisma/generator-helper'; -import { getPrismaVersion } from '@zenstackhq/runtime'; +import { getDMMF as _getDMMF, type GetDMMFOptions } from '@prisma/internals'; +import { DEFAULT_RUNTIME_LOAD_PATH } from '@zenstackhq/runtime'; import path from 'path'; -import * as semver from 'semver'; -import { GeneratorDecl, Model, Plugin, isGeneratorDecl, isPlugin } from './ast'; -import { getLiteral } from './utils'; - -// reexport -export { getPrismaVersion } from '@zenstackhq/runtime'; +import { RUNTIME_PACKAGE } from './constants'; +import type { PluginOptions } from './types'; /** - * Given a ZModel and an import context directory, compute the import spec for the Prisma Client. + * Given an import context directory and plugin options, compute the import spec for the Prisma Client. */ -export function getPrismaClientImportSpec(model: Model, importingFromDir: string) { - const generator = model.declarations.find( - (d) => - isGeneratorDecl(d) && - d.fields.some((f) => f.name === 'provider' && getLiteral(f.value) === 'prisma-client-js') - ) as GeneratorDecl; - - const clientOutputField = generator?.fields.find((f) => f.name === 'output'); - const clientOutput = getLiteral(clientOutputField?.value); - - if (!clientOutput) { - // no user-declared Prisma Client output location +export function getPrismaClientImportSpec(importingFromDir: string, options: PluginOptions) { + if (!options.prismaClientPath || options.prismaClientPath === '@prisma/client') { return '@prisma/client'; } - if (path.isAbsolute(clientOutput)) { - // absolute path - return clientOutput; + if ( + options.prismaClientPath.startsWith(RUNTIME_PACKAGE) || + options.prismaClientPath.startsWith(DEFAULT_RUNTIME_LOAD_PATH) + ) { + return options.prismaClientPath; } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const zmodelDir = path.dirname(model.$document!.uri.fsPath); - - // compute prisma schema absolute output path - let prismaSchemaOutputDir = path.resolve(zmodelDir, './prisma'); - const prismaPlugin = model.declarations.find( - (d) => isPlugin(d) && d.fields.some((f) => f.name === 'provider' && getLiteral(f.value) === '@core/prisma') - ) as Plugin; - if (prismaPlugin) { - const output = getLiteral(prismaPlugin.fields.find((f) => f.name === 'output')?.value); - if (output) { - if (path.isAbsolute(output)) { - // absolute prisma schema output path - prismaSchemaOutputDir = path.dirname(output); - } else { - prismaSchemaOutputDir = path.dirname(path.resolve(zmodelDir, output)); - } - } + if (path.isAbsolute(options.prismaClientPath)) { + // absolute path + return options.prismaClientPath; } - // resolve the prisma client output path, which is relative to the prisma schema - const resolvedPrismaClientOutput = path.resolve(prismaSchemaOutputDir, clientOutput); + // resolve absolute path based on the zmodel file location + const resolvedPrismaClientOutput = path.resolve(path.dirname(options.schemaPath), options.prismaClientPath); + + // translate to path relative to the importing context directory + let result = path.relative(importingFromDir, resolvedPrismaClientOutput); - // DEBUG: - // console.log('PRISMA SCHEMA PATH:', prismaSchemaOutputDir); - // console.log('PRISMA CLIENT PATH:', resolvedPrismaClientOutput); - // console.log('IMPORTING PATH:', importingFromDir); + // remove leading `node_modules` (which may be provided by the user) + result = result.replace(/^([./\\]*)?node_modules\//, ''); // compute prisma client absolute output dir relative to the importing file - return normalizePath(path.relative(importingFromDir, resolvedPrismaClientOutput)); + return normalizePath(result); } function normalizePath(p: string) { return p ? p.split(path.sep).join(path.posix.sep) : p; } -export type GetDMMFOptions = { - datamodel?: string; - cwd?: string; - prismaPath?: string; - datamodelPath?: string; - retry?: number; - previewFeatures?: string[]; -}; +/** + * Loads Prisma DMMF + */ +export function getDMMF(options: GetDMMFOptions): Promise { + return _getDMMF(options); +} /** - * Loads Prisma DMMF with appropriate version + * Gets the installed Prisma's version */ -export function getDMMF(options: GetDMMFOptions, defaultPrismaVersion?: string): Promise { - const prismaVersion = getPrismaVersion() ?? defaultPrismaVersion; - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - const _getDMMF = require('@prisma/internals-v5').getDMMF; - return _getDMMF(options); - } else { - const _getDMMF = require('@prisma/internals').getDMMF; - return _getDMMF(options); +export function getPrismaVersion(): string | undefined { + if (process.env.ZENSTACK_TEST === '1') { + // test environment + try { + return require(path.resolve('./node_modules/@prisma/client/package.json')).version; + } catch { + return undefined; + } + } + + try { + return require('@prisma/client/package.json').version; + } catch { + try { + return require('prisma/package.json').version; + } catch { + return undefined; + } } } + +export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c19fdfc42..a6a4b8629 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,5 +1,6 @@ import type { DMMF } from '@prisma/generator-helper'; import { Model } from '@zenstackhq/language/ast'; +import type { Project } from 'ts-morph'; /** * Plugin configuration option value type @@ -9,22 +10,27 @@ export type OptionValue = string | number | boolean; /** * Plugin configuration options */ -export type PluginOptions = { +export type PluginDeclaredOptions = { /*** * The provider package */ - provider?: string; + provider: string; +} & Record; +/** + * Plugin configuration options for execution + */ +export type PluginOptions = { /** - * The path of the ZModel schema + * ZModel schema absolute path */ schemaPath: string; /** - * The name of the plugin + * PrismaClient import path, either relative to `schemaPath` or absolute */ - name: string; -} & Record; + prismaClientPath?: string; +} & PluginDeclaredOptions; /** * Global options that apply to all plugins @@ -39,6 +45,34 @@ export type PluginGlobalOptions = { * Whether to compile the generated code */ compile: boolean; + + /** + * The `ts-morph` project used for code generation. + * @private + */ + tsProject: Project; +}; + +/** + * Plugin run results. + */ +export type PluginResult = { + /** + * Warnings + */ + warnings: string[]; + + /** + * PrismaClient path, either relative to zmodel path or absolute, if the plugin + * generated a PrismaClient + */ + prismaClientPath?: string; + + /** + * An optional Prisma DMMF document that a plugin can generate + * @private + */ + dmmf?: DMMF.Document; }; /** @@ -47,9 +81,9 @@ export type PluginGlobalOptions = { export type PluginFunction = ( model: Model, options: PluginOptions, - dmmf?: DMMF.Document, + dmmf: DMMF.Document | undefined, globalOptions?: PluginGlobalOptions -) => Promise | string[] | Promise | void; +) => Promise | PluginResult | Promise | void; /** * Plugin error diff --git a/packages/schema/src/utils/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts similarity index 99% rename from packages/schema/src/utils/typescript-expression-transformer.ts rename to packages/sdk/src/typescript-expression-transformer.ts index 27e018aa1..8e33eb4a7 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -20,9 +20,9 @@ import { isNullExpr, isThisExpr, } from '@zenstackhq/language/ast'; -import { ExpressionContext, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from '@zenstackhq/sdk'; import { P, match } from 'ts-pattern'; -import { getIdFields } from './ast-utils'; +import { ExpressionContext } from './constants'; +import { getIdFields, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from './utils'; export class TypeScriptExpressionTransformerError extends Error { constructor(message: string) { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index a32abc068..6617983aa 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -22,6 +22,7 @@ import { isGeneratorDecl, isInvocationExpr, isLiteralExpr, + isMemberAccessExpr, isModel, isObjectExpr, isReferenceExpr, @@ -29,12 +30,13 @@ import { Reference, ReferenceExpr, } from '@zenstackhq/language/ast'; +import fs from 'node:fs'; import path from 'path'; import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; -import { PluginError, PluginOptions } from './types'; +import { PluginError, type PluginDeclaredOptions, type PluginOptions } from './types'; /** - * Gets data models that are not ignored + * Gets data models in the ZModel schema. */ export function getDataModels(model: Model, includeIgnored = false) { const r = model.declarations.filter((d): d is DataModel => isDataModel(d)); @@ -174,39 +176,51 @@ export function isDataModelFieldReference(node: AstNode): node is ReferenceExpr } /** - * Gets `@@id` fields declared at the data model level + * Gets `@@id` fields declared at the data model level (including search in base models) */ export function getModelIdFields(model: DataModel) { - const idAttr = model.attributes.find((attr) => attr.decl.ref?.name === '@@id'); - if (!idAttr) { - return []; - } - const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); - if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { - return []; + const modelsToCheck = model.$baseMerged ? [model] : [model, ...getRecursiveBases(model)]; + + for (const modelToCheck of modelsToCheck) { + const idAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@id'); + if (!idAttr) { + continue; + } + const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); + if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { + continue; + } + + return fieldsArg.value.items + .filter((item): item is ReferenceExpr => isReferenceExpr(item)) + .map((item) => resolved(item.target) as DataModelField); } - return fieldsArg.value.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => resolved(item.target) as DataModelField); + return []; } /** - * Gets `@@unique` fields declared at the data model level + * Gets `@@unique` fields declared at the data model level (including search in base models) */ export function getModelUniqueFields(model: DataModel) { - const uniqueAttr = model.attributes.find((attr) => attr.decl.ref?.name === '@@unique'); - if (!uniqueAttr) { - return []; - } - const fieldsArg = uniqueAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); - if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { - return []; + const modelsToCheck = model.$baseMerged ? [model] : [model, ...getRecursiveBases(model)]; + + for (const modelToCheck of modelsToCheck) { + const uniqueAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@unique'); + if (!uniqueAttr) { + continue; + } + const fieldsArg = uniqueAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); + if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { + continue; + } + + return fieldsArg.value.items + .filter((item): item is ReferenceExpr => isReferenceExpr(item)) + .map((item) => resolved(item.target) as DataModelField); } - return fieldsArg.value.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => resolved(item.target) as DataModelField); + return []; } /** @@ -283,15 +297,54 @@ export function isForeignKeyField(field: DataModelField) { }); } +/** + * Gets the foreign key fields of the given relation field. + */ +export function getForeignKeyFields(relationField: DataModelField) { + if (!isRelationshipField(relationField)) { + return []; + } + + const relAttr = relationField.attributes.find((attr) => attr.decl.ref?.name === '@relation'); + if (relAttr) { + // find "fields" arg + const fieldsArg = getAttributeArg(relAttr, 'fields'); + if (fieldsArg && isArrayExpr(fieldsArg)) { + return fieldsArg.items + .filter((item): item is ReferenceExpr => isReferenceExpr(item)) + .map((item) => item.target.ref as DataModelField); + } + } + + return []; +} + +/** + * Gets the relation field of the given foreign key field. + */ +export function getRelationField(fkField: DataModelField) { + const model = fkField.$container as DataModel; + return model.fields.find((f) => { + const relAttr = f.attributes.find((attr) => attr.decl.ref?.name === '@relation'); + if (relAttr) { + const fieldsArg = getAttributeArg(relAttr, 'fields'); + if (fieldsArg && isArrayExpr(fieldsArg)) { + return fieldsArg.items.some((item) => isReferenceExpr(item) && item.target.ref === fkField); + } + } + return false; + }); +} + export function resolvePath(_path: string, options: Pick) { if (path.isAbsolute(_path)) { return _path; } else { - return path.join(path.dirname(options.schemaPath), _path); + return path.resolve(path.dirname(options.schemaPath), _path); } } -export function requireOption(options: PluginOptions, name: string, pluginName: string): T { +export function requireOption(options: PluginDeclaredOptions, name: string, pluginName: string): T { const value = options[name]; if (value === undefined) { throw new PluginError(pluginName, `Plugin "${options.name}" is missing required option: ${name}`); @@ -299,8 +352,8 @@ export function requireOption(options: PluginOptions, name: string, pluginNam return value as T; } -export function parseOptionAsStrings(options: PluginOptions, optionaName: string, pluginName: string) { - const value = options[optionaName]; +export function parseOptionAsStrings(options: PluginDeclaredOptions, optionName: string, pluginName: string) { + const value = options[optionName]; if (value === undefined) { return undefined; } else if (typeof value === 'string') { @@ -315,7 +368,7 @@ export function parseOptionAsStrings(options: PluginOptions, optionaName: string } else { throw new PluginError( pluginName, - `Invalid "${optionaName}" option: must be a comma-separated string or an array of strings` + `Invalid "${optionName}" option: must be a comma-separated string or an array of strings` ); } } @@ -337,7 +390,11 @@ export function getFunctionExpressionContext(funcDecl: FunctionDecl) { } export function isFutureExpr(node: AstNode) { - return !!(isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref)); + return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); +} + +export function isAuthInvocation(node: AstNode) { + return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); } export function isFromStdlib(node: AstNode) { @@ -376,3 +433,81 @@ export function getAuthModel(dataModels: DataModel[]) { } return authModel; } + +export function isDelegateModel(node: AstNode) { + return isDataModel(node) && hasAttribute(node, '@@delegate'); +} + +export function isDiscriminatorField(field: DataModelField) { + const model = field.$inheritedFrom ?? field.$container; + const delegateAttr = getAttribute(model, '@@delegate'); + if (!delegateAttr) { + return false; + } + const arg = delegateAttr.args[0]?.value; + return isDataModelFieldReference(arg) && arg.target.$refText === field.name; +} + +export function getIdFields(dataModel: DataModel) { + const fieldLevelId = getModelFieldsWithBases(dataModel).find((f) => + f.attributes.some((attr) => attr.decl.$refText === '@id') + ); + if (fieldLevelId) { + return [fieldLevelId]; + } else { + // get model level @@id attribute + const modelIdAttr = dataModel.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); + if (modelIdAttr) { + // get fields referenced in the attribute: @@id([field1, field2]]) + if (!isArrayExpr(modelIdAttr.args[0].value)) { + return []; + } + const argValue = modelIdAttr.args[0].value; + return argValue.items + .filter((expr): expr is ReferenceExpr => isReferenceExpr(expr) && !!getDataModelFieldReference(expr)) + .map((expr) => expr.target.ref as DataModelField); + } + } + return []; +} + +export function getDataModelFieldReference(expr: Expression): DataModelField | undefined { + if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) { + return expr.target.ref; + } else if (isMemberAccessExpr(expr) && isDataModelField(expr.member.ref)) { + return expr.member.ref; + } else { + return undefined; + } +} + +export function getModelFieldsWithBases(model: DataModel) { + return [...model.fields, ...getRecursiveBases(model).flatMap((base) => base.fields)]; +} + +export function getRecursiveBases(dataModel: DataModel): DataModel[] { + const result: DataModel[] = []; + dataModel.superTypes.forEach((superType) => { + const baseDecl = superType.ref; + if (baseDecl) { + result.push(baseDecl); + result.push(...getRecursiveBases(baseDecl)); + } + }); + return result; +} + +export function ensureEmptyDir(dir: string) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + return; + } + + const stats = fs.statSync(dir); + if (stats.isDirectory()) { + fs.rmSync(dir, { recursive: true }); + fs.mkdirSync(dir, { recursive: true }); + } else { + throw new Error(`Path "${dir}" already exists and is not a directory`); + } +} diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/server/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/server/README.md b/packages/server/README.md index 15c7646f2..b4b4ffb4c 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -1,5 +1,5 @@ -# ZenStack Fastify Plugin Library +# ZenStack Server Adapters -This package provides a Fastify plugin for ZenStack. +This package provides adapters and utilities for integrating with popular Node.js servers, including Express, Fastify, and Nest.js. Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/server/package.json b/packages/server/package.json index 39a34bf39..87a709075 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.12.4", + "version": "2.0.0", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", @@ -31,13 +31,16 @@ "lower-case-first": "^2.0.2", "superjson": "^1.11.0", "tiny-invariant": "^1.3.1", - "ts-japi": "^1.8.0", + "ts-japi": "^1.10.1", "upper-case-first": "^2.0.2", "url-pattern": "^1.0.3", "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, "devDependencies": { + "@nestjs/common": "^10.3.7", + "@nestjs/platform-express": "^10.3.7", + "@nestjs/testing": "^10.3.7", "@sveltejs/kit": "1.21.0", "@types/body-parser": "^1.19.2", "@types/express": "^4.17.17", @@ -52,10 +55,12 @@ "isomorphic-fetch": "^3.0.0", "next": "^13.4.5", "nuxt": "^3.7.4", + "reflect-metadata": "^0.2.2", "supertest": "^6.3.3" }, "exports": { "./package.json": "./package.json", + "./api": "./api/index.js", "./api/rest": "./api/rest/index.js", "./api/rpc": "./api/rpc/index.js", "./express": "./express/index.js", @@ -65,6 +70,7 @@ "./next/pages-route-handler": "./next/pages-route-handler.js", "./sveltekit": "./sveltekit/index.js", "./nuxt": "./nuxt/index.js", + "./nestjs": "./nestjs/index.js", "./types": "./types.js" } } diff --git a/packages/server/src/api/base.ts b/packages/server/src/api/base.ts index ba385f31c..6b9dbfbbd 100644 --- a/packages/server/src/api/base.ts +++ b/packages/server/src/api/base.ts @@ -1,5 +1,6 @@ -import { DbClientContract, ModelMeta, ZodSchemas, getDefaultModelMeta } from '@zenstackhq/runtime'; -import { LoggerConfig } from '../types'; +import type { DbClientContract, ModelMeta, ZodSchemas } from '@zenstackhq/runtime'; +import { getDefaultModelMeta } from '../shared'; +import type { LoggerConfig } from '../types'; /** * API request context @@ -58,7 +59,7 @@ export abstract class APIHandlerBase { constructor() { try { - this.defaultModelMeta = getDefaultModelMeta(undefined); + this.defaultModelMeta = getDefaultModelMeta(); } catch { // noop } diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts new file mode 100644 index 000000000..88e96c11e --- /dev/null +++ b/packages/server/src/api/index.ts @@ -0,0 +1,2 @@ +export { RPCApiHandler } from './rpc'; +export { RestApiHandler } from './rest'; diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 88b463c80..530bfbcc9 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -167,6 +167,10 @@ class RequestHandler extends APIHandlerBase { status: 403, title: 'Operation is forbidden', }, + validationError: { + status: 422, + title: 'Operation is unprocessable due to validation errors', + }, unknownError: { status: 400, title: 'Unknown error', @@ -699,7 +703,7 @@ class RequestHandler extends APIHandlerBase { error: this.makeError( 'invalidPayload', fromZodError(parsed.error).message, - undefined, + 422, CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error ), @@ -956,7 +960,7 @@ class RequestHandler extends APIHandlerBase { private buildTypeMap(logger: LoggerConfig | undefined, modelMeta: ModelMeta): void { this.typeMap = {}; - for (const [model, fields] of Object.entries(modelMeta.fields)) { + for (const [model, { fields }] of Object.entries(modelMeta.models)) { const idFields = getIdFields(modelMeta, model); if (idFields.length === 0) { logWarning(logger, `Not including model ${model} in the API because it has no ID field`); @@ -1013,7 +1017,7 @@ class RequestHandler extends APIHandlerBase { this.serializers = new Map(); const linkers: Record> = {}; - for (const model of Object.keys(modelMeta.fields)) { + for (const model of Object.keys(modelMeta.models)) { const ids = getIdFields(modelMeta, model); if (ids.length !== 1) { continue; @@ -1027,7 +1031,7 @@ class RequestHandler extends APIHandlerBase { linkers[model] = linker; let projection: Record | null = {}; - for (const [field, fieldMeta] of Object.entries(modelMeta.fields[model])) { + for (const [field, fieldMeta] of Object.entries(modelMeta.models[model].fields)) { if (fieldMeta.isDataModel) { projection[field] = 0; } @@ -1049,14 +1053,14 @@ class RequestHandler extends APIHandlerBase { } // set relators - for (const model of Object.keys(modelMeta.fields)) { + for (const model of Object.keys(modelMeta.models)) { const serializer = this.serializers.get(model); if (!serializer) { continue; } const relators: Record> = {}; - for (const [field, fieldMeta] of Object.entries(modelMeta.fields[model])) { + for (const [field, fieldMeta] of Object.entries(modelMeta.models[model].fields)) { if (!fieldMeta.isDataModel) { continue; } @@ -1117,7 +1121,7 @@ class RequestHandler extends APIHandlerBase { throw new Error(`serializer not found for model ${model}`); } - // serialize to JSON:API strcuture + // serialize to JSON:API structure const serialized = await serializer.serialize(items, options); // convert the serialization result to plain object otherwise SuperJSON won't work @@ -1577,13 +1581,17 @@ class RequestHandler extends APIHandlerBase { private handlePrismaError(err: unknown) { if (isPrismaClientKnownRequestError(err)) { if (err.code === PrismaErrorCode.CONSTRAINED_FAILED) { - return this.makeError( - 'forbidden', - undefined, - 403, - err.meta?.reason as string, - err.meta?.zodErrors as ZodError - ); + if (err.meta?.reason === CrudFailureReason.DATA_VALIDATION_VIOLATION) { + return this.makeError( + 'validationError', + undefined, + 422, + err.meta?.reason as string, + err.meta?.zodErrors as ZodError + ); + } else { + return this.makeError('forbidden', undefined, 403, err.meta?.reason as string); + } } else if (err.code === 'P2025' || err.code === 'P2018') { return this.makeError('notFound'); } else { @@ -1656,3 +1664,5 @@ export default function makeHandler(options: Options) { const handler = new RequestHandler(options); return handler.handleRequest.bind(handler); } + +export { makeHandler as RestApiHandler }; diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index 983e79154..a7fb44d72 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -130,7 +130,7 @@ class RequestHandler extends APIHandlerBase { const { error, zodErrors, data: parsedArgs } = await this.processRequestPayload(args, model, dbOp, zodSchemas); if (error) { - return { status: 400, body: this.makeError(error, CrudFailureReason.DATA_VALIDATION_VIOLATION, zodErrors) }; + return { status: 422, body: this.makeError(error, CrudFailureReason.DATA_VALIDATION_VIOLATION, zodErrors) }; } try { @@ -155,7 +155,14 @@ class RequestHandler extends APIHandlerBase { return { status: resCode, body: response }; } catch (err) { if (isPrismaClientKnownRequestError(err)) { - const status = ERROR_STATUS_MAPPING[err.code] ?? 400; + let status: number; + + if (err.meta?.reason === CrudFailureReason.DATA_VALIDATION_VIOLATION) { + // data validation error + status = 422; + } else { + status = ERROR_STATUS_MAPPING[err.code] ?? 400; + } const { error } = this.makeError( err.message, @@ -283,3 +290,5 @@ export default function makeHandler() { const handler = new RequestHandler(); return handler.handleRequest.bind(handler); } + +export { makeHandler as RPCApiHandler }; diff --git a/packages/server/src/api/utils.ts b/packages/server/src/api/utils.ts index bc9cc5d71..cbabe7cc4 100644 --- a/packages/server/src/api/utils.ts +++ b/packages/server/src/api/utils.ts @@ -39,12 +39,15 @@ export function registerCustomSerializers() { 'Decimal' ); - SuperJSON.registerCustom( - { - isApplicable: (v): v is Buffer => Buffer.isBuffer(v), - serialize: (v) => v.toString('base64'), - deserialize: (v) => Buffer.from(v, 'base64'), - }, - 'Bytes' - ); + // `Buffer` is not available in edge runtime + if (globalThis.Buffer) { + SuperJSON.registerCustom( + { + isApplicable: (v): v is Buffer => Buffer.isBuffer(v), + serialize: (v) => v.toString('base64'), + deserialize: (v) => Buffer.from(v, 'base64'), + }, + 'Bytes' + ); + } } diff --git a/packages/server/src/express/index.ts b/packages/server/src/express/index.ts index 7cbc0a0c4..1def0479b 100644 --- a/packages/server/src/express/index.ts +++ b/packages/server/src/express/index.ts @@ -1,2 +1,2 @@ -export { default as ZenStackMiddleware } from './middleware'; +export { ZenStackMiddleware } from './middleware'; export * from './middleware'; diff --git a/packages/server/src/express/middleware.ts b/packages/server/src/express/middleware.ts index cdf5a3c6e..67a185704 100644 --- a/packages/server/src/express/middleware.ts +++ b/packages/server/src/express/middleware.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { DbClientContract } from '@zenstackhq/runtime'; import type { Handler, Request, Response } from 'express'; -import RPCAPIHandler from '../api/rpc'; +import { RPCApiHandler } from '../api/rpc'; import { loadAssets } from '../shared'; import { AdapterBaseOptions } from '../types'; @@ -32,12 +32,7 @@ export interface MiddlewareOptions extends AdapterBaseOptions { const factory = (options: MiddlewareOptions): Handler => { const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler || RPCAPIHandler(); - if (options.useSuperJson !== undefined) { - console.warn( - 'The option "useSuperJson" is deprecated. The server APIs automatically use superjson for serialization.' - ); - } + const requestHandler = options.handler || RPCApiHandler(); return async (request, response, next) => { const prisma = (await options.getPrisma(request, response)) as DbClientContract; @@ -88,3 +83,5 @@ const factory = (options: MiddlewareOptions): Handler => { }; export default factory; + +export { factory as ZenStackMiddleware }; diff --git a/packages/server/src/fastify/index.ts b/packages/server/src/fastify/index.ts index 29eb98aed..486b76c64 100644 --- a/packages/server/src/fastify/index.ts +++ b/packages/server/src/fastify/index.ts @@ -1,2 +1,2 @@ -export { default as ZenStackFastifyPlugin } from './plugin'; +export { ZenStackFastifyPlugin } from './plugin'; export * from './plugin'; diff --git a/packages/server/src/fastify/plugin.ts b/packages/server/src/fastify/plugin.ts index 480c4ba8d..a69d9ac98 100644 --- a/packages/server/src/fastify/plugin.ts +++ b/packages/server/src/fastify/plugin.ts @@ -32,11 +32,6 @@ const pluginHandler: FastifyPluginCallback = (fastify, options, d const { modelMeta, zodSchemas } = loadAssets(options); const requestHandler = options.handler ?? RPCApiHandler(); - if (options.useSuperJson !== undefined) { - console.warn( - 'The option "useSuperJson" is deprecated. The server APIs automatically use superjson for serialization.' - ); - } fastify.all(`${prefix}/*`, async (request, reply) => { const prisma = (await options.getPrisma(request, reply)) as DbClientContract; @@ -67,4 +62,8 @@ const pluginHandler: FastifyPluginCallback = (fastify, options, d done(); }; -export default fp(pluginHandler); +const plugin = fp(pluginHandler); + +export default plugin; + +export { plugin as ZenStackFastifyPlugin }; diff --git a/packages/server/src/nestjs/index.ts b/packages/server/src/nestjs/index.ts new file mode 100644 index 000000000..f94469901 --- /dev/null +++ b/packages/server/src/nestjs/index.ts @@ -0,0 +1 @@ +export * from './zenstack.module'; diff --git a/packages/server/src/nestjs/zenstack.module.ts b/packages/server/src/nestjs/zenstack.module.ts new file mode 100644 index 000000000..f2ae601c6 --- /dev/null +++ b/packages/server/src/nestjs/zenstack.module.ts @@ -0,0 +1,98 @@ +import { Module, type DynamicModule, type FactoryProvider, type ModuleMetadata, type Provider } from '@nestjs/common'; + +/** + * The default token used to export the enhanced Prisma service. + */ +export const ENHANCED_PRISMA = 'ENHANCED_PRISMA'; + +/** + * ZenStack module options. + */ +export interface ZenStackModuleOptions { + /** + * A callback for getting an enhanced `PrismaClient`. + */ + getEnhancedPrisma: () => unknown; +} + +/** + * ZenStack module async registration options. + */ +export interface ZenStackModuleAsyncOptions extends Pick { + /** + * Whether the module is global-scoped. + */ + global?: boolean; + + /** + * The token to export the enhanced Prisma service. Default is {@link ENHANCED_PRISMA}. + */ + exportToken?: string; + + /** + * The factory function to create the enhancement options. + */ + useFactory: (...args: unknown[]) => Promise | ZenStackModuleOptions; + + /** + * The dependencies to inject into the factory function. + */ + inject?: FactoryProvider['inject']; + + /** + * Extra providers to facilitate dependency injection. + */ + extraProviders?: Provider[]; +} + +/** + * The ZenStack module for NestJS. The module exports an enhanced Prisma service, + * by default with token {@link ENHANCED_PRISMA}. + */ +@Module({}) +export class ZenStackModule { + /** + * Registers the ZenStack module with the specified options. + */ + static registerAsync(options: ZenStackModuleAsyncOptions): DynamicModule { + return { + module: ZenStackModule, + global: options?.global, + imports: options.imports, + providers: [ + { + provide: options.exportToken ?? ENHANCED_PRISMA, + useFactory: async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...args: unknown[] + ) => { + const { getEnhancedPrisma } = await options.useFactory(...args); + if (!getEnhancedPrisma) { + throw new Error('`getEnhancedPrisma` must be provided in the options'); + } + + // create a proxy to intercept all calls to the Prisma service and forward + // to the enhanced version + + return new Proxy( + {}, + { + get(_target, prop) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const enhancedPrisma: any = getEnhancedPrisma(); + if (!enhancedPrisma) { + throw new Error('`getEnhancedPrisma` must return a valid Prisma client'); + } + return enhancedPrisma[prop]; + }, + } + ); + }, + inject: options.inject, + }, + ...(options.extraProviders ?? []), + ], + exports: [options.exportToken ?? ENHANCED_PRISMA], + }; + } +} diff --git a/packages/server/src/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts index 538f4ceb5..5c8cbe0e5 100644 --- a/packages/server/src/next/app-route-handler.ts +++ b/packages/server/src/next/app-route-handler.ts @@ -3,7 +3,7 @@ import { DbClientContract } from '@zenstackhq/runtime'; import { NextRequest, NextResponse } from 'next/server'; import { AppRouteRequestHandlerOptions } from '.'; -import RPCAPIHandler from '../api/rpc'; +import { RPCApiHandler } from '../api'; import { loadAssets } from '../shared'; type Context = { params: { path: string[] } }; @@ -19,12 +19,7 @@ export default function factory( ): (req: NextRequest, context: Context) => Promise { const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler || RPCAPIHandler(); - if (options.useSuperJson !== undefined) { - console.warn( - 'The option "useSuperJson" is deprecated. The server APIs automatically use superjson for serialization.' - ); - } + const requestHandler = options.handler || RPCApiHandler(); return async (req: NextRequest, context: Context) => { const prisma = (await options.getPrisma(req)) as DbClientContract; diff --git a/packages/server/src/next/pages-route-handler.ts b/packages/server/src/next/pages-route-handler.ts index bd2fbf643..dd25b0c6c 100644 --- a/packages/server/src/next/pages-route-handler.ts +++ b/packages/server/src/next/pages-route-handler.ts @@ -3,7 +3,7 @@ import { DbClientContract } from '@zenstackhq/runtime'; import { NextApiRequest, NextApiResponse } from 'next'; import { PagesRouteRequestHandlerOptions } from '.'; -import RPCAPIHandler from '../api/rpc'; +import { RPCApiHandler } from '../api'; import { loadAssets } from '../shared'; /** @@ -17,12 +17,7 @@ export default function factory( ): (req: NextApiRequest, res: NextApiResponse) => Promise { const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler || RPCAPIHandler(); - if (options.useSuperJson !== undefined) { - console.warn( - 'The option "useSuperJson" is deprecated. The server APIs automatically use superjson for serialization.' - ); - } + const requestHandler = options.handler || RPCApiHandler(); return async (req: NextApiRequest, res: NextApiResponse) => { const prisma = (await options.getPrisma(req, res)) as DbClientContract; diff --git a/packages/server/src/shared.ts b/packages/server/src/shared.ts index 6001fbbaa..27a07ddd9 100644 --- a/packages/server/src/shared.ts +++ b/packages/server/src/shared.ts @@ -1,16 +1,17 @@ -import { ZodSchemas, getDefaultModelMeta, getDefaultZodSchemas } from '@zenstackhq/runtime'; +/* eslint-disable @typescript-eslint/no-var-requires */ +import type { ModelMeta, ZodSchemas } from '@zenstackhq/runtime'; import { AdapterBaseOptions } from './types'; export function loadAssets(options: AdapterBaseOptions) { // model metadata - const modelMeta = options.modelMeta ?? getDefaultModelMeta(options.loadPath); + const modelMeta = options.modelMeta ?? getDefaultModelMeta(); // zod schemas let zodSchemas: ZodSchemas | undefined; if (typeof options.zodSchemas === 'object') { zodSchemas = options.zodSchemas; } else if (options.zodSchemas === true) { - zodSchemas = getDefaultZodSchemas(options.loadPath); + zodSchemas = getDefaultZodSchemas(); if (!zodSchemas) { throw new Error('Unable to load zod schemas from default location'); } @@ -18,3 +19,31 @@ export function loadAssets(options: AdapterBaseOptions) { return { modelMeta, zodSchemas }; } + +/** + * Load model metadata. + * + * @param loadPath The path to load model metadata from. If not provided, + * will use default load path. + */ +export function getDefaultModelMeta(): ModelMeta { + try { + return require('@zenstackhq/runtime/model-meta').default; + } catch { + throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.'); + } +} + +/** + * Load zod schemas. + * + * @param loadPath The path to load zod schemas from. If not provided, + * will use default load path. + */ +export function getDefaultZodSchemas(): ZodSchemas | undefined { + try { + return require('@zenstackhq/runtime/zod'); + } catch { + return undefined; + } +} diff --git a/packages/server/src/sveltekit/handler.ts b/packages/server/src/sveltekit/handler.ts index be1d831d8..f5d1b7995 100644 --- a/packages/server/src/sveltekit/handler.ts +++ b/packages/server/src/sveltekit/handler.ts @@ -29,11 +29,6 @@ export default function createHandler(options: HandlerOptions): Handle { const { modelMeta, zodSchemas } = loadAssets(options); const requestHandler = options.handler ?? RPCApiHandler(); - if (options.useSuperJson !== undefined) { - console.warn( - 'The option "useSuperJson" is deprecated. The server APIs automatically use superjson for serialization.' - ); - } return async ({ event, resolve }) => { if (event.url.pathname.startsWith(options.prefix)) { @@ -88,3 +83,5 @@ export default function createHandler(options: HandlerOptions): Handle { return resolve(event); }; } + +export { createHandler as SvelteKitHandler }; diff --git a/packages/server/src/sveltekit/index.ts b/packages/server/src/sveltekit/index.ts index 83f2980bb..7f040d76f 100644 --- a/packages/server/src/sveltekit/index.ts +++ b/packages/server/src/sveltekit/index.ts @@ -1,2 +1,2 @@ -export { default as SvelteKitHandler } from './handler'; +export { SvelteKitHandler } from './handler'; export * from './handler'; diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index dc72fea25..33b0ef4c9 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -36,9 +36,9 @@ export interface AdapterBaseOptions { logger?: LoggerConfig; /** - * Model metadata. By default loaded from the standard output location - * of the `@zenstackhq/model-meta` plugin. You can pass it in explicitly - * if you configured the plugin to output to a different location. + * Model metadata. By default loaded from the `node_module/.zenstack/model-meta` + * module. You can pass it in explicitly if you configured ZenStack to output to + * a different location. */ modelMeta?: ModelMeta; @@ -48,21 +48,9 @@ export interface AdapterBaseOptions { */ zodSchemas?: ZodSchemas | boolean; - /** - * Path to load model metadata and zod schemas from. Defaults to `node_modules/.zenstack`. - */ - loadPath?: string; - /** * Api request handler function. Can be created using `@zenstackhq/server/api/rest` or `@zenstackhq/server/api/rpc` factory functions. * Defaults to RPC-style API handler created with `/api/rpc`. */ handler?: HandleRequestFn; - - /** - * Whether to use superjson for serialization/deserialization. Defaults to `false`. - * - * @deprecated Not needed anymore and will be removed in a future release. - */ - useSuperJson?: boolean; } diff --git a/packages/server/tests/adapter/express.test.ts b/packages/server/tests/adapter/express.test.ts index 14ec66f84..0627990e7 100644 --- a/packages/server/tests/adapter/express.test.ts +++ b/packages/server/tests/adapter/express.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ /// @@ -8,6 +9,7 @@ import request from 'supertest'; import RESTAPIHandler from '../../src/api/rest'; import { ZenStackMiddleware } from '../../src/express'; import { makeUrl, schema } from '../utils'; +import path from 'path'; describe('Express adapter tests - rpc handler', () => { it('run plugin regular json', async () => { @@ -86,11 +88,18 @@ describe('Express adapter tests - rpc handler', () => { }); it('custom load path', async () => { - const { prisma } = await loadSchema(schema, { output: './zen' }); + const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); const app = express(); app.use(bodyParser.json()); - app.use('/api', ZenStackMiddleware({ getPrisma: () => prisma, loadPath: './zen', zodSchemas: true })); + app.use( + '/api', + ZenStackMiddleware({ + getPrisma: () => prisma, + modelMeta: require(path.join(projectDir, './zen/model-meta')).default, + zodSchemas: require(path.join(projectDir, './zen/zod')), + }) + ); const r = await request(app) .post('/api/user/create') diff --git a/packages/server/tests/adapter/fastify.test.ts b/packages/server/tests/adapter/fastify.test.ts index 4e4775d50..f03066e4f 100644 --- a/packages/server/tests/adapter/fastify.test.ts +++ b/packages/server/tests/adapter/fastify.test.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ /// import { loadSchema } from '@zenstackhq/testtools'; import fastify from 'fastify'; +import path from 'path'; import Rest from '../../src/api/rest'; import RPC from '../../src/api/rpc'; import { ZenStackFastifyPlugin } from '../../src/fastify'; @@ -113,14 +115,14 @@ describe('Fastify adapter tests - rpc handler', () => { }); it('custom load path', async () => { - const { prisma } = await loadSchema(schema, { output: './zen' }); + const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); const app = fastify(); app.register(ZenStackFastifyPlugin, { prefix: '/api', getPrisma: () => prisma, - loadPath: './zen', - zodSchemas: true, + modelMeta: require(path.join(projectDir, './zen/model-meta')).default, + zodSchemas: require(path.join(projectDir, './zen/zod')), handler: RPC(), }); diff --git a/packages/server/tests/adapter/nestjs.test.ts b/packages/server/tests/adapter/nestjs.test.ts new file mode 100644 index 000000000..6cfa48617 --- /dev/null +++ b/packages/server/tests/adapter/nestjs.test.ts @@ -0,0 +1,163 @@ +import { Test } from '@nestjs/testing'; +import { loadSchema } from '@zenstackhq/testtools'; +import { ZenStackModule } from '../../src/nestjs'; +import { ENHANCED_PRISMA } from '../../src/nestjs/zenstack.module'; + +describe('NestJS adapter tests', () => { + const schema = ` + model User { + id Int @id @default(autoincrement()) + posts Post[] + @@allow('all', true) + } + + model Post { + id Int @id @default(autoincrement()) + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int + + @@allow('read', published || auth() == author) + } + `; + + it('anonymous', async () => { + const { prisma, enhanceRaw } = await loadSchema(schema); + + await prisma.user.create({ + data: { + posts: { + create: [ + { title: 'post1', published: true }, + { title: 'post2', published: false }, + ], + }, + }, + }); + + const moduleRef = await Test.createTestingModule({ + imports: [ + ZenStackModule.registerAsync({ + useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), + inject: ['PrismaService'], + extraProviders: [ + { + provide: 'PrismaService', + useValue: prisma, + }, + ], + }), + ], + providers: [ + { + provide: 'PostService', + useFactory: (enhancedPrismaService) => ({ + findAll: () => enhancedPrismaService.post.findMany(), + }), + inject: [ENHANCED_PRISMA], + }, + ], + }).compile(); + + const app = moduleRef.createNestApplication(); + await app.init(); + + const postSvc = app.get('PostService'); + await expect(postSvc.findAll()).resolves.toHaveLength(1); + }); + + it('auth user', async () => { + const { prisma, enhanceRaw } = await loadSchema(schema); + + await prisma.user.create({ + data: { + id: 1, + posts: { + create: [ + { title: 'post1', published: true }, + { title: 'post2', published: false }, + ], + }, + }, + }); + + const moduleRef = await Test.createTestingModule({ + imports: [ + ZenStackModule.registerAsync({ + useFactory: (prismaService) => ({ + getEnhancedPrisma: () => enhanceRaw(prismaService, { user: { id: 1 } }), + }), + inject: ['PrismaService'], + extraProviders: [ + { + provide: 'PrismaService', + useValue: prisma, + }, + ], + }), + ], + providers: [ + { + provide: 'PostService', + useFactory: (enhancedPrismaService) => ({ + findAll: () => enhancedPrismaService.post.findMany(), + }), + inject: [ENHANCED_PRISMA], + }, + ], + }).compile(); + + const app = moduleRef.createNestApplication(); + await app.init(); + + const postSvc = app.get('PostService'); + await expect(postSvc.findAll()).resolves.toHaveLength(2); + }); + + it('custom token', async () => { + const { prisma, enhanceRaw } = await loadSchema(schema); + + await prisma.user.create({ + data: { + posts: { + create: [ + { title: 'post1', published: true }, + { title: 'post2', published: false }, + ], + }, + }, + }); + + const moduleRef = await Test.createTestingModule({ + imports: [ + ZenStackModule.registerAsync({ + useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), + inject: ['PrismaService'], + extraProviders: [ + { + provide: 'PrismaService', + useValue: prisma, + }, + ], + exportToken: 'MyEnhancedPrisma', + }), + ], + providers: [ + { + provide: 'PostService', + useFactory: (enhancedPrismaService) => ({ + findAll: () => enhancedPrismaService.post.findMany(), + }), + inject: ['MyEnhancedPrisma'], + }, + ], + }).compile(); + + const app = moduleRef.createNestApplication(); + await app.init(); + + const postSvc = app.get('PostService'); + await expect(postSvc.findAll()).resolves.toHaveLength(1); + }); +}); diff --git a/packages/server/tests/adapter/next.test.ts b/packages/server/tests/adapter/next.test.ts index 4715273d8..54f290ec0 100644 --- a/packages/server/tests/adapter/next.test.ts +++ b/packages/server/tests/adapter/next.test.ts @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { loadSchema } from '@zenstackhq/testtools'; import { createServer, RequestListener } from 'http'; import { apiResolver } from 'next/dist/server/api-utils/node'; +import path from 'path'; import request from 'supertest'; -import { NextRequestHandler, RequestHandlerOptions } from '../../src/next'; import Rest from '../../src/api/rest'; +import { NextRequestHandler, RequestHandlerOptions } from '../../src/next'; function makeTestClient(apiPath: string, options: RequestHandlerOptions, qArg?: unknown, otherArgs?: any) { const pathParts = apiPath.split('/').filter((p) => p); @@ -170,9 +172,13 @@ model M { } `; - const { prisma } = await loadSchema(model, { output: './zen' }); + const { prisma, projectDir } = await loadSchema(model, { output: './zen' }); - await makeTestClient('/m/create', { getPrisma: () => prisma, zodSchemas: true, loadPath: './zen' }) + await makeTestClient('/m/create', { + getPrisma: () => prisma, + modelMeta: require(path.join(projectDir, './zen/model-meta')).default, + zodSchemas: require(path.join(projectDir, './zen/zod')), + }) .post('/') .send({ data: { id: '1', value: 1 } }) .expect(201) diff --git a/packages/server/tests/adapter/sveltekit.test.ts b/packages/server/tests/adapter/sveltekit.test.ts index 534378987..d9663a2b6 100644 --- a/packages/server/tests/adapter/sveltekit.test.ts +++ b/packages/server/tests/adapter/sveltekit.test.ts @@ -1,11 +1,13 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ /// import { loadSchema } from '@zenstackhq/testtools'; -import { SvelteKitHandler } from '../../src/sveltekit'; -import { schema, makeUrl } from '../utils'; import 'isomorphic-fetch'; +import path from 'path'; import superjson from 'superjson'; import Rest from '../../src/api/rest'; +import { SvelteKitHandler } from '../../src/sveltekit'; +import { makeUrl, schema } from '../utils'; describe('SvelteKit adapter tests - rpc handler', () => { it('run hooks regular json', async () => { @@ -80,13 +82,13 @@ describe('SvelteKit adapter tests - rpc handler', () => { }); it('custom load path', async () => { - const { prisma } = await loadSchema(schema, { output: './zen' }); + const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); const handler = SvelteKitHandler({ prefix: '/api', getPrisma: () => prisma, - zodSchemas: true, - loadPath: './zen', + modelMeta: require(path.join(projectDir, './zen/model-meta')).default, + zodSchemas: require(path.join(projectDir, './zen/zod')), }); const r = await handler( diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 7af912ec3..bdb2f4d8c 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /// -import { CrudFailureReason, ModelMeta, withPolicy } from '@zenstackhq/runtime'; +import { CrudFailureReason, type ModelMeta } from '@zenstackhq/runtime'; import { loadSchema, run } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; @@ -1014,7 +1014,7 @@ describe('REST server tests', () => { query: { include: 'posts.comments' }, prisma, }); - expect(r.body.included).toHaveLength(3); + expect(r.body.included).toHaveLength(4); expect(r.body.included[2]).toMatchObject({ type: 'comment', attributes: { content: 'Comment1' }, @@ -1027,7 +1027,7 @@ describe('REST server tests', () => { query: { include: 'posts.comments,profile' }, prisma, }); - expect(r.body.included).toHaveLength(4); + expect(r.body.included).toHaveLength(5); const profile = r.body.included.find((item: any) => item.type === 'profile'); expect(profile).toMatchObject({ type: 'profile', @@ -1333,7 +1333,7 @@ describe('REST server tests', () => { prisma, }); - expect(r.status).toBe(400); + expect(r.status).toBe(422); expect(r.body.errors[0].code).toBe('invalid-payload'); }); @@ -1670,7 +1670,7 @@ describe('REST server tests', () => { prisma, }); - expect(r.status).toBe(400); + expect(r.status).toBe(422); expect(r.body.errors[0].code).toBe('invalid-payload'); }); @@ -1940,7 +1940,7 @@ describe('REST server tests', () => { prisma, }); - expect(r.status).toBe(400); + expect(r.status).toBe(422); expect(r.body.errors[0].code).toBe('invalid-payload'); expect(r.body.errors[0].reason).toBe(CrudFailureReason.DATA_VALIDATION_VIOLATION); expect(r.body.errors[0].zodErrors).toBeTruthy(); @@ -1970,7 +1970,7 @@ describe('REST server tests', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params); + prisma = params.enhanceRaw(params.prisma, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; @@ -2083,7 +2083,7 @@ describe('REST server tests', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params); + prisma = params.enhanceRaw(params.prisma, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; diff --git a/packages/server/tests/api/rpc.test.ts b/packages/server/tests/api/rpc.test.ts index 5d7708745..432abec2c 100644 --- a/packages/server/tests/api/rpc.test.ts +++ b/packages/server/tests/api/rpc.test.ts @@ -5,7 +5,7 @@ import { CrudFailureReason, type ZodSchemas } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; -import RPCAPIHandler from '../../src/api/rpc'; +import { RPCApiHandler } from '../../src/api'; import { schema } from '../utils'; describe('RPC API Handler Tests', () => { @@ -176,7 +176,7 @@ describe('RPC API Handler Tests', () => { path: '/post/findUnique', prisma, }); - expect(r.status).toBe(400); + expect(r.status).toBe(422); expect(r.error.message).toContain('Validation error'); expect(r.error.message).toContain('where'); @@ -187,9 +187,20 @@ describe('RPC API Handler Tests', () => { prisma, zodSchemas, }); - expect(r.status).toBe(400); + expect(r.status).toBe(422); expect(r.error.message).toContain('Validation error'); expect(r.error.message).toContain('data'); + + r = await handleRequest({ + method: 'post', + path: '/user/create', + requestBody: { data: { email: 'hello' } }, + prisma: enhance(), + zodSchemas, + }); + expect(r.status).toBe(422); + expect(r.error.message).toContain('Validation error'); + expect(r.error.message).toContain('email'); }); it('invalid path or args', async () => { @@ -397,7 +408,7 @@ describe('RPC API Handler Tests', () => { }); function makeHandler(zodSchemas?: ZodSchemas) { - const _handler = RPCAPIHandler(); + const _handler = RPCApiHandler(); return async (args: any) => { const r = await _handler({ ...args, url: new URL(`http://localhost/${args.path}`), modelMeta, zodSchemas }); return { diff --git a/packages/server/tests/utils.ts b/packages/server/tests/utils.ts index 472a6818d..78cc38345 100644 --- a/packages/server/tests/utils.ts +++ b/packages/server/tests/utils.ts @@ -5,11 +5,11 @@ model User { id String @id @default(cuid()) createdAt DateTime @default (now()) updatedAt DateTime @updatedAt - email String @unique + email String @unique @email posts Post[] @@allow('all', auth() == this) - @@allow('read', true) + @@allow('create,read', true) } model Post { diff --git a/packages/testtools/CHANGELOG.md b/packages/testtools/CHANGELOG.md new file mode 100644 index 000000000..cc2a59fdc --- /dev/null +++ b/packages/testtools/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) + + +### Miscellaneous Chores + +* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 02c05d54a..d12aa129f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.12.4", + "version": "2.0.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, @@ -19,13 +19,13 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "json5": "^2.2.3", "langium": "1.3.1", "pg": "^8.11.1", + "tiny-invariant": "^1.3.1", "tmp": "^0.2.1", "vscode-uri": "^3.0.6", "zenstack": "workspace:*" diff --git a/tests/integration/utils/jest-ext.ts b/packages/testtools/src/jest-ext.ts similarity index 98% rename from tests/integration/utils/jest-ext.ts rename to packages/testtools/src/jest-ext.ts index ee24741a5..244ba40f3 100644 --- a/tests/integration/utils/jest-ext.ts +++ b/packages/testtools/src/jest-ext.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { format } from 'util'; import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; diff --git a/packages/testtools/src/model.ts b/packages/testtools/src/model.ts index 310a2c019..adbc89453 100644 --- a/packages/testtools/src/model.ts +++ b/packages/testtools/src/model.ts @@ -18,7 +18,7 @@ export class SchemaLoadingError extends Error { export async function loadModel(content: string, validate = true, verbose = true) { const { name: docPath } = tmp.fileSync({ postfix: '.zmodel' }); fs.writeFileSync(docPath, content); - const { shared } = createZModelServices(NodeFileSystem); + const { shared, ZModel } = createZModelServices(NodeFileSystem); const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( URI.file(path.resolve(__dirname, '../../schema/src/res/stdlib.zmodel')) ); @@ -53,7 +53,7 @@ export async function loadModel(content: string, validate = true, verbose = true const model = (await doc.parseResult.value) as Model; - mergeBaseModel(model); + mergeBaseModel(model, ZModel.references.Linker); return model; } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index a6885237f..7249e6c4a 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,9 +1,14 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { DMMF } from '@prisma/generator-helper'; import type { Model } from '@zenstackhq/language/ast'; -import { enhance, withOmit, withPassword, withPolicy, type AuthUser, type DbOperations } from '@zenstackhq/runtime'; -import { getDMMF } from '@zenstackhq/sdk'; +import { + DEFAULT_RUNTIME_LOAD_PATH, + type AuthUser, + type CrudContract, + type EnhancementKind, + type EnhancementOptions, +} from '@zenstackhq/runtime'; +import { getDMMF, type DMMF } from '@zenstackhq/sdk/prisma'; import { execSync } from 'child_process'; import * as fs from 'fs'; import json from 'json5'; @@ -26,7 +31,7 @@ export const FILE_SPLITTER = '#FILE_SPLITTER#'; tmp.setGracefulCleanup(); -export type FullDbClientContract = Record & { +export type FullDbClientContract = CrudContract & { $on(eventType: any, callback: (event: any) => void): void; $use(cb: any): void; $disconnect: () => Promise; @@ -92,22 +97,16 @@ datasource db { generator js { provider = 'prisma-client-js' - previewFeatures = ['clientExtensions'] } -plugin meta { - provider = '@core/model-meta' - // preserveTsFiles = true -} - -plugin policy { - provider = '@core/access-policy' - // preserveTsFiles = true +plugin enhancer { + provider = '@core/enhancer' + ${options.preserveTsFiles ? 'preserveTsFiles = true' : ''} } plugin zod { provider = '@core/zod' - preserveTsFiles = true + ${options.preserveTsFiles ? 'preserveTsFiles = true' : ''} modelOnly = ${!options.fullZod} } `; @@ -127,6 +126,11 @@ export type SchemaLoadOptions = { dbUrl?: string; pulseApiKey?: string; getPrismaOnly?: boolean; + enhancements?: EnhancementKind[]; + enhanceOptions?: Partial; + extraSourceFiles?: { name: string; content: string }[]; + projectDir?: string; + preserveTsFiles?: boolean; }; const defaultOptions: SchemaLoadOptions = { @@ -137,6 +141,7 @@ const defaultOptions: SchemaLoadOptions = { compile: false, logPrismaQuery: false, provider: 'sqlite', + preserveTsFiles: false, }; export async function loadSchemaFromFile(schemaFile: string, options?: SchemaLoadOptions) { @@ -147,7 +152,11 @@ export async function loadSchemaFromFile(schemaFile: string, options?: SchemaLoa export async function loadSchema(schema: string, options?: SchemaLoadOptions) { const opt = { ...defaultOptions, ...options }; - const { name: projectRoot } = tmp.dirSync({ unsafeCleanup: true }); + let projectDir = opt.projectDir; + if (!projectDir) { + const r = tmp.dirSync({ unsafeCleanup: true }); + projectDir = r.name; + } const workspaceRoot = getWorkspaceRoot(__dirname); @@ -155,11 +164,11 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { throw new Error('Could not find workspace root'); } - console.log('Workdir:', projectRoot); - process.chdir(projectRoot); + console.log('Workdir:', projectDir); + process.chdir(projectDir); // copy project structure from scaffold (prepared by test-setup.ts) - fs.cpSync(path.join(workspaceRoot, '.test/scaffold'), projectRoot, { recursive: true, force: true }); + fs.cpSync(path.join(workspaceRoot, '.test/scaffold'), projectDir, { recursive: true, force: true }); // install local deps const localInstallDeps = [ @@ -172,12 +181,12 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { run(`npm i --no-audit --no-fund ${localInstallDeps.map((d) => path.join(workspaceRoot, d)).join(' ')}`); - let zmodelPath = path.join(projectRoot, 'schema.zmodel'); + let zmodelPath = path.join(projectDir, 'schema.zmodel'); const files = schema.split(FILE_SPLITTER); // Use this one to replace $projectRoot placeholder in the schema file - const normalizedProjectRoot = normalizePath(projectRoot); + const normalizedProjectRoot = normalizePath(projectDir); if (files.length > 1) { // multiple files @@ -188,7 +197,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { let fileContent = file.substring(firstLine + 1); if (index === 0) { // The first file is the main schema file - zmodelPath = path.join(projectRoot, fileName); + zmodelPath = path.join(projectDir, fileName); if (opt.addPrelude) { // plugin need to be added after import statement fileContent = `${fileContent}\n${makePrelude(opt)}`; @@ -196,14 +205,14 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { } fileContent = fileContent.replaceAll('$projectRoot', normalizedProjectRoot); - const filePath = path.join(projectRoot, fileName); + const filePath = path.join(projectDir, fileName); fs.writeFileSync(filePath, fileContent); }); } else { schema = schema.replaceAll('$projectRoot', normalizedProjectRoot); const content = opt.addPrelude ? `${makePrelude(opt)}\n${schema}` : schema; if (opt.customSchemaFilePath) { - zmodelPath = path.join(projectRoot, opt.customSchemaFilePath); + zmodelPath = path.join(projectDir, opt.customSchemaFilePath); fs.mkdirSync(path.dirname(zmodelPath), { recursive: true }); fs.writeFileSync(zmodelPath, content); } else { @@ -238,90 +247,100 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { opt.copyDependencies?.forEach((dep) => { const pkgJson = JSON.parse(fs.readFileSync(path.join(dep, 'package.json'), { encoding: 'utf-8' })); - fs.cpSync(dep, path.join(projectRoot, 'node_modules', pkgJson.name), { recursive: true, force: true }); + fs.cpSync(dep, path.join(projectDir, 'node_modules', pkgJson.name), { recursive: true, force: true }); }); - const PrismaClient = require(path.join(projectRoot, 'node_modules/.prisma/client')).PrismaClient; + const PrismaClient = require(path.join(projectDir, 'node_modules/.prisma/client')).PrismaClient; let prisma = new PrismaClient({ log: ['info', 'warn', 'error'] }); // https://github.com/prisma/prisma/issues/18292 prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient'; - const Prisma = require(path.join(projectRoot, 'node_modules/@prisma/client')).Prisma; + const prismaModule = require(path.join(projectDir, 'node_modules/@prisma/client')).Prisma; if (opt.pulseApiKey) { - const withPulse = require(path.join(projectRoot, 'node_modules/@prisma/extension-pulse/dist/cjs')).withPulse; + const withPulse = require(path.join(projectDir, 'node_modules/@prisma/extension-pulse/dist/cjs')).withPulse; prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey })); } + opt.extraSourceFiles?.forEach(({ name, content }) => { + fs.writeFileSync(path.join(projectDir, name), content); + }); + + if (opt.extraSourceFiles && opt.extraSourceFiles.length > 0 && !opt.compile) { + console.warn('`extraSourceFiles` is true but `compile` is false.'); + } + if (opt.compile) { console.log('Compiling...'); + run('npx tsc --init'); // add generated '.zenstack/zod' folder to typescript's search path, // so that it can be resolved from symbolic-linked files - const tsconfig = json.parse(fs.readFileSync(path.join(projectRoot, './tsconfig.json'), 'utf-8')); + const tsconfig = json.parse(fs.readFileSync(path.join(projectDir, './tsconfig.json'), 'utf-8')); tsconfig.compilerOptions.paths = { '.zenstack/zod/input': ['./node_modules/.zenstack/zod/input/index.d.ts'], + '.zenstack/models': ['./node_modules/.zenstack/models.d.ts'], }; - fs.writeFileSync(path.join(projectRoot, './tsconfig.json'), JSON.stringify(tsconfig, null, 2)); + tsconfig.include = ['**/*.ts']; + tsconfig.exclude = ['node_modules']; + fs.writeFileSync(path.join(projectDir, './tsconfig.json'), JSON.stringify(tsconfig, null, 2)); run('npx tsc --project tsconfig.json'); } if (options?.getPrismaOnly) { return { prisma, - Prisma, - projectDir: projectRoot, - withPolicy: undefined as any, - withOmit: undefined as any, - withPassword: undefined as any, + prismaModule, + projectDir, enhance: undefined as any, + enhanceRaw: undefined as any, + policy: undefined as any, + modelMeta: undefined as any, + zodSchemas: undefined as any, }; } - let policy: any; - let modelMeta: any; - let zodSchemas: any; + const outputPath = opt.output + ? path.isAbsolute(opt.output) + ? opt.output + : path.join(projectDir, opt.output) + : path.join(projectDir, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); - const outputPath = path.join(projectRoot, 'node_modules'); + const policy = require(path.join(outputPath, 'policy')).default; + const modelMeta = require(path.join(outputPath, 'model-meta')).default; + let zodSchemas: any; try { - policy = require(path.join(outputPath, '.zenstack/policy')).default; - } catch { - /* noop */ - } - try { - modelMeta = require(path.join(outputPath, '.zenstack/model-meta')).default; - } catch { - /* noop */ - } - try { - zodSchemas = require(path.join(outputPath, '.zenstack/zod')); + zodSchemas = require(path.join(outputPath, 'zod')); } catch { /* noop */ } + const enhance = require(path.join(outputPath, 'enhance')).enhance; + return { - projectDir: projectRoot, + projectDir: projectDir, prisma, - Prisma, - withPolicy: (user?: AuthUser) => - withPolicy( + enhance: (user?: AuthUser, options?: EnhancementOptions): FullDbClientContract => + enhance( prisma, { user }, - { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery } - ), - withOmit: () => withOmit(prisma, { modelMeta }), - withPassword: () => withPassword(prisma, { modelMeta }), - enhance: (user?: AuthUser) => - enhance( - prisma, - { user }, - { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery } + { + policy, + modelMeta, + zodSchemas, + logPrismaQuery: opt.logPrismaQuery, + transactionTimeout: 1000000, + kinds: opt.enhancements, + ...(options ?? opt.enhanceOptions), + } ), + enhanceRaw: enhance, policy, modelMeta, zodSchemas, + prismaModule, }; } @@ -346,7 +365,17 @@ export async function loadZModelAndDmmf( const model = await loadDocument(modelFile); const { name: prismaFile } = tmp.fileSync({ postfix: '.prisma' }); - await prismaPlugin(model, { schemaPath: modelFile, name: 'Prisma', output: prismaFile, generateClient: false }); + await prismaPlugin( + model, + { + provider: '@core/plugin', + schemaPath: modelFile, + output: prismaFile, + generateClient: false, + }, + undefined, + undefined + ); const prismaContent = fs.readFileSync(prismaFile, { encoding: 'utf-8' }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b432b95c7..8af4e228d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: ^20.10.2 version: 20.10.2 '@typescript-eslint/eslint-plugin': - specifier: ^6.13.1 - version: 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.55.0)(typescript@5.3.2) + specifier: ^7.6.0 + version: 7.6.0(@typescript-eslint/parser@7.6.0)(eslint@8.57.0)(typescript@5.4.4) '@typescript-eslint/parser': - specifier: ^6.13.1 - version: 6.13.1(eslint@8.55.0)(typescript@5.3.2) + specifier: ^7.6.0 + version: 7.6.0(eslint@8.57.0)(typescript@5.4.4) concurrently: specifier: ^7.4.0 version: 7.4.0 @@ -30,11 +30,11 @@ importers: specifier: ^2.4.1 version: 2.4.1 eslint: - specifier: ^8.55.0 - version: 8.55.0 + specifier: ^8.56.0 + version: 8.57.0 eslint-plugin-jest: - specifier: ^27.6.0 - version: 27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.55.0)(jest@29.7.0)(typescript@5.3.2) + specifier: ^28.2.0 + version: 28.2.0(@typescript-eslint/eslint-plugin@7.6.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.4) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.10.2)(ts-node@10.9.1) @@ -46,19 +46,19 @@ importers: version: 3.0.2 ts-jest: specifier: ^29.1.1 - version: 29.1.1(@babel/core@7.23.2)(esbuild@0.19.4)(jest@29.7.0)(typescript@5.3.2) + version: 29.1.1(@babel/core@7.23.2)(esbuild@0.19.4)(jest@29.7.0)(typescript@5.4.4) ts-node: specifier: ^10.9.1 - version: 10.9.1(@types/node@20.10.2)(typescript@5.3.2) + version: 10.9.1(@types/node@20.10.2)(typescript@5.4.4) tsup: specifier: ^8.0.1 - version: 8.0.1(ts-node@10.9.1)(typescript@5.3.2) + version: 8.0.1(ts-node@10.9.1)(typescript@5.4.4) tsx: specifier: ^4.7.1 version: 4.7.1 typescript: - specifier: ^5.3.2 - version: 5.3.2 + specifier: ^5.4.4 + version: 5.4.4 packages/ide/jetbrains: devDependencies: @@ -122,9 +122,6 @@ importers: packages/plugins/openapi: dependencies: - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/runtime': specifier: workspace:* version: link:../../runtime/dist @@ -190,9 +187,6 @@ importers: packages/plugins/swr: dependencies: - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/runtime': specifier: workspace:* version: link:../../runtime/dist @@ -252,9 +246,6 @@ importers: packages/plugins/tanstack-query: dependencies: - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/runtime': specifier: workspace:* version: link:../../runtime/dist @@ -282,6 +273,9 @@ importers: ts-morph: specifier: ^16.0.0 version: 16.0.0 + ts-pattern: + specifier: ^4.3.0 + version: 4.3.0 upper-case-first: specifier: ^2.0.2 version: 2.0.2 @@ -304,9 +298,6 @@ importers: '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0)(react@18.2.0) - '@types/nock': - specifier: ^11.1.0 - version: 11.1.0 '@types/react': specifier: 18.2.0 version: 18.2.0 @@ -347,9 +338,6 @@ importers: packages/plugins/trpc: dependencies: - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/sdk': specifier: workspace:* version: link:../../sdk/dist @@ -384,19 +372,25 @@ importers: '@types/prettier': specifier: ^2.7.2 version: 2.7.2 + '@types/tmp': + specifier: ^0.2.3 + version: 0.2.3 '@zenstackhq/testtools': specifier: workspace:* version: link:../../testtools/dist next: specifier: ^13.4.7 version: 13.4.7(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0) + tmp: + specifier: ^0.2.3 + version: 0.2.3 publishDirectory: dist packages/runtime: dependencies: - '@types/bcryptjs': - specifier: ^2.4.2 - version: 2.4.2 + '@prisma/client': + specifier: 5.0.0 - 5.12.x + version: 5.12.0(prisma@5.12.0) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -406,21 +400,24 @@ importers: change-case: specifier: ^4.1.2 version: 4.1.2 - colors: - specifier: 1.4.0 - version: 1.4.0 decimal.js: specifier: ^10.4.2 version: 10.4.2 deepcopy: specifier: ^2.1.0 version: 2.1.0 + deepmerge: + specifier: ^4.3.1 + version: 4.3.1 lower-case-first: specifier: ^2.0.2 version: 2.0.2 pluralize: specifier: ^8.0.0 version: 8.0.0 + safe-json-stringify: + specifier: ^1.2.0 + version: 1.2.0 semver: specifier: ^7.5.2 version: 7.5.4 @@ -446,9 +443,15 @@ importers: specifier: ^1.5.0 version: 1.5.0(zod@3.22.4) devDependencies: + '@types/bcryptjs': + specifier: ^2.4.2 + version: 2.4.2 '@types/pluralize': specifier: ^0.0.29 version: 0.0.29 + '@types/safe-json-stringify': + specifier: ^1.1.5 + version: 1.1.5 '@types/semver': specifier: ^7.3.13 version: 7.5.0 @@ -462,9 +465,9 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.0 version: 2.2.0 - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 + '@types/node': + specifier: ^20.12.7 + version: 20.12.7 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -554,8 +557,8 @@ importers: version: 1.5.0(zod@3.22.4) devDependencies: '@prisma/client': - specifier: ^4.8.0 - version: 4.16.2(prisma@4.16.2) + specifier: 5.12.0 + version: 5.12.0(prisma@5.12.0) '@types/async-exit-hook': specifier: ^2.0.0 version: 2.0.0 @@ -590,8 +593,8 @@ importers: specifier: ^0.15.12 version: 0.15.12 prisma: - specifier: ^4.8.0 - version: 4.16.2 + specifier: 5.12.0 + version: 5.12.0 renamer: specifier: ^4.0.0 version: 4.0.0 @@ -609,32 +612,32 @@ importers: packages/sdk: dependencies: '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 + specifier: 5.7.0 + version: 5.7.0 '@prisma/internals': - specifier: ^4.16.0 - version: 4.16.2 - '@prisma/internals-v5': - specifier: npm:@prisma/internals@^5.0.0 - version: /@prisma/internals@5.0.0 + specifier: 5.7.0 + version: 5.7.0 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime/dist + langium: + specifier: 1.3.1 + version: 1.3.1 lower-case-first: specifier: ^2.0.2 version: 2.0.2 - prettier: - specifier: ^2.8.3 || 3.x - version: 2.8.8 semver: specifier: ^7.5.2 version: 7.5.4 ts-morph: specifier: ^16.0.0 version: 16.0.0 + ts-pattern: + specifier: ^4.3.0 + version: 4.3.0 upper-case-first: specifier: ^2.0.2 version: 2.0.2 @@ -662,8 +665,8 @@ importers: specifier: ^1.3.1 version: 1.3.1 ts-japi: - specifier: ^1.8.0 - version: 1.8.0 + specifier: ^1.10.1 + version: 1.10.1 upper-case-first: specifier: ^2.0.2 version: 2.0.2 @@ -677,6 +680,15 @@ importers: specifier: ^1.5.0 version: 1.5.0(zod@3.22.4) devDependencies: + '@nestjs/common': + specifier: ^10.3.7 + version: 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/platform-express': + specifier: ^10.3.7 + version: 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5) + '@nestjs/testing': + specifier: ^10.3.7 + version: 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5)(@nestjs/platform-express@10.3.7) '@sveltejs/kit': specifier: 1.21.0 version: 1.21.0(svelte@4.2.1)(vite@4.4.11) @@ -718,7 +730,10 @@ importers: version: 13.4.5(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0) nuxt: specifier: ^3.7.4 - version: 3.7.4(@types/node@20.10.2)(eslint@8.55.0)(typescript@5.3.2) + version: 3.7.4(@types/node@20.10.2)(eslint@8.57.0)(typescript@5.4.4) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 supertest: specifier: ^6.3.3 version: 6.3.3 @@ -726,9 +741,6 @@ importers: packages/testtools: dependencies: - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -747,6 +759,9 @@ importers: pg: specifier: ^8.11.1 version: 8.11.1 + tiny-invariant: + specifier: ^1.3.1 + version: 1.3.1 tmp: specifier: ^0.2.1 version: 0.2.1 @@ -841,6 +856,31 @@ importers: specifier: 'workspace: *' version: link:../../packages/schema/dist + tests/regression: + dependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.0.0 + '@zenstackhq/sdk': + specifier: workspace:* + version: link:../../packages/sdk/dist + '@zenstackhq/testtools': + specifier: workspace:* + version: link:../../packages/testtools/dist + decimal.js: + specifier: ^10.4.2 + version: 10.4.2 + devDependencies: + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../../packages/runtime/dist + '@zenstackhq/server': + specifier: workspace:* + version: link:../../packages/server/dist + zenstack: + specifier: 'workspace: *' + version: link:../../packages/schema/dist + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -855,11 +895,6 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.18 - /@antfu/ni@0.21.4: - resolution: {integrity: sha512-O0Uv9LbLDSoEg26fnMDdDRiPwFJnQSoD4WnrflDwKCJm8Cx/0mV4cGxwBLXan5mGIrpK4Dd7vizf4rQm0QCEAA==} - hasBin: true - dev: false - /@apidevtools/openapi-schemas@2.1.0: resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} engines: {node: '>=10'} @@ -1524,7 +1559,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@envelop/types': 4.0.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@envelop/core@5.0.0: @@ -1532,7 +1567,7 @@ packages: engines: {node: '>=18.0.0'} dependencies: '@envelop/types': 5.0.0 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@envelop/depth-limit@3.0.3(@envelop/core@4.0.3)(graphql@16.8.1): @@ -1545,7 +1580,7 @@ packages: '@envelop/core': 4.0.3 graphql: 16.8.1 graphql-depth-limit: 1.1.0(graphql@16.8.1) - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@envelop/disable-introspection@5.0.3(@envelop/core@4.0.3)(graphql@16.8.1): @@ -1557,7 +1592,7 @@ packages: dependencies: '@envelop/core': 4.0.3 graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@envelop/filter-operation-type@5.0.3(@envelop/core@4.0.3)(graphql@16.8.1): @@ -1569,7 +1604,7 @@ packages: dependencies: '@envelop/core': 4.0.3 graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@envelop/on-resolve@3.0.3(@envelop/core@4.0.3)(graphql@16.8.1): @@ -1587,14 +1622,14 @@ packages: resolution: {integrity: sha512-ULo27/doEsP7uUhm2iTnElx13qTO6I5FKvmLoX41cpfuw8x6e0NUFknoqhEsLzAbgz8xVS5mjwcxGCXh4lDYzg==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@envelop/types@5.0.0: resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} engines: {node: '>=18.0.0'} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@esbuild/aix-ppc64@0.19.12: @@ -2508,13 +2543,13 @@ packages: graphql: 16.8.1 dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.55.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.55.0 + eslint: 8.57.0 eslint-visitor-keys: 3.4.3 dev: true @@ -2531,7 +2566,7 @@ packages: debug: 4.3.4 espree: 9.6.1 globals: 13.20.0 - ignore: 5.2.4 + ignore: 5.3.1 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -2540,8 +2575,8 @@ packages: - supports-color dev: true - /@eslint/js@8.55.0: - resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -2582,7 +2617,7 @@ packages: '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) '@repeaterjs/repeater': 3.0.5 graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 value-or-promise: 1.0.12 dev: true @@ -2594,7 +2629,7 @@ packages: dependencies: '@graphql-tools/utils': 10.0.11(graphql@16.8.1) graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-tools/schema@10.0.2(graphql@16.8.1): @@ -2606,7 +2641,7 @@ packages: '@graphql-tools/merge': 9.0.1(graphql@16.8.1) '@graphql-tools/utils': 10.0.11(graphql@16.8.1) graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 value-or-promise: 1.0.12 dev: true @@ -2620,7 +2655,7 @@ packages: cross-inspect: 1.0.0 dset: 3.1.3 graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-typed-document-node/core@3.2.0(graphql@16.8.1): @@ -2635,14 +2670,14 @@ packages: resolution: {integrity: sha512-JYoxwnPggH2BfO+dWlWZkDeFhyFZqaTRGLvFhy+Pjp2UxitEW6nDrw+pEDw/K9tJwMjIFMmTT9VfTqrnESmBHg==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-yoga/logger@2.0.0: resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} engines: {node: '>=18.0.0'} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-yoga/subscription@4.0.0: @@ -2652,7 +2687,7 @@ packages: '@graphql-yoga/typed-event-target': 2.0.0 '@repeaterjs/repeater': 3.0.5 '@whatwg-node/events': 0.1.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-yoga/subscription@5.0.0: @@ -2662,7 +2697,7 @@ packages: '@graphql-yoga/typed-event-target': 3.0.0 '@repeaterjs/repeater': 3.0.5 '@whatwg-node/events': 0.1.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-yoga/typed-event-target@2.0.0: @@ -2670,7 +2705,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@repeaterjs/repeater': 3.0.5 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@graphql-yoga/typed-event-target@3.0.0: @@ -2678,14 +2713,14 @@ packages: engines: {node: '>=18.0.0'} dependencies: '@repeaterjs/repeater': 3.0.5 - tslib: 2.6.0 + tslib: 2.6.2 dev: true - /@humanwhocodes/config-array@0.11.13: - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} dependencies: - '@humanwhocodes/object-schema': 2.0.1 + '@humanwhocodes/object-schema': 2.0.3 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: @@ -2702,8 +2737,8 @@ packages: engines: {node: '>=10.10.0'} dev: true - /@humanwhocodes/object-schema@2.0.1: - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} dev: true /@iarna/toml@2.2.5: @@ -2735,7 +2770,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -2756,14 +2791,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@18.0.0)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@20.10.2)(ts-node@10.9.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -2791,7 +2826,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 jest-mock: 29.7.0 dev: true @@ -2818,7 +2853,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 18.0.0 + '@types/node': 20.10.2 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2851,7 +2886,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 18.0.0 + '@types/node': 20.10.2 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -2939,7 +2974,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.0.0 + '@types/node': 20.10.2 '@types/yargs': 17.0.24 chalk: 4.1.2 dev: true @@ -2998,6 +3033,11 @@ packages: /@kamilkisiela/fast-url-parser@1.1.4: resolution: {integrity: sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==} + /@lukeed/csprng@1.1.0: + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + dev: true + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -3036,6 +3076,94 @@ packages: - supports-color dev: true + /@nestjs/common@10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1): + resolution: {integrity: sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + dev: true + + /@nestjs/core@10.3.5(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1): + resolution: {integrity: sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==} + requiresBuild: true + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + dependencies: + '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/platform-express': 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5) + '@nuxtjs/opencollective': 0.3.2 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.2.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + transitivePeerDependencies: + - encoding + dev: true + + /@nestjs/platform-express@10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5): + resolution: {integrity: sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + dependencies: + '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.5(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + body-parser: 1.20.2 + cors: 2.8.5 + express: 4.19.2 + multer: 1.4.4-lts.1 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@nestjs/testing@10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5)(@nestjs/platform-express@10.3.7): + resolution: {integrity: sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + dependencies: + '@nestjs/common': 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.5(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/platform-express': 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5) + tslib: 2.6.2 + dev: true + /@netlify/functions@2.2.1: resolution: {integrity: sha512-qx/yZF0/8mZHhxLG6U/OvHyq/lHsPXNeAtw85ELW7YYYXV4kVXnpjx1sM3H/eL+FiFTG8LwJes16agWingM2iQ==} engines: {node: '>=14.0.0'} @@ -3452,7 +3580,7 @@ packages: resolution: {integrity: sha512-5gc02Pu1HycOVUWJ8aYsWeeXcSTPe8iX8+KIrhyEtEoOSkY0eMBuo0ssljB8wALuEmepv31DlYe5gpiRwkjESA==} dev: true - /@nuxt/vite-builder@3.7.4(@types/node@20.10.2)(eslint@8.55.0)(typescript@5.3.2)(vue@3.3.4): + /@nuxt/vite-builder@3.7.4(@types/node@20.10.2)(eslint@8.57.0)(typescript@5.4.4)(vue@3.3.4): resolution: {integrity: sha512-EWZlUzYvkSfIZPA0pQoi7P++68Mlvf5s/G3GBPksS5JB/9l3yZTX+ZqGvLeORSBmoEpJ6E2oMn2WvCHV0W5y6Q==} engines: {node: ^14.18.0 || >=16.10.0} peerDependencies: @@ -3491,7 +3619,7 @@ packages: unplugin: 1.5.0 vite: 4.4.11(@types/node@20.10.2) vite-node: 0.33.0(@types/node@20.10.2) - vite-plugin-checker: 0.6.2(eslint@8.55.0)(typescript@5.3.2)(vite@4.4.11) + vite-plugin-checker: 0.6.2(eslint@8.57.0)(typescript@5.4.4)(vite@4.4.11) vue: 3.3.4 vue-bundle-renderer: 2.0.0 transitivePeerDependencies: @@ -3514,10 +3642,17 @@ packages: - vue-tsc dev: true - /@opentelemetry/api@1.4.1: - resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} - engines: {node: '>=8.0.0'} - dev: false + /@nuxtjs/opencollective@0.3.2: + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + node-fetch: 2.6.12 + transitivePeerDependencies: + - encoding + dev: true /@opentelemetry/api@1.7.0: resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==} @@ -3696,9 +3831,9 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true - /@prisma/client@4.16.2(prisma@4.16.2): - resolution: {integrity: sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==} - engines: {node: '>=14.17'} + /@prisma/client@5.12.0(prisma@5.12.0): + resolution: {integrity: sha512-bk/+KPpRm0+IzqFCtAxrj+/TNiHzulspnO+OkysaYY/atc/eX0Gx8V3tTLxbHKVX0LKD4Hi8KKCcSbU1U72n7Q==} + engines: {node: '>=16.13'} requiresBuild: true peerDependencies: prisma: '*' @@ -3706,9 +3841,7 @@ packages: prisma: optional: true dependencies: - '@prisma/engines-version': 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 - prisma: 4.16.2 - dev: true + prisma: 5.12.0 /@prisma/client@5.7.0: resolution: {integrity: sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==} @@ -3721,46 +3854,28 @@ packages: optional: true dev: true - /@prisma/debug@4.16.2: - resolution: {integrity: sha512-7L7WbG0qNNZYgLpsVB8rCHCXEyHFyIycRlRDNwkVfjQmACC2OW6AWCYCbfdjQhkF/t7+S3njj8wAWAocSs+Brw==} - dependencies: - '@types/debug': 4.1.8 - debug: 4.3.4 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /@prisma/debug@5.0.0: - resolution: {integrity: sha512-3q/M/KqlQ01/HJXifU/zCNOHkoTWu24kGelMF/IBrRxm7njPqTTbwfnT1dh4JK+nuWM5/Dg1Lv00u2c0l7AHxg==} - dependencies: - '@types/debug': 4.1.8 - debug: 4.3.4 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - dev: false + /@prisma/debug@5.12.0: + resolution: {integrity: sha512-wK3fQLxPLMqf5riT5ZIhl8NffPSzFUwtzFX5CH7z/oI9Swmo9UhQlUgZABIVgdXSJ5OAlmRcDZtDKaMApIl8sg==} /@prisma/debug@5.7.0: resolution: {integrity: sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==} dev: false - /@prisma/engines-version@4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81: - resolution: {integrity: sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==} - dev: true + /@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab: + resolution: {integrity: sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==} /@prisma/engines-version@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9: resolution: {integrity: sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==} dev: false - /@prisma/engines@4.16.2: - resolution: {integrity: sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==} - requiresBuild: true - - /@prisma/engines@5.0.0: - resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==} + /@prisma/engines@5.12.0: + resolution: {integrity: sha512-rFNRul9JGu0d3tf8etBgmDQ4NVoDwgGrRguvQOc8i+c6g7xPjRuu4aKzMMvHWUuccvRx5+fs1KMBxQ0x2THt+Q==} requiresBuild: true - dev: false + dependencies: + '@prisma/debug': 5.12.0 + '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab + '@prisma/fetch-engine': 5.12.0 + '@prisma/get-platform': 5.12.0 /@prisma/engines@5.7.0: resolution: {integrity: sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==} @@ -3772,55 +3887,12 @@ packages: '@prisma/get-platform': 5.7.0 dev: false - /@prisma/fetch-engine@4.16.2: - resolution: {integrity: sha512-lnCnHcOaNn0kw8qTJbVcNhyfIf5Lus2GFXbj3qpkdKEIB9xLgqkkuTP+35q1xFaqwQ0vy4HFpdRUpFP7njE15g==} + /@prisma/fetch-engine@5.12.0: + resolution: {integrity: sha512-qkHQbZ1hspvOwcImvqY4yj7+FUlw0+uP+6tu3g24V4ULHOXLLkvr5ZZc6vy26OF0hkbD3kcDJCeutFis3poKgg==} dependencies: - '@prisma/debug': 4.16.2 - '@prisma/get-platform': 4.16.2 - execa: 5.1.1 - find-cache-dir: 3.3.2 - fs-extra: 11.1.1 - hasha: 5.2.2 - http-proxy-agent: 7.0.0 - https-proxy-agent: 7.0.0 - kleur: 4.1.5 - node-fetch: 2.6.11 - p-filter: 2.1.0 - p-map: 4.0.0 - p-retry: 4.6.2 - progress: 2.0.3 - rimraf: 3.0.2 - temp-dir: 2.0.0 - tempy: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /@prisma/fetch-engine@5.0.0: - resolution: {integrity: sha512-eSzHTE0KcMvM5+O1++eaMuVf4D1zwWHdqjWr6D70skCg37q7RYsuty4GFnlWBuqC4aXwVf06EvIxiJ0SQIIeRw==} - dependencies: - '@prisma/debug': 5.0.0 - '@prisma/get-platform': 5.0.0 - execa: 5.1.1 - find-cache-dir: 3.3.2 - fs-extra: 11.1.1 - hasha: 5.2.2 - http-proxy-agent: 7.0.0 - https-proxy-agent: 7.0.0 - kleur: 4.1.5 - node-fetch: 2.6.12 - p-filter: 2.1.0 - p-map: 4.0.0 - p-retry: 4.6.2 - progress: 2.0.3 - rimraf: 3.0.2 - temp-dir: 2.0.0 - tempy: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: false + '@prisma/debug': 5.12.0 + '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab + '@prisma/get-platform': 5.12.0 /@prisma/fetch-engine@5.7.0: resolution: {integrity: sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==} @@ -3830,67 +3902,16 @@ packages: '@prisma/get-platform': 5.7.0 dev: false - /@prisma/generator-helper@4.16.2: - resolution: {integrity: sha512-bMOH7y73Ui7gpQrioFeavMQA+Tf8ksaVf8Nhs9rQNzuSg8SSV6E9baczob0L5KGZTSgYoqnrRxuo03kVJYrnIg==} - dependencies: - '@prisma/debug': 4.16.2 - '@types/cross-spawn': 6.0.2 - cross-spawn: 7.0.3 - kleur: 4.1.5 - transitivePeerDependencies: - - supports-color - dev: false - - /@prisma/generator-helper@5.0.0: - resolution: {integrity: sha512-pufQ1mhoH6WzKNtzL79HZDoW4Ql3Lf8QEKVmBoW8e3Tdb50bxpYBYue5LBqp9vNW1xd1pgZO53cNiRfLX2d4Zg==} - dependencies: - '@prisma/debug': 5.0.0 - '@types/cross-spawn': 6.0.2 - cross-spawn: 7.0.3 - kleur: 4.1.5 - transitivePeerDependencies: - - supports-color - dev: false - /@prisma/generator-helper@5.7.0: resolution: {integrity: sha512-Fn4hJHKGJ49+E8sxpfslRauB3Goa3RAENJ/W25NMR754B9KxvmbCJyE3MT/lIZxML2nGgIdXYUtoDHZHnRaKDw==} dependencies: '@prisma/debug': 5.7.0 dev: false - /@prisma/get-platform@4.16.2: - resolution: {integrity: sha512-fnDey1/iSefHJRMB+w243BhWENf+paRouPMdCqIVqu8dYkR1NqhldblsSUC4Zr2sKS7Ta2sK4OLdt9IH+PZTfw==} + /@prisma/get-platform@5.12.0: + resolution: {integrity: sha512-81Ptv9YJnwTArEBPQ2Lvu58sZPxy4OixKxVVgysFan6A3bFP7q8gIg15WTjsRuH4WXh6B667EM9sqoMTNu0fLQ==} dependencies: - '@prisma/debug': 4.16.2 - escape-string-regexp: 4.0.0 - execa: 5.1.1 - fs-jetpack: 5.1.0 - kleur: 4.1.5 - replace-string: 3.1.0 - strip-ansi: 6.0.1 - tempy: 1.0.1 - terminal-link: 2.1.1 - ts-pattern: 4.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /@prisma/get-platform@5.0.0: - resolution: {integrity: sha512-JT/rz/jaMTggDkd9OIma50si9rPLzSFe7XSrV3mKXwtv9t+rdwx5ZhmKJd+Rz6S1vhn/291k21JLfaxOW6u8KQ==} - dependencies: - '@prisma/debug': 5.0.0 - escape-string-regexp: 4.0.0 - execa: 5.1.1 - fs-jetpack: 5.1.0 - kleur: 4.1.5 - replace-string: 3.1.0 - strip-ansi: 6.0.1 - tempy: 1.0.1 - terminal-link: 2.1.1 - ts-pattern: 4.3.0 - transitivePeerDependencies: - - supports-color - dev: false + '@prisma/debug': 5.12.0 /@prisma/get-platform@5.7.0: resolution: {integrity: sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==} @@ -3898,108 +3919,6 @@ packages: '@prisma/debug': 5.7.0 dev: false - /@prisma/internals@4.16.2: - resolution: {integrity: sha512-/3OiSADA3RRgsaeEE+MDsBgL6oAMwddSheXn6wtYGUnjERAV/BmF5bMMLnTykesQqwZ1s8HrISrJ0Vf6cjOxMg==} - dependencies: - '@antfu/ni': 0.21.4 - '@opentelemetry/api': 1.4.1 - '@prisma/debug': 4.16.2 - '@prisma/engines': 4.16.2 - '@prisma/fetch-engine': 4.16.2 - '@prisma/generator-helper': 4.16.2 - '@prisma/get-platform': 4.16.2 - '@prisma/prisma-fmt-wasm': 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 - archiver: 5.3.1 - arg: 5.0.2 - checkpoint-client: 1.1.24 - cli-truncate: 2.1.0 - dotenv: 16.0.3 - escape-string-regexp: 4.0.0 - execa: 5.1.1 - find-up: 5.0.0 - fp-ts: 2.16.0 - fs-extra: 11.1.1 - fs-jetpack: 5.1.0 - global-dirs: 3.0.1 - globby: 11.1.0 - indent-string: 4.0.0 - is-windows: 1.0.2 - is-wsl: 2.2.0 - kleur: 4.1.5 - new-github-issue-url: 0.2.1 - node-fetch: 2.6.11 - npm-packlist: 5.1.3 - open: 7.4.2 - p-map: 4.0.0 - prompts: 2.4.2 - read-pkg-up: 7.0.1 - replace-string: 3.1.0 - resolve: 1.22.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - strip-indent: 3.0.0 - temp-dir: 2.0.0 - temp-write: 4.0.0 - tempy: 1.0.1 - terminal-link: 2.1.1 - tmp: 0.2.1 - ts-pattern: 4.3.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /@prisma/internals@5.0.0: - resolution: {integrity: sha512-VGWyFk6QlSBXT8z65Alq5F3o9E8IiTtaBoa3rmKkGpZjUk85kJy3jZz4xkRv53TaeghGE5rWfwkfak26KtY5yQ==} - dependencies: - '@antfu/ni': 0.21.4 - '@opentelemetry/api': 1.4.1 - '@prisma/debug': 5.0.0 - '@prisma/engines': 5.0.0 - '@prisma/fetch-engine': 5.0.0 - '@prisma/generator-helper': 5.0.0 - '@prisma/get-platform': 5.0.0 - '@prisma/prisma-schema-wasm': 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584 - archiver: 5.3.1 - arg: 5.0.2 - checkpoint-client: 1.1.24 - cli-truncate: 2.1.0 - dotenv: 16.0.3 - escape-string-regexp: 4.0.0 - execa: 5.1.1 - find-up: 5.0.0 - fp-ts: 2.16.0 - fs-extra: 11.1.1 - fs-jetpack: 5.1.0 - global-dirs: 3.0.1 - globby: 11.1.0 - indent-string: 4.0.0 - is-windows: 1.0.2 - is-wsl: 2.2.0 - kleur: 4.1.5 - new-github-issue-url: 0.2.1 - node-fetch: 2.6.12 - npm-packlist: 5.1.3 - open: 7.4.2 - p-map: 4.0.0 - prompts: 2.4.2 - read-pkg-up: 7.0.1 - replace-string: 3.1.0 - resolve: 1.22.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - strip-indent: 3.0.0 - temp-dir: 2.0.0 - temp-write: 4.0.0 - tempy: 1.0.1 - terminal-link: 2.1.1 - tmp: 0.2.1 - ts-pattern: 4.3.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - /@prisma/internals@5.7.0: resolution: {integrity: sha512-O9x47W1DECAyvNjYUx6oZHmTX10emKuBgsFHZemUbkIcJdCsp3X8Cy2JMJ5z3hqkRX6a6omMamFsWjuTARoaSw==} dependencies: @@ -4013,14 +3932,6 @@ packages: prompts: 2.4.2 dev: false - /@prisma/prisma-fmt-wasm@4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81: - resolution: {integrity: sha512-g090+dEH7wrdCw359+8J9+TGH84qK28V/dxwINjhhNCtju9lej99z9w/AVsJP9UhhcCPS4psYz4iu8d53uxVpA==} - dev: false - - /@prisma/prisma-schema-wasm@4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584: - resolution: {integrity: sha512-JFdsnSgBPN8reDTLOI9Vh/6ccCb2aD1LbY/LWQnkcIgNo6IdpzvuM+qRVbBuA6IZP2SdqQI8Lu6RL2P8EFBQUA==} - dev: false - /@prisma/prisma-schema-wasm@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9: resolution: {integrity: sha512-w+HdQtux0dJDEn6BG3fgNn+fXErXiekj9n//uHRAgrmZghockJkhnikOmG8aSXjTb1Tu5DrGasBX+rYX6rHT1w==} dev: false @@ -4522,13 +4433,13 @@ packages: /@swc/helpers@0.4.11: resolution: {integrity: sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@swc/helpers@0.5.1: resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /@tanstack/match-sorter-utils@8.8.4: @@ -4801,12 +4712,13 @@ packages: /@types/bcryptjs@2.4.2: resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==} + dev: true /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/chai-subset@1.3.3: @@ -4822,7 +4734,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/cookie@0.5.1: @@ -4833,18 +4745,6 @@ packages: resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} dev: true - /@types/cross-spawn@6.0.2: - resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} - dependencies: - '@types/node': 18.0.0 - dev: false - - /@types/debug@4.1.8: - resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} - dependencies: - '@types/ms': 0.7.31 - dev: false - /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: true @@ -4852,7 +4752,7 @@ packages: /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -4871,13 +4771,13 @@ packages: resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} dependencies: '@types/jsonfile': 6.1.1 - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/http-errors@2.0.1: @@ -4887,7 +4787,7 @@ packages: /@types/http-proxy@1.17.12: resolution: {integrity: sha512-kQtujO08dVtQ2wXAuSFfk9ASy3sug4+ogFR8Kd8UgP8PEuc1/G/8yjYRmp//PcDNJEUKOza/MrQu15bouEUCiw==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/is-ci@3.0.0: @@ -4922,7 +4822,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 '@types/tough-cookie': 4.0.4 parse5: 7.1.2 dev: true @@ -4931,10 +4831,14 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + /@types/jsonfile@6.1.1: resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/line-column@1.0.0: @@ -4953,25 +4857,13 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true - /@types/ms@0.7.31: - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} - dev: false - - /@types/nock@11.1.0: - resolution: {integrity: sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==} - deprecated: This is a stub types definition. nock provides its own type definitions, so you do not need this installed. - dependencies: - nock: 13.3.7 - transitivePeerDependencies: - - supports-color - dev: true - /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true /@types/node@18.0.0: resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==} + dev: false /@types/node@20.10.2: resolution: {integrity: sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw==} @@ -4979,13 +4871,19 @@ packages: undici-types: 5.26.5 dev: true + /@types/node@20.12.7: + resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + dependencies: + undici-types: 5.26.5 + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true /@types/pg@8.10.2: resolution: {integrity: sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 pg-protocol: 1.6.0 pg-types: 4.0.1 dev: true @@ -5028,9 +4926,9 @@ packages: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true - /@types/retry@0.12.0: - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - dev: false + /@types/safe-json-stringify@1.1.5: + resolution: {integrity: sha512-wQ1unJoajjDOP7bkg7FHOYelVp6BSsuBIFSvifNKeiMHegXWa6vddoqM/dHTVkX8bn9fJcor4Hukff9AtFybcA==} + dev: true /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} @@ -5048,11 +4946,15 @@ packages: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + /@types/send@0.17.1: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/serve-static@1.15.2: @@ -5060,7 +4962,7 @@ packages: dependencies: '@types/http-errors': 2.0.1 '@types/mime': 3.0.1 - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/stack-utils@2.0.1: @@ -5075,7 +4977,7 @@ packages: resolution: {integrity: sha512-LOWgpacIV8GHhrsQU+QMZuomfqXiqzz3ILLkCtKx3Us6AmomFViuzKT9D693QTKgyut2oCytMG8/efOop+DB+w==} dependencies: '@types/cookiejar': 2.1.2 - '@types/node': 18.0.0 + '@types/node': 20.10.2 dev: true /@types/supertest@2.0.12: @@ -5116,64 +5018,56 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.55.0)(typescript@5.3.2): - resolution: {integrity: sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0)(eslint@8.57.0)(typescript@5.4.4): + resolution: {integrity: sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==} + engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.13.1(eslint@8.55.0)(typescript@5.3.2) - '@typescript-eslint/scope-manager': 6.13.1 - '@typescript-eslint/type-utils': 6.13.1(eslint@8.55.0)(typescript@5.3.2) - '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.3.2) - '@typescript-eslint/visitor-keys': 6.13.1 + '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.4.4) + '@typescript-eslint/scope-manager': 7.6.0 + '@typescript-eslint/type-utils': 7.6.0(eslint@8.57.0)(typescript@5.4.4) + '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.4.4) + '@typescript-eslint/visitor-keys': 7.6.0 debug: 4.3.4 - eslint: 8.55.0 + eslint: 8.57.0 graphemer: 1.4.0 - ignore: 5.2.4 + ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.3.2) - typescript: 5.3.2 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.13.1(eslint@8.55.0)(typescript@5.3.2): - resolution: {integrity: sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.4.4): + resolution: {integrity: sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==} + engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.13.1 - '@typescript-eslint/types': 6.13.1 - '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.3.2) - '@typescript-eslint/visitor-keys': 6.13.1 + '@typescript-eslint/scope-manager': 7.6.0 + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) + '@typescript-eslint/visitor-keys': 7.6.0 debug: 4.3.4 - eslint: 8.55.0 - typescript: 5.3.2 + eslint: 8.57.0 + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@5.60.1: - resolution: {integrity: sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.60.1 - '@typescript-eslint/visitor-keys': 5.60.1 - dev: true - /@typescript-eslint/scope-manager@6.13.1: resolution: {integrity: sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5182,123 +5076,123 @@ packages: '@typescript-eslint/visitor-keys': 6.13.1 dev: true - /@typescript-eslint/type-utils@6.13.1(eslint@8.55.0)(typescript@5.3.2): - resolution: {integrity: sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/scope-manager@7.6.0: + resolution: {integrity: sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==} + engines: {node: ^18.18.0 || >=20.0.0} + dependencies: + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/visitor-keys': 7.6.0 + dev: true + + /@typescript-eslint/type-utils@7.6.0(eslint@8.57.0)(typescript@5.4.4): + resolution: {integrity: sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==} + engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.3.2) - '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.3.2) + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) + '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.4.4) debug: 4.3.4 - eslint: 8.55.0 - ts-api-utils: 1.0.3(typescript@5.3.2) - typescript: 5.3.2 + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@5.60.1: - resolution: {integrity: sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@typescript-eslint/types@6.13.1: resolution: {integrity: sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@5.60.1(typescript@5.3.2): - resolution: {integrity: sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/types@7.6.0: + resolution: {integrity: sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==} + engines: {node: ^18.18.0 || >=20.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.13.1(typescript@5.4.4): + resolution: {integrity: sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.60.1 - '@typescript-eslint/visitor-keys': 5.60.1 + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/visitor-keys': 6.13.1 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0(typescript@5.3.2) - typescript: 5.3.2 + ts-api-utils: 1.0.3(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.13.1(typescript@5.3.2): - resolution: {integrity: sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/typescript-estree@7.6.0(typescript@5.4.4): + resolution: {integrity: sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==} + engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.13.1 - '@typescript-eslint/visitor-keys': 6.13.1 + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/visitor-keys': 7.6.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.3.2) - typescript: 5.3.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@5.60.1(eslint@8.55.0)(typescript@5.3.2): - resolution: {integrity: sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.60.1 - '@typescript-eslint/types': 5.60.1 - '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.3.2) - eslint: 8.55.0 - eslint-scope: 5.1.1 - semver: 7.5.4 + minimatch: 9.0.4 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color - - typescript dev: true - /@typescript-eslint/utils@6.13.1(eslint@8.55.0)(typescript@5.3.2): + /@typescript-eslint/utils@6.13.1(eslint@8.57.0)(typescript@5.4.4): resolution: {integrity: sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 6.13.1 '@typescript-eslint/types': 6.13.1 - '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.3.2) - eslint: 8.55.0 + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.4.4) + eslint: 8.57.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@5.60.1: - resolution: {integrity: sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/utils@7.6.0(eslint@8.57.0)(typescript@5.4.4): + resolution: {integrity: sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 dependencies: - '@typescript-eslint/types': 5.60.1 - eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.6.0 + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) + eslint: 8.57.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript dev: true /@typescript-eslint/visitor-keys@6.13.1: @@ -5309,6 +5203,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@7.6.0: + resolution: {integrity: sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==} + engines: {node: ^18.18.0 || >=20.0.0} + dependencies: + '@typescript-eslint/types': 7.6.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -5609,14 +5511,14 @@ packages: '@whatwg-node/events': 0.1.1 busboy: 1.6.0 fast-querystring: 1.1.2 - tslib: 2.6.0 + tslib: 2.6.2 /@whatwg-node/server@0.9.19: resolution: {integrity: sha512-GViwZq7iE1qCV6fSL2JHAHPQb6Jn2Ke34pkC5Wv7nAZi0qIqqPcBrEUG7TbJc79hYjCSrzRTi7FEs2xyWnQn7Q==} engines: {node: '>=16.0.0'} dependencies: '@whatwg-node/fetch': 0.9.14 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /abab@2.0.6: @@ -5693,14 +5595,7 @@ packages: debug: 4.3.4 transitivePeerDependencies: - supports-color - - /aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: false + dev: true /ajv-draft-04@1.0.0(ajv@8.12.0): resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} @@ -5800,6 +5695,10 @@ packages: picomatch: 2.3.1 dev: true + /append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + dev: true + /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: true @@ -5808,22 +5707,6 @@ packages: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} dev: true - /archiver-utils@2.1.0: - resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} - engines: {node: '>= 6'} - dependencies: - glob: 7.2.3 - graceful-fs: 4.2.11 - lazystream: 1.0.1 - lodash.defaults: 4.2.0 - lodash.difference: 4.5.0 - lodash.flatten: 4.4.0 - lodash.isplainobject: 4.0.6 - lodash.union: 4.6.0 - normalize-path: 3.0.0 - readable-stream: 2.3.8 - dev: false - /archiver-utils@4.0.1: resolution: {integrity: sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==} engines: {node: '>= 12.0.0'} @@ -5836,19 +5719,6 @@ packages: readable-stream: 3.6.2 dev: true - /archiver@5.3.1: - resolution: {integrity: sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==} - engines: {node: '>= 10'} - dependencies: - archiver-utils: 2.1.0 - async: 3.2.4 - buffer-crc32: 0.2.13 - readable-stream: 3.6.2 - readdir-glob: 1.1.3 - tar-stream: 2.2.0 - zip-stream: 4.1.0 - dev: false - /archiver@6.0.1: resolution: {integrity: sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==} engines: {node: '>= 12.0.0'} @@ -5933,6 +5803,7 @@ packages: /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + dev: true /array.prototype.flat@1.3.1: resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} @@ -5989,11 +5860,6 @@ packages: - rollup dev: true - /astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - dev: false - /async-exit-hook@2.0.1: resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} engines: {node: '>=0.12.0'} @@ -6005,6 +5871,7 @@ packages: /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -6273,6 +6140,7 @@ packages: /buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -6368,7 +6236,7 @@ packages: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: pascal-case: 3.1.2 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /camelcase-keys@6.2.2: @@ -6409,7 +6277,7 @@ packages: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: no-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 upper-case-first: 2.0.2 dev: false @@ -6476,20 +6344,6 @@ packages: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true - /checkpoint-client@1.1.24: - resolution: {integrity: sha512-nIOlLhDS7MKs4tUzS3LCm+sE1NgTCVnVrXlD0RRxaoEkkLu8LIWSUNiNWai6a+LK5unLzTyZeTCYX1Smqy0YoA==} - dependencies: - ci-info: 3.8.0 - env-paths: 2.2.1 - fast-write-atomic: 0.2.1 - make-dir: 3.1.0 - ms: 2.1.3 - node-fetch: 2.6.11 - uuid: 9.0.0 - transitivePeerDependencies: - - encoding - dev: false - /cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} dependencies: @@ -6561,6 +6415,7 @@ packages: /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} + dev: true /ci-info@4.0.0: resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} @@ -6577,11 +6432,6 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true - /clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: false - /clear@0.1.0: resolution: {integrity: sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==} dev: true @@ -6605,14 +6455,6 @@ packages: engines: {node: '>=6'} dev: false - /cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - dev: false - /cli-truncate@3.1.0: resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6790,21 +6632,12 @@ packages: /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true /component-emitter@1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} dev: true - /compress-commons@4.1.1: - resolution: {integrity: sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==} - engines: {node: '>= 10'} - dependencies: - buffer-crc32: 0.2.13 - crc32-stream: 4.0.2 - normalize-path: 3.0.0 - readable-stream: 3.6.2 - dev: false - /compress-commons@5.0.1: resolution: {integrity: sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==} engines: {node: '>= 12.0.0'} @@ -6818,6 +6651,16 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: true + /concurrently@7.4.0: resolution: {integrity: sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==} engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} @@ -6841,6 +6684,10 @@ packages: proto-list: 1.2.4 dev: false + /consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + dev: true + /consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -6854,7 +6701,7 @@ packages: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} dependencies: no-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 upper-case: 2.0.2 dev: false @@ -6890,6 +6737,11 @@ packages: engines: {node: '>= 0.6'} dev: true + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: true + /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true @@ -6925,18 +6777,19 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: true + /crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} hasBin: true - - /crc32-stream@4.0.2: - resolution: {integrity: sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==} - engines: {node: '>= 10'} - dependencies: - crc-32: 1.2.2 - readable-stream: 3.6.2 - dev: false + dev: true /crc32-stream@5.0.0: resolution: {integrity: sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==} @@ -6989,7 +6842,7 @@ packages: resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /cross-spawn@5.1.0: @@ -7008,11 +6861,6 @@ packages: shebang-command: 2.0.0 which: 2.0.2 - /crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - dev: false - /css-declaration-sorter@6.4.1(postcss@8.4.31): resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} engines: {node: ^10 || ^12 || >=14} @@ -7330,20 +7178,6 @@ packages: resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} dev: true - /del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - dev: false - /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -7430,6 +7264,7 @@ packages: engines: {node: '>=8'} dependencies: path-type: 4.0.0 + dev: true /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} @@ -7480,7 +7315,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /dot-prop@8.0.2: @@ -7504,6 +7339,7 @@ packages: /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + dev: true /dotenv@16.3.1: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} @@ -7567,6 +7403,8 @@ packages: requiresBuild: true dependencies: once: 1.4.0 + dev: true + optional: true /enhanced-resolve@4.5.0: resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==} @@ -7609,11 +7447,6 @@ packages: engines: {node: '>=0.12'} dev: true - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: false - /envinfo@7.11.0: resolution: {integrity: sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==} engines: {node: '>=4'} @@ -7631,6 +7464,7 @@ packages: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 + dev: true /es-abstract@1.21.2: resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} @@ -8061,6 +7895,7 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + dev: true /escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} @@ -8079,12 +7914,12 @@ packages: source-map: 0.6.1 dev: true - /eslint-plugin-jest@27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.55.0)(jest@29.7.0)(typescript@5.3.2): - resolution: {integrity: sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /eslint-plugin-jest@28.2.0(@typescript-eslint/eslint-plugin@7.6.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.4): + resolution: {integrity: sha512-yRDti/a+f+SMSmNTiT9/M/MzXGkitl8CfzUxnpoQcTyfq8gUrXMriVcWU36W1X6BZSUoyUCJrDAWWUA2N4hE5g==} + engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 - eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/eslint-plugin': ^6.0.0 || ^7.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 jest: '*' peerDependenciesMeta: '@typescript-eslint/eslint-plugin': @@ -8092,23 +7927,15 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.55.0)(typescript@5.3.2) - '@typescript-eslint/utils': 5.60.1(eslint@8.55.0)(typescript@5.3.2) - eslint: 8.55.0 + '@typescript-eslint/eslint-plugin': 7.6.0(@typescript-eslint/parser@7.6.0)(eslint@8.57.0)(typescript@5.4.4) + '@typescript-eslint/utils': 6.13.1(eslint@8.57.0)(typescript@5.4.4) + eslint: 8.57.0 jest: 29.7.0(@types/node@20.10.2)(ts-node@10.9.1) transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true - /eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8122,16 +7949,16 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.55.0: - resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==} + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/regexpp': 4.10.0 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.55.0 - '@humanwhocodes/config-array': 0.11.13 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.2.0 @@ -8152,7 +7979,7 @@ packages: glob-parent: 6.0.2 globals: 13.20.0 graphemer: 1.4.0 - ignore: 5.2.4 + ignore: 5.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -8202,11 +8029,6 @@ packages: estraverse: 5.3.0 dev: true - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true - /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -8337,6 +8159,45 @@ packages: - supports-color dev: true + /express@4.19.2: + resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.2 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.6.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} dev: true @@ -8436,10 +8297,6 @@ packages: resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} dev: true - /fast-write-atomic@0.2.1: - resolution: {integrity: sha512-WvJe06IfNYlr+6cO3uQkdKdy3Cb1LlCJSF8zRs2eT8yuhdbSlR9nIt+TgQ92RUxiRrQm+/S7RARnMfCs5iuAjw==} - dev: false - /fastify-plugin@4.5.0: resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==} dev: true @@ -8537,15 +8394,6 @@ packages: - supports-color dev: true - /find-cache-dir@3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} - dependencies: - commondir: 1.0.1 - make-dir: 3.1.0 - pkg-dir: 4.2.0 - dev: false - /find-my-way@7.6.2: resolution: {integrity: sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==} engines: {node: '>=14'} @@ -8568,6 +8416,7 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + dev: true /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -8575,6 +8424,7 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 + dev: true /find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} @@ -8646,10 +8496,6 @@ packages: engines: {node: '>= 0.6'} dev: true - /fp-ts@2.16.0: - resolution: {integrity: sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ==} - dev: false - /fraction.js@4.3.6: resolution: {integrity: sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==} dev: true @@ -8669,6 +8515,8 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} requiresBuild: true + dev: true + optional: true /fs-extra@11.1.0: resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} @@ -8686,6 +8534,7 @@ packages: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 + dev: true /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -8705,12 +8554,6 @@ packages: universalify: 0.1.2 dev: true - /fs-jetpack@5.1.0: - resolution: {integrity: sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==} - dependencies: - minimatch: 5.1.6 - dev: false - /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -8930,12 +8773,14 @@ packages: inherits: 2.0.4 minimatch: 5.1.6 once: 1.4.0 + dev: true /global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} dependencies: ini: 2.0.0 + dev: true /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -8965,6 +8810,7 @@ packages: ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 + dev: true /globby@13.2.2: resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} @@ -8989,6 +8835,7 @@ packages: /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -9015,7 +8862,7 @@ packages: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 dependencies: graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /graphql-tag@2.12.6(graphql@16.8.1): @@ -9025,7 +8872,7 @@ packages: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 dependencies: graphql: 16.8.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /graphql-yoga@4.0.4(graphql@16.8.1): @@ -9045,7 +8892,7 @@ packages: dset: 3.1.3 graphql: 16.8.1 lru-cache: 10.0.1 - tslib: 2.6.0 + tslib: 2.6.2 dev: true /graphql-yoga@5.0.2(graphql@16.8.1): @@ -9146,19 +8993,11 @@ packages: resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} dev: true - /hasha@5.2.2: - resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} - engines: {node: '>=8'} - dependencies: - is-stream: 2.0.1 - type-fest: 0.8.1 - dev: false - /header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} dependencies: capital-case: 1.0.4 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /hexoid@1.0.0: @@ -9176,6 +9015,7 @@ packages: /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true /hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} @@ -9231,16 +9071,6 @@ packages: - supports-color dev: true - /http-proxy-agent@7.0.0: - resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - /http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -9266,16 +9096,6 @@ packages: - supports-color dev: true - /https-proxy-agent@7.0.0: - resolution: {integrity: sha512-0euwPCRyAPSgGdzD1IVN9nJYHtBhJwb6XPfbpQcYbPCwrBidX6GzxmchnaF4sfF/jPb74Ojx5g4yTg3sixlyPw==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - /https-proxy-agent@7.0.2: resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} engines: {node: '>= 14'} @@ -9327,16 +9147,15 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - /ignore-walk@5.0.1: - resolution: {integrity: sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - minimatch: 5.1.6 - dev: false - /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + dev: true + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -9363,6 +9182,7 @@ packages: /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + dev: true /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -9379,6 +9199,7 @@ packages: /ini@2.0.0: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} + dev: true /internal-slot@1.0.5: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} @@ -9441,6 +9262,7 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -9498,6 +9320,7 @@ packages: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true + dev: true /is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} @@ -9561,14 +9384,10 @@ packages: resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} dev: false - /is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - dev: false - /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + dev: true /is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} @@ -9705,12 +9524,14 @@ packages: /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + dev: true /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} dependencies: is-docker: 2.2.1 + dev: true /isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} @@ -9801,6 +9622,11 @@ packages: istanbul-lib-report: 3.0.0 dev: true + /iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + dev: true + /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9818,7 +9644,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -9867,47 +9693,6 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@18.0.0)(ts-node@10.9.1): - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.23.2 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 18.0.0 - babel-jest: 29.7.0(@babel/core@7.23.2) - chalk: 4.1.2 - ci-info: 3.8.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.10.2)(typescript@5.3.2) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - dev: true - /jest-config@29.7.0(@types/node@20.10.2)(ts-node@10.9.1): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9943,7 +9728,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.10.2)(typescript@5.3.2) + ts-node: 10.9.1(@types/node@20.10.2)(typescript@5.4.4) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -9990,7 +9775,7 @@ packages: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 18.0.0 + '@types/node': 20.10.2 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -10007,7 +9792,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -10032,7 +9817,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 18.0.0 + '@types/node': 20.10.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -10083,7 +9868,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 jest-util: 29.7.0 dev: true @@ -10138,7 +9923,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -10169,7 +9954,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.1 @@ -10221,7 +10006,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -10246,7 +10031,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.0.0 + '@types/node': 20.10.2 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -10258,7 +10043,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.10.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -10361,6 +10146,7 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -10407,6 +10193,7 @@ packages: universalify: 2.0.0 optionalDependencies: graceful-fs: 4.2.11 + dev: true /jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} @@ -10469,6 +10256,7 @@ packages: /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + dev: true /klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} @@ -10519,6 +10307,7 @@ packages: engines: {node: '>= 0.6.3'} dependencies: readable-stream: 2.3.8 + dev: true /leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -10555,6 +10344,7 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true /linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} @@ -10638,12 +10428,14 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 + dev: true /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} dependencies: p-locate: 5.0.0 + dev: true /lodash-decorators@6.0.1(lodash@4.17.21): resolution: {integrity: sha512-1M0YC8G3nFTkejZEk2ehyvryEdcqj6xATH+ybI8j53cLs/bKRsavaE//y7nz/A0vxEFhxYqev7vdWfsuTJ1AtQ==} @@ -10665,14 +10457,7 @@ packages: /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - - /lodash.difference@4.5.0: - resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} - dev: false - - /lodash.flatten@4.4.0: - resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} - dev: false + dev: true /lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -10696,6 +10481,7 @@ packages: /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true /lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} @@ -10725,10 +10511,6 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true - /lodash.union@4.6.0: - resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} - dev: false - /lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} dev: true @@ -10777,7 +10559,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: false /lowlight@1.17.0: @@ -10853,6 +10635,7 @@ packages: engines: {node: '>=8'} dependencies: semver: 6.3.1 + dev: true /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -11001,6 +10784,7 @@ packages: /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + dev: true /minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -11019,6 +10803,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -11071,6 +10862,13 @@ packages: dev: true optional: true + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -11117,6 +10915,20 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /multer@1.4.4-lts.1: + resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + dev: true /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -11166,11 +10978,6 @@ packages: engines: {node: '>= 0.6'} dev: true - /new-github-issue-url@0.2.1: - resolution: {integrity: sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==} - engines: {node: '>=10'} - dev: false - /next@12.3.1(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} @@ -11395,7 +11202,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /nock@13.3.7: @@ -11432,18 +11239,6 @@ packages: resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==} dev: true - /node-fetch@2.6.11: - resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - /node-fetch@2.6.12: resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} engines: {node: 4.x || >=6.0.0} @@ -11498,39 +11293,18 @@ packages: resolve: 1.22.2 semver: 5.7.1 validate-npm-package-license: 3.0.4 + dev: true /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + dev: true /normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} dev: true - /npm-bundled@2.0.1: - resolution: {integrity: sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - npm-normalize-package-bin: 2.0.0 - dev: false - - /npm-normalize-package-bin@2.0.0: - resolution: {integrity: sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dev: false - - /npm-packlist@5.1.3: - resolution: {integrity: sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - hasBin: true - dependencies: - glob: 8.1.0 - ignore-walk: 5.0.1 - npm-bundled: 2.0.1 - npm-normalize-package-bin: 2.0.0 - dev: false - /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -11567,7 +11341,7 @@ packages: fsevents: 2.3.3 dev: true - /nuxt@3.7.4(@types/node@20.10.2)(eslint@8.55.0)(typescript@5.3.2): + /nuxt@3.7.4(@types/node@20.10.2)(eslint@8.57.0)(typescript@5.4.4): resolution: {integrity: sha512-voXN2kheEpi7DJd0hkikfLuA41UiP9IwDDol65dvoJiHnRseWfaw1MyJl6FLHHDHwRzisX9QXWIyMfa9YF4nGg==} engines: {node: ^14.18.0 || >=16.10.0} hasBin: true @@ -11585,7 +11359,7 @@ packages: '@nuxt/schema': 3.7.4 '@nuxt/telemetry': 2.5.2 '@nuxt/ui-templates': 1.3.1 - '@nuxt/vite-builder': 3.7.4(@types/node@20.10.2)(eslint@8.55.0)(typescript@5.3.2)(vue@3.3.4) + '@nuxt/vite-builder': 3.7.4(@types/node@20.10.2)(eslint@8.57.0)(typescript@5.4.4)(vue@3.3.4) '@types/node': 20.10.2 '@unhead/dom': 1.7.4 '@unhead/ssr': 1.7.4 @@ -11759,14 +11533,6 @@ packages: mimic-fn: 4.0.0 dev: true - /open@7.4.2: - resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} - engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 - is-wsl: 2.2.0 - dev: false - /open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} @@ -11832,6 +11598,7 @@ packages: engines: {node: '>=8'} dependencies: p-map: 2.1.0 + dev: true /p-is-promise@3.0.0: resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} @@ -11843,12 +11610,14 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 + dev: true /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 + dev: true /p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} @@ -11862,35 +11631,24 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 + dev: true /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} dependencies: p-limit: 3.1.0 + dev: true /p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} - - /p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: false - - /p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - dev: false + dev: true /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + dev: true /packet-reader@1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} @@ -11900,7 +11658,7 @@ packages: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: dot-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /parent-module@1.0.1: @@ -11930,6 +11688,7 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + dev: true /parse-path@7.0.0: resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} @@ -11971,7 +11730,7 @@ packages: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: no-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /pascalcase@1.0.0: @@ -11986,12 +11745,13 @@ packages: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: dot-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -12013,9 +11773,14 @@ packages: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} dev: true + /path-to-regexp@3.2.0: + resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + dev: true /pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} @@ -12200,6 +11965,7 @@ packages: engines: {node: '>=8'} dependencies: find-up: 4.1.0 + dev: true /pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} @@ -12328,7 +12094,7 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - ts-node: 10.9.1(@types/node@20.10.2)(typescript@5.3.2) + ts-node: 10.9.1(@types/node@20.10.2)(typescript@5.4.4) yaml: 2.3.2 dev: true @@ -12723,14 +12489,13 @@ packages: hasBin: true dev: true - /prisma@4.16.2: - resolution: {integrity: sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==} - engines: {node: '>=14.17'} + /prisma@5.12.0: + resolution: {integrity: sha512-zxw4WSIvpsyNbpv8r7Fxgm7nwTFVmD6wbN6VuH13lClOceSANDOMl4jO3oxE6VzhjxmnEJqOGZjON2T2UpmLag==} + engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 4.16.2 - dev: true + '@prisma/engines': 5.12.0 /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -12751,11 +12516,6 @@ packages: through2: 2.0.5 dev: false - /progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - dev: false - /promise-polyfill@8.3.0: resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} dev: true @@ -12993,6 +12753,7 @@ packages: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 + dev: true /read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} @@ -13002,6 +12763,7 @@ packages: normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 + dev: true /read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} @@ -13062,6 +12824,7 @@ packages: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} dependencies: minimatch: 5.1.6 + dev: true /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -13100,6 +12863,10 @@ packages: engines: {node: '>=6'} dev: true + /reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + dev: true + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: true @@ -13166,11 +12933,6 @@ packages: yargs: 17.7.2 dev: true - /replace-string@3.1.0: - resolution: {integrity: sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==} - engines: {node: '>=8'} - dev: false - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -13243,11 +13005,6 @@ packages: engines: {node: '>=4'} dev: true - /retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - dev: false - /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -13322,7 +13079,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /sade@1.8.1: @@ -13338,6 +13095,10 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-json-stringify@1.2.0: + resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} + dev: false + /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: @@ -13389,6 +13150,7 @@ packages: /semver@5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true + dev: true /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -13401,6 +13163,14 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -13426,7 +13196,7 @@ packages: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} dependencies: no-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 upper-case-first: 2.0.2 dev: false @@ -13543,6 +13313,7 @@ packages: /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + dev: true /slash@4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} @@ -13553,15 +13324,6 @@ packages: resolution: {integrity: sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==} dev: false - /slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - dev: false - /slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -13590,7 +13352,7 @@ packages: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.6.0 + tslib: 2.6.2 dev: false /sonic-boom@3.3.0: @@ -13657,18 +13419,22 @@ packages: dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.13 + dev: true /spdx-exceptions@2.3.0: resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true /spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.13 + dev: true /spdx-license-ids@3.0.13: resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} + dev: true /speedometer@1.0.0: resolution: {integrity: sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==} @@ -13847,6 +13613,7 @@ packages: engines: {node: '>=8'} dependencies: min-indent: 1.0.1 + dev: true /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} @@ -14117,6 +13884,8 @@ packages: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 + dev: true + optional: true /tar-stream@3.1.6: resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} @@ -14138,38 +13907,6 @@ packages: yallist: 4.0.0 dev: true - /temp-dir@1.0.0: - resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} - engines: {node: '>=4'} - dev: false - - /temp-dir@2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} - dev: false - - /temp-write@4.0.0: - resolution: {integrity: sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw==} - engines: {node: '>=8'} - dependencies: - graceful-fs: 4.2.11 - is-stream: 2.0.1 - make-dir: 3.1.0 - temp-dir: 1.0.0 - uuid: 3.4.0 - dev: false - - /tempy@1.0.1: - resolution: {integrity: sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==} - engines: {node: '>=10'} - dependencies: - del: 6.1.1 - is-stream: 2.0.1 - temp-dir: 2.0.0 - type-fest: 0.16.0 - unique-string: 2.0.0 - dev: false - /term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -14257,7 +13994,7 @@ packages: /title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: true /tmp@0.0.33: @@ -14273,6 +14010,11 @@ packages: dependencies: rimraf: 3.0.2 + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + dev: true + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -14333,25 +14075,34 @@ packages: engines: {node: '>=8'} dev: true - /ts-api-utils@1.0.3(typescript@5.3.2): + /ts-api-utils@1.0.3(typescript@5.4.4): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.3.2 + typescript: 5.4.4 + dev: true + + /ts-api-utils@1.3.0(typescript@5.4.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.4.4 dev: true /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /ts-japi@1.8.0: - resolution: {integrity: sha512-77cNpz1CMJsImcikE7gnihMKOME4c/prCaruBmr7yMRHGnmEt1llFyJbuokXCaEmm0hERPv2VlzTyZ+pb43leg==} + /ts-japi@1.10.1: + resolution: {integrity: sha512-03frHSZma1Bj5lxunZqqZTBmNjV1sdzLb60gGJ7O3viPS7g/rjF52tozZPy2iRnHazjj5I4r53mG4hmO8XeAyw==} engines: {node: '>=10'} dev: false - /ts-jest@29.1.1(@babel/core@7.23.2)(esbuild@0.19.4)(jest@29.7.0)(typescript@5.3.2): + /ts-jest@29.1.1(@babel/core@7.23.2)(esbuild@0.19.4)(jest@29.7.0)(typescript@5.4.4): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -14382,7 +14133,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.4 - typescript: 5.3.2 + typescript: 5.4.4 yargs-parser: 21.1.1 dev: true @@ -14400,7 +14151,7 @@ packages: code-block-writer: 11.0.3 dev: false - /ts-node@10.9.1(@types/node@20.10.2)(typescript@5.3.2): + /ts-node@10.9.1(@types/node@20.10.2)(typescript@5.4.4): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -14426,7 +14177,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.3.2 + typescript: 5.4.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -14449,6 +14200,7 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false /tslib@2.4.1: resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} @@ -14457,7 +14209,10 @@ packages: /tslib@2.6.0: resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} - /tsup@8.0.1(ts-node@10.9.1)(typescript@5.3.2): + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + /tsup@8.0.1(ts-node@10.9.1)(typescript@5.4.4): resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} engines: {node: '>=18'} hasBin: true @@ -14490,22 +14245,12 @@ packages: source-map: 0.8.0-beta.0 sucrase: 3.33.0 tree-kill: 1.2.2 - typescript: 5.3.2 + typescript: 5.4.4 transitivePeerDependencies: - supports-color - ts-node dev: true - /tsutils@3.21.0(typescript@5.3.2): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 5.3.2 - dev: true - /tsx@4.7.1: resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} engines: {node: '>=18.0.0'} @@ -14557,11 +14302,6 @@ packages: engines: {node: '>=10'} dev: true - /type-fest@0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} - dev: false - /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -14574,10 +14314,12 @@ packages: /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} + dev: true /type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + dev: true /type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} @@ -14613,8 +14355,12 @@ packages: underscore: 1.13.6 dev: true - /typescript@5.3.2: - resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + + /typescript@5.4.4: + resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -14642,6 +14388,13 @@ packages: resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==} dev: true + /uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + dependencies: + '@lukeed/csprng': 1.1.0 + dev: true + /ultrahtml@1.5.2: resolution: {integrity: sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw==} dev: true @@ -14674,7 +14427,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /undici@5.22.1: resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} @@ -14727,13 +14479,6 @@ packages: - rollup dev: true - /unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - dependencies: - crypto-random-string: 2.0.0 - dev: false - /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -14747,6 +14492,7 @@ packages: /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + dev: true /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -14901,7 +14647,7 @@ packages: /upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} dependencies: - tslib: 2.6.0 + tslib: 2.6.2 dev: false /uqr@0.1.2: @@ -14953,12 +14699,6 @@ packages: engines: {node: '>= 0.4.0'} dev: true - /uuid@3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true - dev: false - /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true @@ -14985,6 +14725,7 @@ packages: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + dev: true /value-or-promise@1.0.12: resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} @@ -14996,7 +14737,7 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-node@0.29.7(@types/node@18.0.0): + /vite-node@0.29.7(@types/node@20.12.7): resolution: {integrity: sha512-PakCZLvz37yFfUPWBnLa1OYHPCGm5v4pmRrTcFN4V/N/T3I6tyP3z07S//9w+DdeL7vVd0VSeyMZuAh+449ZWw==} engines: {node: '>=v14.16.0'} hasBin: true @@ -15006,7 +14747,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.3.9(@types/node@18.0.0) + vite: 4.3.9(@types/node@20.12.7) transitivePeerDependencies: - '@types/node' - less @@ -15039,7 +14780,7 @@ packages: - terser dev: true - /vite-plugin-checker@0.6.2(eslint@8.55.0)(typescript@5.3.2)(vite@4.4.11): + /vite-plugin-checker@0.6.2(eslint@8.57.0)(typescript@5.4.4)(vite@4.4.11): resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==} engines: {node: '>=14.16'} peerDependencies: @@ -15075,7 +14816,7 @@ packages: chalk: 4.1.2 chokidar: 3.5.3 commander: 8.3.0 - eslint: 8.55.0 + eslint: 8.57.0 fast-glob: 3.3.2 fs-extra: 11.1.1 lodash.debounce: 4.0.8 @@ -15084,7 +14825,7 @@ packages: semver: 7.5.4 strip-ansi: 6.0.1 tiny-invariant: 1.3.1 - typescript: 5.3.2 + typescript: 5.4.4 vite: 4.4.11(@types/node@20.10.2) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 @@ -15092,7 +14833,7 @@ packages: vscode-uri: 3.0.7 dev: true - /vite@4.3.9(@types/node@18.0.0): + /vite@4.3.9(@types/node@20.12.7): resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -15117,7 +14858,7 @@ packages: terser: optional: true dependencies: - '@types/node': 18.0.0 + '@types/node': 20.12.7 esbuild: 0.17.19 postcss: 8.4.24 rollup: 3.25.3 @@ -15202,7 +14943,7 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 18.0.0 + '@types/node': 20.12.7 '@vitest/expect': 0.29.7 '@vitest/runner': 0.29.7 '@vitest/spy': 0.29.7 @@ -15221,8 +14962,8 @@ packages: tinybench: 2.5.0 tinypool: 0.4.0 tinyspy: 1.1.1 - vite: 4.3.9(@types/node@18.0.0) - vite-node: 0.29.7(@types/node@18.0.0) + vite: 4.3.9(@types/node@20.12.7) + vite-node: 0.29.7(@types/node@20.12.7) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -15724,6 +15465,7 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + dev: true /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} @@ -15734,15 +15476,6 @@ packages: resolution: {integrity: sha512-T6kZx8TYdLhuy2vURjPUj9EK9Dobnctu12CYw9ibu6Xj/UAqh2q2bQaA3vFrL4Rna5+CXYHYN3uJrUu6VulYzw==} dev: true - /zip-stream@4.1.0: - resolution: {integrity: sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==} - engines: {node: '>= 10'} - dependencies: - archiver-utils: 2.1.0 - compress-commons: 4.1.1 - readable-stream: 3.6.2 - dev: false - /zip-stream@5.0.1: resolution: {integrity: sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==} engines: {node: '>= 12.0.0'} diff --git a/script/test-prisma-v5.sh b/script/test-prisma-v5.sh deleted file mode 100755 index 51fc8e3cb..000000000 --- a/script/test-prisma-v5.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo Setting Prisma Versions to V5 -npx replace-in-file '/"prisma":\s*"\^4.\d.\d"/g' '"prisma": "^5.0.0"' 'packages/testtools/**/package*.json' 'tests/integration/**/package*.json' --isRegex -npx replace-in-file '/"@prisma/client":\s*"\^4.\d.\d"/g' '"@prisma/client": "^5.0.0"' 'packages/testtools/**/package*.json' 'tests/integration/**/package*.json' --isRegex \ No newline at end of file diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts index ddf3c999a..4c7a81b51 100644 --- a/script/test-scaffold.ts +++ b/script/test-scaffold.ts @@ -19,6 +19,6 @@ function run(cmd: string) { } run('npm init -y'); -run('npm i --no-audit --no-fund typescript prisma @prisma/client zod decimal.js'); +run('npm i --no-audit --no-fund typescript prisma @prisma/client zod decimal.js @types/node'); console.log('Test scaffold setup complete.'); diff --git a/test-setup.ts b/test-setup.ts new file mode 100644 index 000000000..9856ff4b5 --- /dev/null +++ b/test-setup.ts @@ -0,0 +1,9 @@ +import fs from 'fs'; +import path from 'path'; + +export default function globalSetup() { + if (!fs.existsSync(path.join(__dirname, '.test/scaffold/package-lock.json'))) { + console.error(`Test scaffold not found. Please run \`pnpm test-scaffold\` first.`); + process.exit(1); + } +} diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json index d4e05bd29..2a4fedfb4 100644 --- a/tests/integration/test-run/package.json +++ b/tests/integration/test-run/package.json @@ -10,9 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^4.8.0", + "@prisma/client": "5.12.0", "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "^4.8.0", + "prisma": "5.12.0", "react": "^18.2.0", "swr": "^1.3.0", "typescript": "^4.9.3", diff --git a/tests/integration/test-setup.ts b/tests/integration/test-setup.ts index 6936a0cd4..b6147c287 100644 --- a/tests/integration/test-setup.ts +++ b/tests/integration/test-setup.ts @@ -5,7 +5,7 @@ import { toResolveFalsy, toResolveNull, toBeRejectedWithCode, -} from './utils/jest-ext'; +} from '@zenstackhq/testtools/jest-ext'; expect.extend({ toBeRejectedByPolicy, diff --git a/tests/integration/tests/cli/config.test.ts b/tests/integration/tests/cli/config.test.ts deleted file mode 100644 index 096e86ca3..000000000 --- a/tests/integration/tests/cli/config.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/// - -import * as fs from 'fs'; -import * as tmp from 'tmp'; -import { createProgram } from '../../../../packages/schema/src/cli'; - -tmp.setGracefulCleanup(); - -describe('CLI Config Tests', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - const r = tmp.dirSync({ unsafeCleanup: true }); - console.log(`Project dir: ${r.name}`); - process.chdir(r.name); - - fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - // for ensuring backward compatibility only - it('valid default config empty', async () => { - fs.writeFileSync('zenstack.config.json', JSON.stringify({})); - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - }); - - // for ensuring backward compatibility only - it('valid default config non-empty', async () => { - fs.writeFileSync( - 'zenstack.config.json', - JSON.stringify({ guardFieldName: 'myGuardField', transactionFieldName: 'myTransactionField' }) - ); - - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - }); - - it('custom config file does not exist', async () => { - const program = createProgram(); - const configFile = `my.config.json`; - await expect( - program.parseAsync(['init', '--tag', 'latest', '--config', configFile], { from: 'user' }) - ).rejects.toThrow(/Config file could not be found/i); - }); - - it('custom config file is not json', async () => { - const program = createProgram(); - const configFile = `my.config.json`; - fs.writeFileSync(configFile, ` đŸ˜Ŧ đŸ˜Ŧ đŸ˜Ŧ`); - await expect( - program.parseAsync(['init', '--tag', 'latest', '--config', configFile], { from: 'user' }) - ).rejects.toThrow(/Config is not a valid JSON file/i); - }); - - // for ensuring backward compatibility only - it('valid custom config file', async () => { - fs.writeFileSync('my.config.json', JSON.stringify({ guardFieldName: 'myGuardField' })); - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest', '--config', 'my.config.json'], { from: 'user' }); - }); -}); diff --git a/tests/integration/tests/cli/format.test.ts b/tests/integration/tests/cli/format.test.ts index 9d7b2a52b..8fca5b6a2 100644 --- a/tests/integration/tests/cli/format.test.ts +++ b/tests/integration/tests/cli/format.test.ts @@ -35,6 +35,32 @@ generator client { model Post { id Int @id() @default(autoincrement()) users User[] +}`; + // set up schema + fs.writeFileSync('schema.zmodel', model, 'utf-8'); + const program = createProgram(); + await program.parseAsync(['format', '--no-prisma-style'], { from: 'user' }); + + expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(formattedModel); + }); + + it('prisma format', async () => { + const model = ` + datasource db {provider="sqlite" url="file:./dev.db"} + generator client {provider = "prisma-client-js"} + model Post {id Int @id() @default(autoincrement())users User[]}`; + + const formattedModel = ` +datasource db { + provider="sqlite" + url="file:./dev.db" +} +generator client { + provider = "prisma-client-js" +} +model Post { + id Int @id() @default(autoincrement()) + users User[] }`; // set up schema fs.writeFileSync('schema.zmodel', model, 'utf-8'); diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 5dd8ddf1c..d90ce14cc 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -88,32 +88,11 @@ model Post { expect(fs.existsSync('./out/zod')).toBeTruthy(); }); - it('generate custom output override', async () => { - fs.appendFileSync( - 'schema.zmodel', - ` - plugin policy { - provider = '@core/access-policy' - output = 'policy-out' - } - ` - ); - - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '-o', 'out'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack')).toBeFalsy(); - expect(fs.existsSync('./out/model-meta.js')).toBeTruthy(); - expect(fs.existsSync('./out/zod')).toBeTruthy(); - expect(fs.existsSync('./out/policy.js')).toBeFalsy(); - expect(fs.existsSync('./policy-out/policy.js')).toBeTruthy(); - }); - it('generate no default plugins run nothing', async () => { const program = createProgram(); await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeFalsy(); expect(fs.existsSync('./prisma/schema.prisma')).toBeFalsy(); }); @@ -130,7 +109,6 @@ model Post { await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeFalsy(); expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); }); @@ -138,8 +116,8 @@ model Post { fs.appendFileSync( 'schema.zmodel', ` - plugin policy { - provider = '@core/access-policy' + plugin enhancer { + provider = '@core/enhancer' } ` ); @@ -147,7 +125,6 @@ model Post { await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeTruthy(); expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); }); @@ -155,8 +132,8 @@ model Post { fs.appendFileSync( 'schema.zmodel', ` - plugin policy { - provider = '@core/access-policy' + plugin enhancer { + provider = '@core/enhancer' } ` ); @@ -169,7 +146,8 @@ model Post { expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/zod')).toBeFalsy(); + const z = require(path.join(process.cwd(), './node_modules/.zenstack/zod/models')); + expect(z).toEqual({}); }); it('generate no compile', async () => { diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts index 0f9f69894..892bad97b 100644 --- a/tests/integration/tests/cli/init.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -27,10 +27,14 @@ describe.skip('CLI init command tests', () => { process.chdir(origDir); }); + // eslint-disable-next-line jest/no-disabled-tests it('init project t3 npm std', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', { - npm_config_user_agent: 'npm', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { + stdio: 'inherit', + env: { + npm_config_user_agent: 'npm', + npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + }, }); createNpmrc(); @@ -44,9 +48,12 @@ describe.skip('CLI init command tests', () => { }); it('init project t3 yarn std', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', { - npm_config_user_agent: 'yarn', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { + stdio: 'inherit', + env: { + npm_config_user_agent: 'yarn', + npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + }, }); createNpmrc(); @@ -60,9 +67,12 @@ describe.skip('CLI init command tests', () => { }); it('init project t3 pnpm std', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', { - npm_config_user_agent: 'pnpm', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { + stdio: 'inherit', + env: { + npm_config_user_agent: 'pnpm', + npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + }, }); createNpmrc(); @@ -76,9 +86,12 @@ describe.skip('CLI init command tests', () => { }); it('init project t3 non-std prisma schema', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', { - npm_config_user_agent: 'npm', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { + stdio: 'inherit', + env: { + npm_config_user_agent: 'npm', + npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), + }, }); createNpmrc(); fs.renameSync('prisma/schema.prisma', 'prisma/my.prisma'); diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 5a8e44ae7..9ef16a31d 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -26,10 +26,10 @@ describe('CLI Plugins Tests', () => { const PACKAGE_MANAGERS = ['npm' /*, 'pnpm', 'pnpm-workspace'*/] as const; - function zenstackGenerate(pm: (typeof PACKAGE_MANAGERS)[number]) { + function zenstackGenerate(pm: (typeof PACKAGE_MANAGERS)[number], output?: string) { switch (pm) { case 'npm': - run(`ZENSTACK_TEST=0 npx zenstack generate`); + run(`ZENSTACK_TEST=0 npx zenstack generate${output ? ' --output ' + output : ''}`); break; // case 'pnpm': // case 'pnpm-workspace': @@ -73,9 +73,9 @@ describe('CLI Plugins Tests', () => { 'zod@3.21.1', 'react', 'swr', - '@tanstack/react-query@^4.0.0', + '@tanstack/react-query@^5.0.0', '@trpc/server', - '@prisma/client@^4.0.0', + '@prisma/client@^5.0.0', `${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`, @@ -85,7 +85,7 @@ describe('CLI Plugins Tests', () => { const devDepPkgs = [ 'typescript', '@types/react', - 'prisma@^4.0.0', + 'prisma@^5.0.0', `${path.join(__dirname, '../../../../.build/zenstack-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-tanstack-query-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-swr-' + ver + '.tgz')}`, @@ -132,14 +132,8 @@ describe('CLI Plugins Tests', () => { output = 'prisma/my.prisma' generateClient = true }`, - `plugin meta { - provider = '@core/model-meta' - output = 'model-meta' - } - `, - `plugin policy { - provider = '@core/access-policy' - output = 'policy' + `plugin enhancer { + provider = '@core/enhancer' }`, `plugin tanstack { provider = '@zenstackhq/tanstack-query' @@ -281,4 +275,38 @@ ${BASE_MODEL} run('npx tsc'); } }); + + it('all plugins custom core output path', async () => { + for (const pm of PACKAGE_MANAGERS) { + console.log('[PACKAGE MANAGER]', pm); + await initProject(pm); + + let schemaContent = ` +generator client { + provider = "prisma-client-js" +} + +${BASE_MODEL} + `; + for (const plugin of plugins) { + if (!plugin.includes('trp')) { + schemaContent += `\n${plugin}`; + } + } + + schemaContent += `plugin trpc { + provider = '@zenstackhq/trpc' + output = 'lib/trpc' + zodSchemasImport = '../../../zen/zod' + }`; + + fs.writeFileSync('schema.zmodel', schemaContent); + + // generate + zenstackGenerate(pm, './zen'); + + // compile + run('npx tsc'); + } + }); }); diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts new file mode 100644 index 000000000..6a31540d7 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -0,0 +1,1198 @@ +import { PrismaErrorCode } from '@zenstackhq/runtime'; +import { loadSchema } from '@zenstackhq/testtools'; +import { POLYMORPHIC_MANY_TO_MANY_SCHEMA, POLYMORPHIC_SCHEMA } from './utils'; + +describe('Polymorphism Test', () => { + const schema = POLYMORPHIC_SCHEMA; + + async function setup() { + const { enhance } = await loadSchema(schema, { logPrismaQuery: true, enhancements: ['delegate'] }); + const db = enhance(); + + const user = await db.user.create({ data: { id: 1 } }); + + const video = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + + const videoWithOwner = await db.ratedVideo.findUnique({ where: { id: video.id }, include: { owner: true } }); + + return { db, video, user, videoWithOwner }; + } + + it('create hierarchy', async () => { + const { enhance } = await loadSchema(schema, { logPrismaQuery: true, enhancements: ['delegate'] }); + const db = enhance(); + + const user = await db.user.create({ data: { id: 1 } }); + + const video = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + include: { owner: true }, + }); + + expect(video).toMatchObject({ + viewCount: 1, + duration: 100, + url: 'xyz', + rating: 100, + assetType: 'Video', + videoType: 'RatedVideo', + owner: user, + }); + + await expect(db.asset.create({ data: { type: 'Video' } })).rejects.toThrow('is a delegate'); + await expect(db.video.create({ data: { type: 'RatedVideo' } })).rejects.toThrow('is a delegate'); + + const image = await db.image.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, format: 'png' }, + include: { owner: true }, + }); + expect(image).toMatchObject({ + viewCount: 1, + format: 'png', + assetType: 'Image', + owner: user, + }); + + // create in a nested payload + const gallery = await db.gallery.create({ + data: { + images: { + create: [ + { owner: { connect: { id: user.id } }, format: 'png', viewCount: 1 }, + { owner: { connect: { id: user.id } }, format: 'jpg', viewCount: 2 }, + ], + }, + }, + include: { images: { include: { owner: true } } }, + }); + expect(gallery.images).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + format: 'png', + assetType: 'Image', + viewCount: 1, + owner: user, + }), + expect.objectContaining({ + format: 'jpg', + assetType: 'Image', + viewCount: 2, + owner: user, + }), + ]) + ); + }); + + it('create with base all defaults', async () => { + const { enhance } = await loadSchema( + ` + model Base { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + type String + + @@delegate(type) + } + + model Foo extends Base { + name String + } + `, + { logPrismaQuery: true, enhancements: ['delegate'] } + ); + + const db = enhance(); + const r = await db.foo.create({ data: { name: 'foo' } }); + expect(r).toMatchObject({ name: 'foo', type: 'Foo', id: expect.any(Number), createdAt: expect.any(Date) }); + }); + + it('create with nesting', async () => { + const { enhance } = await loadSchema(schema, { logPrismaQuery: true, enhancements: ['delegate'] }); + const db = enhance(); + + // nested create a relation from base + await expect( + db.ratedVideo.create({ + data: { owner: { create: { id: 2 } }, url: 'xyz', rating: 200, duration: 200 }, + include: { owner: true }, + }) + ).resolves.toMatchObject({ owner: { id: 2 } }); + }); + + it('create many polymorphic model', async () => { + const { enhance } = await loadSchema(schema, { logPrismaQuery: true, enhancements: ['delegate'] }); + const db = enhance(); + + await expect( + db.ratedVideo.createMany({ data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 } }) + ).resolves.toMatchObject({ count: 1 }); + + await expect( + db.ratedVideo.createMany({ + data: [ + { viewCount: 2, duration: 200, url: 'xyz', rating: 100 }, + { viewCount: 3, duration: 300, url: 'xyz', rating: 100 }, + ], + }) + ).resolves.toMatchObject({ count: 2 }); + }); + + it('create many polymorphic relation', async () => { + const { enhance } = await loadSchema(schema, { logPrismaQuery: true, enhancements: ['delegate'] }); + const db = enhance(); + + const video1 = await db.ratedVideo.create({ + data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + await expect( + db.user.createMany({ data: { id: 1, assets: { connect: { id: video1.id } } } }) + ).resolves.toMatchObject({ count: 1 }); + + const video2 = await db.ratedVideo.create({ + data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + await expect( + db.user.createMany({ data: [{ id: 2, assets: { connect: { id: video2.id } } }, { id: 3 }] }) + ).resolves.toMatchObject({ count: 2 }); + }); + + it('read with concrete', async () => { + const { db, user, video } = await setup(); + + // find with include + let found = await db.ratedVideo.findFirst({ include: { owner: true } }); + expect(found).toMatchObject(video); + expect(found.owner).toMatchObject(user); + + // find with select + found = await db.ratedVideo.findFirst({ select: { id: true, createdAt: true, url: true, rating: true } }); + expect(found).toMatchObject({ id: video.id, createdAt: video.createdAt, url: video.url, rating: video.rating }); + + // findFirstOrThrow + found = await db.ratedVideo.findFirstOrThrow(); + expect(found).toMatchObject(video); + await expect( + db.ratedVideo.findFirstOrThrow({ + where: { id: video.id + 1 }, + }) + ).rejects.toThrow(); + + // findUnique + found = await db.ratedVideo.findUnique({ + where: { id: video.id }, + }); + expect(found).toMatchObject(video); + + // findUniqueOrThrow + found = await db.ratedVideo.findUniqueOrThrow({ + where: { id: video.id }, + }); + expect(found).toMatchObject(video); + await expect( + db.ratedVideo.findUniqueOrThrow({ + where: { id: video.id + 1 }, + }) + ).rejects.toThrow(); + + // findMany + let items = await db.ratedVideo.findMany(); + expect(items).toHaveLength(1); + expect(items[0]).toMatchObject(video); + + // findMany not found + items = await db.ratedVideo.findMany({ where: { id: video.id + 1 } }); + expect(items).toHaveLength(0); + + // findMany with select + items = await db.ratedVideo.findMany({ select: { id: true, createdAt: true, url: true, rating: true } }); + expect(items).toHaveLength(1); + expect(items[0]).toMatchObject({ + id: video.id, + createdAt: video.createdAt, + url: video.url, + rating: video.rating, + }); + + // find with base filter + found = await db.ratedVideo.findFirst({ where: { viewCount: video.viewCount } }); + expect(found).toMatchObject(video); + found = await db.ratedVideo.findFirst({ where: { url: video.url, owner: { id: user.id } } }); + expect(found).toMatchObject(video); + + // image: single inheritance + const image = await db.image.create({ + data: { owner: { connect: { id: 1 } }, viewCount: 1, format: 'png' }, + include: { owner: true }, + }); + const readImage = await db.image.findFirst({ include: { owner: true } }); + expect(readImage).toMatchObject(image); + expect(readImage.owner).toMatchObject(user); + }); + + it('read with base', async () => { + const { db, user, video: r } = await setup(); + + let video = await db.video.findFirst({ where: { duration: r.duration }, include: { owner: true } }); + expect(video).toMatchObject({ + ...r, + assetType: 'Video', + videoType: 'RatedVideo', + }); + expect(video.owner).toMatchObject(user); + + const asset = await db.asset.findFirst({ where: { viewCount: r.viewCount }, include: { owner: true } }); + expect(asset).toMatchObject({ + ...r, + assetType: 'Video', + videoType: 'RatedVideo', + owner: expect.objectContaining(user), + }); + + const userWithAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); + expect(userWithAssets.assets[0]).toMatchObject(r); + + const image = await db.image.create({ + data: { owner: { connect: { id: 1 } }, viewCount: 1, format: 'png' }, + include: { owner: true }, + }); + const imgAsset = await db.asset.findFirst({ where: { assetType: 'Image' }, include: { owner: true } }); + expect(imgAsset).toMatchObject({ + id: image.id, + createdAt: image.createdAt, + assetType: 'Image', + viewCount: image.viewCount, + format: 'png', + owner: expect.objectContaining(user), + }); + }); + + it('order by base fields', async () => { + const { db, user } = await setup(); + + await expect( + db.video.findMany({ + orderBy: { viewCount: 'desc' }, + }) + ).resolves.toHaveLength(1); + + await expect( + db.ratedVideo.findMany({ + orderBy: { duration: 'asc' }, + }) + ).resolves.toHaveLength(1); + + await expect( + db.user.findMany({ + orderBy: { assets: { _count: 'desc' } }, + }) + ).resolves.toHaveLength(1); + + await expect( + db.user.findUnique({ + where: { id: user.id }, + include: { + ratedVideos: { + orderBy: { + viewCount: 'desc', + }, + }, + }, + }) + ).toResolveTruthy(); + }); + + it('update simple', async () => { + const { db, videoWithOwner: video } = await setup(); + + // update with concrete + let updated = await db.ratedVideo.update({ + where: { id: video.id }, + data: { rating: 200 }, + include: { owner: true }, + }); + expect(updated.rating).toBe(200); + expect(updated.owner).toBeTruthy(); + + // update with base + updated = await db.video.update({ + where: { id: video.id }, + data: { duration: 200 }, + select: { duration: true, createdAt: true }, + }); + expect(updated.duration).toBe(200); + expect(updated.createdAt).toBeTruthy(); + + // update with base + updated = await db.asset.update({ + where: { id: video.id }, + data: { viewCount: 200 }, + }); + expect(updated.viewCount).toBe(200); + + // set discriminator + await expect(db.ratedVideo.update({ where: { id: video.id }, data: { assetType: 'Image' } })).rejects.toThrow( + 'is a discriminator' + ); + await expect( + db.ratedVideo.update({ where: { id: video.id }, data: { videoType: 'RatedVideo' } }) + ).rejects.toThrow('is a discriminator'); + }); + + it('update nested create', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + // create delegate not allowed + await expect( + db.user.update({ + where: { id: user.id }, + data: { + assets: { + create: { viewCount: 1 }, + }, + }, + include: { assets: true }, + }) + ).rejects.toThrow('is a delegate'); + + // create concrete + await expect( + db.user.update({ + where: { id: user.id }, + data: { + ratedVideos: { + create: { + viewCount: 1, + duration: 100, + url: 'xyz', + rating: 100, + owner: { connect: { id: user.id } }, + }, + }, + }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([ + expect.objectContaining({ viewCount: 1, duration: 100, url: 'xyz', rating: 100 }), + ]), + }); + + // nested create a relation from base + const newVideo = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + await expect( + db.ratedVideo.update({ + where: { id: newVideo.id }, + data: { owner: { create: { id: 2 } }, url: 'xyz', duration: 200, rating: 200 }, + include: { owner: true }, + }) + ).resolves.toMatchObject({ owner: { id: 2 } }); + }); + + it('update nested updateOne', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + // update + let updated = await db.asset.update({ + where: { id: video.id }, + data: { owner: { update: { level: 1 } } }, + include: { owner: true }, + }); + expect(updated.owner.level).toBe(1); + + updated = await db.video.update({ + where: { id: video.id }, + data: { duration: 300, owner: { update: { level: 2 } } }, + include: { owner: true }, + }); + expect(updated.duration).toBe(300); + expect(updated.owner.level).toBe(2); + + updated = await db.ratedVideo.update({ + where: { id: video.id }, + data: { rating: 300, owner: { update: { level: 3 } } }, + include: { owner: true }, + }); + expect(updated.rating).toBe(300); + expect(updated.owner.level).toBe(3); + }); + + it('update nested updateMany', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + // updateMany + await db.user.update({ + where: { id: user.id }, + data: { + ratedVideos: { + create: { url: 'xyz', duration: 111, rating: 222, owner: { connect: { id: user.id } } }, + }, + }, + }); + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { updateMany: { where: { duration: 111 }, data: { rating: 333 } } } }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ ratedVideos: expect.arrayContaining([expect.objectContaining({ rating: 333 })]) }); + }); + + it('update nested deleteOne', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + // delete with base + await db.user.update({ + where: { id: user.id }, + data: { assets: { delete: { id: video.id } } }, + }); + await expect(db.asset.findUnique({ where: { id: video.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: video.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: video.id } })).resolves.toBeNull(); + + // delete with concrete + let vid = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'xyz', + duration: 111, + rating: 222, + }, + }); + await db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { delete: { id: vid.id } } }, + }); + await expect(db.asset.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); + + // delete with mixed filter + vid = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'xyz', + duration: 111, + rating: 222, + }, + }); + await db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { delete: { id: vid.id, duration: 111 } } }, + }); + await expect(db.asset.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); + + // delete not found + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { delete: { id: vid.id } } }, + }) + ).toBeNotFound(); + }); + + it('update nested deleteMany', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + // delete with base no filter + await db.user.update({ + where: { id: user.id }, + data: { assets: { deleteMany: {} } }, + }); + await expect(db.asset.findUnique({ where: { id: video.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: video.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: video.id } })).resolves.toBeNull(); + + // delete with concrete + let vid1 = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'abc', + duration: 111, + rating: 111, + }, + }); + let vid2 = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'xyz', + duration: 222, + rating: 222, + }, + }); + await db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { deleteMany: { rating: 111 } } }, + }); + await expect(db.asset.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: vid2.id } })).toResolveTruthy(); + await db.asset.deleteMany(); + + // delete with mixed args + vid1 = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'abc', + duration: 111, + rating: 111, + viewCount: 111, + }, + }); + vid2 = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'xyz', + duration: 222, + rating: 222, + viewCount: 222, + }, + }); + await db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { deleteMany: { url: 'abc', rating: 111, viewCount: 111 } } }, + }); + await expect(db.asset.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: vid2.id } })).toResolveTruthy(); + await db.asset.deleteMany(); + + // delete not found + vid1 = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'abc', + duration: 111, + rating: 111, + }, + }); + vid2 = await db.ratedVideo.create({ + data: { + user: { connect: { id: user.id } }, + owner: { connect: { id: user.id } }, + url: 'xyz', + duration: 222, + rating: 222, + }, + }); + await db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { deleteMany: { url: 'abc', rating: 222 } } }, + }); + await expect(db.asset.count()).resolves.toBe(2); + }); + + it('update nested relation manipulation', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + // connect, disconnect with base + await expect( + db.user.update({ + where: { id: user.id }, + data: { assets: { disconnect: { id: video.id } } }, + include: { assets: true }, + }) + ).resolves.toMatchObject({ + assets: expect.arrayContaining([]), + }); + await expect( + db.user.update({ + where: { id: user.id }, + data: { assets: { connect: { id: video.id } } }, + include: { assets: true }, + }) + ).resolves.toMatchObject({ + assets: expect.arrayContaining([expect.objectContaining({ id: video.id })]), + }); + + /// connect, disconnect with concrete + + let vid1 = await db.ratedVideo.create({ + data: { + url: 'abc', + duration: 111, + rating: 111, + }, + }); + let vid2 = await db.ratedVideo.create({ + data: { + url: 'xyz', + duration: 222, + rating: 222, + }, + }); + + // connect not found + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { connect: [{ id: vid2.id + 1 }] } }, + include: { ratedVideos: true }, + }) + ).toBeRejectedWithCode(PrismaErrorCode.REQUIRED_CONNECTED_RECORD_NOT_FOUND); + + // connect found + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { connect: [{ id: vid1.id, duration: vid1.duration, rating: vid1.rating }] } }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([expect.objectContaining({ id: vid1.id })]), + }); + + // connectOrCreate + await expect( + db.user.update({ + where: { id: user.id }, + data: { + ratedVideos: { + connectOrCreate: [ + { + where: { id: vid2.id, duration: 333 }, + create: { + url: 'xyz', + duration: 333, + rating: 333, + }, + }, + ], + }, + }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([expect.objectContaining({ duration: 333 })]), + }); + + // disconnect not found + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { disconnect: [{ id: vid2.id }] } }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([expect.objectContaining({ id: vid1.id })]), + }); + + // disconnect found + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { disconnect: [{ id: vid1.id, duration: vid1.duration, rating: vid1.rating }] } }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([]), + }); + + // set + await expect( + db.user.update({ + where: { id: user.id }, + data: { + ratedVideos: { + set: [ + { id: vid1.id, viewCount: vid1.viewCount }, + { id: vid2.id, viewCount: vid2.viewCount }, + ], + }, + }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([ + expect.objectContaining({ id: vid1.id }), + expect.objectContaining({ id: vid2.id }), + ]), + }); + await expect( + db.user.update({ + where: { id: user.id }, + data: { ratedVideos: { set: [] } }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([]), + }); + await expect( + db.user.update({ + where: { id: user.id }, + data: { + ratedVideos: { + set: { id: vid1.id, viewCount: vid1.viewCount }, + }, + }, + include: { ratedVideos: true }, + }) + ).resolves.toMatchObject({ + ratedVideos: expect.arrayContaining([expect.objectContaining({ id: vid1.id })]), + }); + }); + + it('updateMany', async () => { + const { db, videoWithOwner: video, user } = await setup(); + const otherVideo = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 10000, duration: 10000, url: 'xyz', rating: 10000 }, + }); + + // update only the current level + await expect( + db.ratedVideo.updateMany({ + where: { rating: video.rating, viewCount: video.viewCount }, + data: { rating: 100 }, + }) + ).resolves.toMatchObject({ count: 1 }); + let read = await db.ratedVideo.findUnique({ where: { id: video.id } }); + expect(read).toMatchObject({ rating: 100 }); + + // update with concrete + await expect( + db.ratedVideo.updateMany({ + where: { id: video.id }, + data: { viewCount: 1, duration: 11, rating: 101 }, + }) + ).resolves.toMatchObject({ count: 1 }); + read = await db.ratedVideo.findUnique({ where: { id: video.id } }); + expect(read).toMatchObject({ viewCount: 1, duration: 11, rating: 101 }); + + // update with base + await db.video.updateMany({ + where: { viewCount: 1, duration: 11 }, + data: { viewCount: 2, duration: 12 }, + }); + read = await db.ratedVideo.findUnique({ where: { id: video.id } }); + expect(read).toMatchObject({ viewCount: 2, duration: 12 }); + + // update with base + await db.asset.updateMany({ + where: { viewCount: 2 }, + data: { viewCount: 3 }, + }); + read = await db.ratedVideo.findUnique({ where: { id: video.id } }); + expect(read.viewCount).toBe(3); + + // the other video is unchanged + await expect(await db.ratedVideo.findUnique({ where: { id: otherVideo.id } })).toMatchObject(otherVideo); + + // update with concrete no where + await expect( + db.ratedVideo.updateMany({ + data: { viewCount: 111, duration: 111, rating: 111 }, + }) + ).resolves.toMatchObject({ count: 2 }); + await expect(db.ratedVideo.findUnique({ where: { id: video.id } })).resolves.toMatchObject({ duration: 111 }); + await expect(db.ratedVideo.findUnique({ where: { id: otherVideo.id } })).resolves.toMatchObject({ + duration: 111, + }); + + // set discriminator + await expect(db.ratedVideo.updateMany({ data: { assetType: 'Image' } })).rejects.toThrow('is a discriminator'); + await expect(db.ratedVideo.updateMany({ data: { videoType: 'RatedVideo' } })).rejects.toThrow( + 'is a discriminator' + ); + }); + + it('upsert', async () => { + const { db, videoWithOwner: video, user } = await setup(); + + await expect( + db.asset.upsert({ + where: { id: video.id }, + create: { id: video.id, viewCount: 1 }, + update: { viewCount: 2 }, + }) + ).rejects.toThrow('is a delegate'); + + // update + await expect( + db.ratedVideo.upsert({ + where: { id: video.id }, + create: { + viewCount: 1, + duration: 300, + url: 'xyz', + rating: 100, + owner: { connect: { id: user.id } }, + }, + update: { duration: 200 }, + }) + ).resolves.toMatchObject({ + id: video.id, + duration: 200, + }); + + // create + const created = await db.ratedVideo.upsert({ + where: { id: video.id + 1 }, + create: { viewCount: 1, duration: 300, url: 'xyz', rating: 100, owner: { connect: { id: user.id } } }, + update: { duration: 200 }, + }); + expect(created.id).not.toEqual(video.id); + expect(created.duration).toBe(300); + }); + + it('delete', async () => { + let { db, user, video: ratedVideo } = await setup(); + + let deleted = await db.ratedVideo.delete({ + where: { id: ratedVideo.id }, + select: { rating: true, owner: true }, + }); + expect(deleted).toMatchObject({ rating: 100 }); + expect(deleted.owner).toMatchObject(user); + await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + + // delete with base + ratedVideo = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + const video = await db.video.findUnique({ where: { id: ratedVideo.id } }); + deleted = await db.video.delete({ where: { id: ratedVideo.id }, include: { owner: true } }); + expect(deleted).toMatchObject(video); + expect(deleted.owner).toMatchObject(user); + await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + + // delete with concrete + ratedVideo = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + let asset = await db.asset.findUnique({ where: { id: ratedVideo.id } }); + deleted = await db.video.delete({ where: { id: ratedVideo.id }, include: { owner: true } }); + expect(deleted).toMatchObject(asset); + expect(deleted.owner).toMatchObject(user); + await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + + // delete with combined condition + ratedVideo = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + asset = await db.asset.findUnique({ where: { id: ratedVideo.id } }); + deleted = await db.video.delete({ where: { id: ratedVideo.id, viewCount: 1 } }); + expect(deleted).toMatchObject(asset); + await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); + }); + + it('deleteMany', async () => { + const { enhance } = await loadSchema(schema, { logPrismaQuery: true, enhancements: ['delegate'] }); + const db = enhance(); + + const user = await db.user.create({ data: { id: 1 } }); + + // no where + let video1 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + let video2 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, + }); + await expect(db.ratedVideo.deleteMany()).resolves.toMatchObject({ count: 2 }); + await expect(db.ratedVideo.findUnique({ where: { id: video1.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: video1.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: video1.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.findUnique({ where: { id: video2.id } })).resolves.toBeNull(); + await expect(db.video.findUnique({ where: { id: video2.id } })).resolves.toBeNull(); + await expect(db.asset.findUnique({ where: { id: video2.id } })).resolves.toBeNull(); + await expect(db.ratedVideo.count()).resolves.toBe(0); + + // with base + video1 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, + }); + video2 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, + }); + await expect(db.asset.deleteMany({ where: { viewCount: 1 } })).resolves.toMatchObject({ count: 1 }); + await expect(db.asset.count()).resolves.toBe(1); + await db.asset.deleteMany(); + + // where current level + video1 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, + }); + video2 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, + }); + await expect(db.ratedVideo.deleteMany({ where: { rating: 100 } })).resolves.toMatchObject({ count: 1 }); + await expect(db.ratedVideo.count()).resolves.toBe(1); + await db.ratedVideo.deleteMany(); + + // where mixed with base level + video1 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, + }); + video2 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, + }); + await expect(db.ratedVideo.deleteMany({ where: { viewCount: 1, duration: 100 } })).resolves.toMatchObject({ + count: 1, + }); + await expect(db.ratedVideo.count()).resolves.toBe(1); + await db.ratedVideo.deleteMany(); + + // delete not found + video1 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, + }); + video2 = await db.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, + }); + await expect(db.ratedVideo.deleteMany({ where: { viewCount: 2, duration: 100 } })).resolves.toMatchObject({ + count: 0, + }); + await expect(db.ratedVideo.count()).resolves.toBe(2); + }); + + it('aggregate', async () => { + const { db } = await setup(); + + const aggregate = await db.ratedVideo.aggregate({ + _count: true, + _sum: { rating: true }, + where: { viewCount: { gt: 0 }, rating: { gt: 10 } }, + orderBy: { + duration: 'desc', + }, + }); + expect(aggregate).toMatchObject({ _count: 1, _sum: { rating: 100 } }); + + expect(() => db.ratedVideo.aggregate({ _count: true, _sum: { rating: true, viewCount: true } })).toThrow( + 'aggregate with fields from base type is not supported yet' + ); + }); + + it('count', async () => { + const { db } = await setup(); + + let count = await db.ratedVideo.count(); + expect(count).toBe(1); + + count = await db.ratedVideo.count({ + select: { _all: true, rating: true }, + where: { viewCount: { gt: 0 }, rating: { gt: 10 } }, + }); + expect(count).toMatchObject({ _all: 1, rating: 1 }); + + expect(() => db.ratedVideo.count({ select: { rating: true, viewCount: true } })).toThrow( + 'count with fields from base type is not supported yet' + ); + }); + + it('groupBy', async () => { + const { db, video } = await setup(); + + let group = await db.ratedVideo.groupBy({ by: ['rating'] }); + expect(group).toHaveLength(1); + expect(group[0]).toMatchObject({ rating: video.rating }); + + group = await db.ratedVideo.groupBy({ + by: ['id', 'rating'], + where: { viewCount: { gt: 0 }, rating: { gt: 10 } }, + }); + expect(group).toHaveLength(1); + expect(group[0]).toMatchObject({ id: video.id, rating: video.rating }); + + group = await db.ratedVideo.groupBy({ + by: ['id'], + _sum: { rating: true }, + }); + expect(group).toHaveLength(1); + expect(group[0]).toMatchObject({ id: video.id, _sum: { rating: video.rating } }); + + group = await db.ratedVideo.groupBy({ + by: ['id'], + _sum: { rating: true }, + having: { rating: { _sum: { gt: video.rating } } }, + }); + expect(group).toHaveLength(0); + + expect(() => db.ratedVideo.groupBy({ by: 'viewCount' })).toThrow( + 'groupBy with fields from base type is not supported yet' + ); + expect(() => db.ratedVideo.groupBy({ having: { rating: { gt: 0 }, viewCount: { gt: 0 } } })).toThrow( + 'groupBy with fields from base type is not supported yet' + ); + }); + + it('many to many', async () => { + const { enhance } = await loadSchema(POLYMORPHIC_MANY_TO_MANY_SCHEMA); + const db = enhance(); + + const video = await db.video.create({ data: { viewCount: 1, duration: 100 } }); + const image = await db.image.create({ data: { viewCount: 2, format: 'png' } }); + + await expect( + db.user.create({ + data: { + id: 1, + level: 10, + assets: { + connect: [{ id: video.id }, { id: image.id }], + }, + }, + include: { assets: true }, + }) + ).resolves.toMatchObject({ + id: 1, + level: 10, + assets: expect.arrayContaining([video, image]), + }); + + await expect(db.user.findUnique({ where: { id: 1 }, include: { assets: true } })).resolves.toMatchObject({ + id: 1, + assets: expect.arrayContaining([video, image]), + }); + await expect(db.asset.findUnique({ where: { id: video.id }, include: { users: true } })).resolves.toMatchObject( + { + id: video.id, + users: expect.arrayContaining([{ id: 1, level: 10 }]), + } + ); + }); + + it('typescript compilation plain prisma', async () => { + const src = ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + const prisma = new PrismaClient(); + + async function main() { + const db = enhance(prisma); + + const user1 = await db.user.create({ data: { } }); + + await db.ratedVideo.create({ + data: { + owner: { connect: { id: user1.id } }, + duration: 100, + url: 'abc', + rating: 10, + }, + }); + + await db.image.create({ + data: { + owner: { connect: { id: user1.id } }, + format: 'webp', + }, + }); + + const video = await db.video.findFirst({ include: { owner: true } }); + console.log(video?.duration); + console.log(video?.viewCount); + + const asset = await db.asset.findFirstOrThrow(); + console.log(asset.assetType); + console.log(asset.viewCount); + + if (asset.assetType === 'Video') { + console.log('Video: duration', asset.duration); + } else { + console.log('Image: format', asset.format); + } + } + + main(); + `; + await loadSchema(schema, { + compile: true, + enhancements: ['delegate'], + extraSourceFiles: [ + { + name: 'main.ts', + content: src, + }, + ], + }); + }); + + it('typescript compilation extended prisma', async () => { + const src = ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + const prisma = new PrismaClient().$extends({ + model: { + user: { + async signUp() { + return prisma.user.create({ data: {} }); + }, + }, + }, + }); + + async function main() { + const db = enhance(prisma); + + const user1 = await db.user.signUp(); + + await db.ratedVideo.create({ + data: { + owner: { connect: { id: user1.id } }, + duration: 100, + url: 'abc', + rating: 10, + }, + }); + + await db.image.create({ + data: { + owner: { connect: { id: user1.id } }, + format: 'webp', + }, + }); + + const video = await db.video.findFirst({ include: { owner: true } }); + console.log(video?.duration); + console.log(video?.viewCount); + + const asset = await db.asset.findFirstOrThrow(); + console.log(asset.assetType); + console.log(asset.viewCount); + + if (asset.assetType === 'Video') { + console.log('Video: duration', asset.duration); + } else { + console.log('Image: format', asset.format); + } + } + + main(); + `; + await loadSchema(schema, { + compile: true, + enhancements: ['delegate'], + extraSourceFiles: [ + { + name: 'main.ts', + content: src, + }, + ], + }); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts new file mode 100644 index 000000000..cd566c71f --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts @@ -0,0 +1,53 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1058', () => { + it('test', 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 loadSchema(schema, { pushDb: false, provider: 'postgresql' }); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/issue-1064.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1064.test.ts new file mode 100644 index 000000000..a8505f507 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1064.test.ts @@ -0,0 +1,291 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1064', () => { + it('test', async () => { + const schema = ` + model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? // @db.Text + access_token String? // @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? // @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@allow('all', auth().id == userId) + @@unique([provider, providerAccountId]) + } + + model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@allow('all', auth().id == userId) + } + + model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@allow('all', true) + @@unique([identifier, token]) + } + + model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String + accounts Account[] + sessions Session[] + + username String @unique @length(min: 4, max: 20) + about String? @length(max: 500) + location String? @length(max: 100) + + role String @default("USER") @deny(operation: "update", auth().role != "ADMIN") + + inserted_at DateTime @default(now()) + updated_at DateTime @updatedAt() @default(now()) + + editComments EditComment[] + + posts Post[] + rankings UserRanking[] + ratings UserRating[] + favorites UserFavorite[] + + people Person[] + studios Studio[] + edits Edit[] + attachments Attachment[] + galleries Gallery[] + + uploads UserUpload[] + + maxUploadsPerDay Int @default(10) + maxEditsPerDay Int @default(10) + + // everyone can signup, and user profile is also publicly readable + @@allow('create,read', true) + // only the user can update or delete their own profile + @@allow('update,delete', auth() == this) + } + + abstract model UserEntityRelation { + entityId String? + entity Entity? @relation(fields: [entityId], references: [id], onUpdate: NoAction) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + + // everyone can read + @@allow('read', true) + @@allow('create,update,delete', auth().id == this.userId) + + @@unique([userId,entityId]) + } + + model UserUpload { + timestamp DateTime @default(now()) + + key String @id + url String @unique + size Int + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@allow('create', auth().id == userId) + @@allow('all', auth().role == "ADMIN") + } + + model Post { + id Int @id @default(autoincrement()) + title String @length(max: 100) + body String @length(max: 1000) + createdAt DateTime @default(now()) + + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@allow('read', true) + @@allow('create,update,delete', auth().id == authorId && auth().role == "ADMIN") + } + + model Edit extends UserEntityRelation { + id String @id @default(cuid()) + status String @default("PENDING") @allow('update', auth().role in ["ADMIN", "MODERATOR"]) + type String @allow('update', false) + timestamp DateTime @default(now()) + note String? @length(max: 300) + // for creates - createPayload & updates - data before diff is applied + data String? + // for updates + diff String? + + comments EditComment[] + } + + model EditComment { + id Int @id @default(autoincrement()) + timestamp DateTime @default(now()) + content String @length(max: 300) + editId String + edit Edit @relation(fields: [editId], references: [id], onUpdate: Cascade) + authorId String + author User @relation(fields: [authorId], references: [id], onUpdate: Cascade) + + // everyone can read + @@allow('read', true) + @@allow('create,update,delete', auth().id == this.authorId || auth().role in ["ADMIN", "MODERATOR"]) + } + + model MetadataIdentifier { + id Int @default(autoincrement()) @id + + identifier String + + metadataSource String + MetadataSource MetadataSource @relation(fields: [metadataSource], references: [slug], onUpdate: Cascade) + + entities Entity[] + + @@unique([identifier, metadataSource]) + + @@allow('read', true) + @@allow('create,update,delete', auth().role in ["ADMIN", "MODERATOR"]) + } + + model MetadataSource { + slug String @id + name String @unique + identifierRegex String + desc String? + url String + icon String + identifiers MetadataIdentifier[] + + @@allow('all', auth().role == "ADMIN") + } + + model Attachment extends UserEntityRelation { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + key String @unique + url String @unique + galleries Gallery[] + @@allow('delete', auth().role in ["ADMIN", "MODERATOR"]) + } + + model Entity { + id String @id @default(cuid()) + name String + desc String? + + attachments Attachment[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @default(now()) + + type String + + status String @default("PENDING") // PENDING ON INITIAL CREATION + verified Boolean @default(false) + + edits Edit[] + userRankings UserRanking[] + userFavorites UserFavorite[] + userRatings UserRating[] + metaIdentifiers MetadataIdentifier[] + + @@delegate(type) + + @@allow('read', true) + @@allow('create', auth() != null) + @@allow('update', auth().role in ["ADMIN", "MODERATOR"]) + @@allow('delete', auth().role == "ADMIN") + } + + model Person extends Entity { + studios Studio[] + owners User[] + clips Clip[] + events Event[] + galleries Gallery[] + } + + model Studio extends Entity { + people Person[] + owners User[] + clips Clip[] + events Event[] + galleries Gallery[] + } + + model Clip extends Entity { + url String? + people Person[] + studios Studio[] + galleries Gallery[] + } + + model UserRanking extends UserEntityRelation { + id String @id @default(cuid()) + rank Int @gte(1) @lte(100) + note String? @length(max: 300) + } + + model UserFavorite extends UserEntityRelation { + id String @id @default(cuid()) + favoritedAt DateTime @default(now()) + } + + model UserRating extends UserEntityRelation { + id String @id @default(cuid()) + rating Int @gte(1) @lte(5) + note String? @length(max: 500) + ratedAt DateTime @default(now()) + } + + model Event { + id Int @id @default(autoincrement()) + name String @length(max: 100) + desc String? @length(max: 500) + location String? @length(max: 100) + date DateTime? + people Person[] + studios Studio[] + + @@allow('read', true) + @@allow('create,update,delete', auth().role == "ADMIN") + } + + model Gallery { + id String @id @default(cuid()) + studioId String? + personId String? + timestamp DateTime @default(now()) + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction) + people Person[] + studios Studio[] + clips Clip[] + attachments Attachment[] + + @@allow('read', true) + @@allow('create,update,delete', auth().id == this.authorId && auth().role == "ADMIN") + } + `; + + await loadSchema(schema); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts new file mode 100644 index 000000000..8b1945b8d --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts @@ -0,0 +1,69 @@ +import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1100', () => { + it('missing opposite relation', async () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String? + content Content[] + post Post[] + } + + model Content { + id String @id @default(cuid()) + published Boolean @default(false) + contentType String + @@delegate(contentType) + + user User @relation(fields: [userId], references: [id]) + userId String + } + + model Post extends Content { + title String + } + + model Image extends Content { + url String + } + `; + + await expect(loadModelWithError(schema)).resolves.toContain( + 'The relation field "post" on model "User" is missing an opposite relation field on model "Post"' + ); + }); + + it('success', async () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String? + content Content[] + post Post[] + } + + model Content { + id String @id @default(cuid()) + published Boolean @default(false) + contentType String + @@delegate(contentType) + + user User @relation(fields: [userId], references: [id]) + userId String + } + + model Post extends Content { + title String + author User @relation(fields: [authorId], references: [id]) + authorId String + } + + model Image extends Content { + url String + } + `; + + await expect(loadSchema(schema)).toResolveTruthy(); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/issue-1123.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1123.test.ts new file mode 100644 index 000000000..02ee7a983 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1123.test.ts @@ -0,0 +1,47 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1123', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + 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) + } + ` + ); + + const db = enhance(); + 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/integration/tests/enhancements/with-delegate/issue-1135.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1135.test.ts new file mode 100644 index 000000000..1497621d5 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1135.test.ts @@ -0,0 +1,85 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1135', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Attachment { + id String @id @default(cuid()) + url String + myEntityId String + myEntity Entity @relation(fields: [myEntityId], references: [id], onUpdate: NoAction) + + @@allow('all', true) + } + + model Entity { + id String @id @default(cuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @default(now()) + + attachments Attachment[] + + type String + @@delegate(type) + @@allow('all', true) + } + + model Person extends Entity { + age Int? + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { enhance } from '.zenstack/enhance'; +import { PrismaClient } from '@prisma/client'; + +const db = enhance(new PrismaClient()); + +db.person.create({ + data: { + name: 'test', + attachments: { + create: { + url: 'https://...', + }, + }, + }, +}); + `, + }, + ], + } + ); + + const db = enhance(); + 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/integration/tests/enhancements/with-delegate/issue-1149.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1149.test.ts new file mode 100644 index 000000000..3f3f43e85 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1149.test.ts @@ -0,0 +1,112 @@ +import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1149', () => { + let prisma: any; + let dbUrl: string; + + beforeAll(async () => { + dbUrl = await createPostgresDb('issue-1149'); + }); + + afterAll(async () => { + if (prisma) { + await prisma.$disconnect(); + } + dropPostgresDb('issue-1149'); + }); + + it('test', 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 { enhance, prisma: _prisma } = await loadSchema(schema, { + provider: 'postgresql', + dbUrl, + enhancements: ['delegate'], + }); + + prisma = _prisma; + const db = enhance(); + + 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, + entity: { connect: { id: person.id } }, + user: { connect: { id: user.id } }, + }, + }) + ).resolves.toMatchObject({ count: 1 }); + + await expect( + db.userRanking.createMany({ + data: [ + { + rank: 2, + entity: { connect: { id: person.id } }, + user: { connect: { id: user.id } }, + }, + { + rank: 3, + entity: { connect: { id: person.id } }, + user: { connect: { id: 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/integration/tests/enhancements/with-delegate/issue-1243.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1243.test.ts new file mode 100644 index 000000000..941bc9b61 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1243.test.ts @@ -0,0 +1,55 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +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 loadSchema(schema, { + enhancements: ['delegate'], + }); + }); + + 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 loadSchema(schema, { + enhancements: ['delegate'], + }); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts new file mode 100644 index 000000000..8e6562e20 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts @@ -0,0 +1,25 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { POLYMORPHIC_SCHEMA } from './utils'; +import path from 'path'; + +describe('Polymorphic Plugin Interaction Test', () => { + it('tanstack-query', async () => { + const tanstackPlugin = path.resolve(__dirname, '../../../../../packages/plugins/tanstack-query/dist'); + const schema = ` + ${POLYMORPHIC_SCHEMA} + + plugin hooks { + provider = '${tanstackPlugin}' + output = '$projectRoot/hooks' + target = 'react' + version = 'v5' + } + `; + + await loadSchema(schema, { + compile: true, + copyDependencies: [tanstackPlugin], + extraDependencies: ['@tanstack/react-query'], + }); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts new file mode 100644 index 000000000..d0316595d --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts @@ -0,0 +1,217 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Polymorphic Policy Test', () => { + it('simple boolean', async () => { + const booleanCondition = ` + model User { + id Int @id @default(autoincrement()) + level Int @default(0) + assets Asset[] + banned Boolean @default(false) + + @@allow('all', true) + } + + model Asset { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + published Boolean @default(false) + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + assetType String + viewCount Int @default(0) + + @@delegate(assetType) + @@allow('create', viewCount >= 0) + @@deny('read', !published) + @@allow('read', true) + @@deny('all', owner.banned) + } + + model Video extends Asset { + watched Boolean @default(false) + videoType String + + @@delegate(videoType) + @@deny('read', !watched) + @@allow('read', true) + } + + model RatedVideo extends Video { + rated Boolean @default(false) + @@deny('read', !rated) + @@allow('read', true) + } + `; + + const booleanExpression = ` + model User { + id Int @id @default(autoincrement()) + level Int @default(0) + assets Asset[] + banned Boolean @default(false) + + @@allow('all', true) + } + + model Asset { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + published Boolean @default(false) + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + assetType String + viewCount Int @default(0) + + @@delegate(assetType) + @@allow('create', viewCount >= 0) + @@deny('read', published == false) + @@allow('read', true) + @@deny('all', owner.banned == true) + } + + model Video extends Asset { + watched Boolean @default(false) + videoType String + + @@delegate(videoType) + @@deny('read', watched == false) + @@allow('read', true) + } + + model RatedVideo extends Video { + rated Boolean @default(false) + @@deny('read', rated == false) + @@allow('read', true) + } + `; + + for (const schema of [booleanCondition, booleanExpression]) { + const { enhanceRaw: enhance, prisma } = await loadSchema(schema); + + const fullDb = enhance(prisma, undefined, { kinds: ['delegate'], logPrismaQuery: true }); + + const user = await fullDb.user.create({ data: { id: 1 } }); + const userDb = enhance( + prisma, + { user: { id: user.id } }, + { kinds: ['delegate', 'policy'], logPrismaQuery: true } + ); + + // violating Asset create + await expect( + userDb.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, viewCount: -1 }, + }) + ).toBeRejectedByPolicy(); + + let video = await fullDb.ratedVideo.create({ + data: { owner: { connect: { id: user.id } } }, + }); + // violating all three layer read + await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveNull(); + await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveNull(); + await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); + + video = await fullDb.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, published: true }, + }); + // violating Video && RatedVideo read + await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveTruthy(); + await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveNull(); + await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); + + video = await fullDb.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, published: true, watched: true }, + }); + // violating RatedVideo read + await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveTruthy(); + await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveTruthy(); + await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); + + video = await fullDb.ratedVideo.create({ + data: { owner: { connect: { id: user.id } }, rated: true, watched: true, published: true }, + }); + // meeting all read conditions + await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveTruthy(); + await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveTruthy(); + await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveTruthy(); + + // ban the user + await prisma.user.update({ where: { id: user.id }, data: { banned: true } }); + + // banned user can't read + await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveNull(); + await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveNull(); + await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); + + // banned user can't create + await expect( + userDb.ratedVideo.create({ + data: { owner: { connect: { id: user.id } } }, + }) + ).toBeRejectedByPolicy(); + } + }); + + it('interaction with updateMany/deleteMany', async () => { + const schema = ` + model User { + id Int @id @default(autoincrement()) + level Int @default(0) + assets Asset[] + banned Boolean @default(false) + + @@allow('all', true) + } + + model Asset { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + published Boolean @default(false) + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + assetType String + viewCount Int @default(0) + version Int @default(0) + + @@delegate(assetType) + @@deny('update', viewCount > 0) + @@deny('delete', viewCount > 0) + @@allow('all', true) + } + + model Video extends Asset { + watched Boolean @default(false) + + @@deny('update', watched) + @@deny('delete', watched) + } + `; + + const { enhance } = await loadSchema(schema, { + logPrismaQuery: true, + }); + const db = enhance(); + + const user = await db.user.create({ data: { id: 1 } }); + const vid1 = await db.video.create({ + data: { watched: false, viewCount: 0, owner: { connect: { id: user.id } } }, + }); + const vid2 = await db.video.create({ + data: { watched: true, viewCount: 1, owner: { connect: { id: user.id } } }, + }); + + await expect(db.asset.updateMany({ data: { version: { increment: 1 } } })).resolves.toMatchObject({ + count: 1, + }); + await expect(db.asset.findUnique({ where: { id: vid1.id } })).resolves.toMatchObject({ version: 1 }); + await expect(db.asset.findUnique({ where: { id: vid2.id } })).resolves.toMatchObject({ version: 0 }); + + await expect(db.asset.deleteMany()).resolves.toMatchObject({ + count: 1, + }); + await expect(db.asset.findUnique({ where: { id: vid1.id } })).toResolveNull(); + await expect(db.asset.findUnique({ where: { id: vid2.id } })).toResolveTruthy(); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/utils.ts b/tests/integration/tests/enhancements/with-delegate/utils.ts new file mode 100644 index 000000000..c04106ea2 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/utils.ts @@ -0,0 +1,75 @@ +export const POLYMORPHIC_SCHEMA = ` +model User { + id Int @id @default(autoincrement()) + level Int @default(0) + assets Asset[] + ratedVideos RatedVideo[] @relation('direct') + + @@allow('all', true) +} + +model Asset { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + viewCount Int @default(0) + owner User? @relation(fields: [ownerId], references: [id]) + ownerId Int? + assetType String + + @@delegate(assetType) + @@allow('all', true) +} + +model Video extends Asset { + duration Int + url String + videoType String + + @@delegate(videoType) +} + +model RatedVideo extends Video { + rating Int + user User? @relation(name: 'direct', fields: [userId], references: [id]) + userId Int? +} + +model Image extends Asset { + format String + gallery Gallery? @relation(fields: [galleryId], references: [id]) + galleryId Int? +} + +model Gallery { + id Int @id @default(autoincrement()) + images Image[] +} +`; + +export const POLYMORPHIC_MANY_TO_MANY_SCHEMA = ` +model User { + id Int @id @default(autoincrement()) + level Int @default(0) + assets Asset[] + + @@allow('all', true) +} + +model Asset { + id Int @id @default(autoincrement()) + viewCount Int @default(0) + users User[] + assetType String + + @@delegate(assetType) + @@allow('all', true) +} + +model Video extends Asset { + duration Int +} + +model Image extends Asset { + format String +} +`; diff --git a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts index e2db0a3f8..73fdaf806 100644 --- a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts +++ b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts @@ -1,4 +1,3 @@ -import { withOmit } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; @@ -33,9 +32,9 @@ describe('Omit test', () => { `; it('omit tests', async () => { - const { withOmit } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withOmit(); + const db = enhance(); const r = await db.user.create({ include: { profile: true }, data: { @@ -79,9 +78,12 @@ describe('Omit test', () => { }); it('customization', async () => { - const { prisma } = await loadSchema(model, { getPrismaOnly: true, output: './zen' }); + const { prisma, enhance } = await loadSchema(model, { + output: './zen', + enhancements: ['omit'], + }); - const db = withOmit(prisma, { loadPath: './zen' }); + const db = enhance(prisma); const r = await db.user.create({ include: { profile: true }, data: { @@ -93,7 +95,7 @@ describe('Omit test', () => { expect(r.password).toBeUndefined(); expect(r.profile.image).toBeUndefined(); - const db1 = withOmit(prisma, { modelMeta: require(path.resolve('./zen/model-meta')).default }); + const db1 = enhance(prisma, { modelMeta: require(path.resolve('./zen/model-meta')).default }); const r1 = await db1.user.create({ include: { profile: true }, data: { @@ -107,7 +109,7 @@ describe('Omit test', () => { }); it('to-many', async () => { - const { withOmit } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(cuid()) @@ -133,10 +135,11 @@ describe('Omit test', () => { @@allow('all', true) } - ` + `, + { enhancements: ['omit'] } ); - const db = withOmit(); + const db = enhance(); const r = await db.user.create({ include: { posts: { include: { images: true } } }, data: { diff --git a/tests/integration/tests/enhancements/with-password/with-password.test.ts b/tests/integration/tests/enhancements/with-password/with-password.test.ts index 62e30636b..37b23ecde 100644 --- a/tests/integration/tests/enhancements/with-password/with-password.test.ts +++ b/tests/integration/tests/enhancements/with-password/with-password.test.ts @@ -1,4 +1,3 @@ -import { withPassword } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import { compareSync } from 'bcryptjs'; import path from 'path'; @@ -23,9 +22,9 @@ describe('Password test', () => { }`; it('password tests', async () => { - const { withPassword } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPassword(); + const db = enhance(); const r = await db.user.create({ data: { id: '1', @@ -42,26 +41,4 @@ describe('Password test', () => { }); expect(compareSync('abc456', r1.password)).toBeTruthy(); }); - - it('customization', async () => { - const { prisma } = await loadSchema(model, { getPrismaOnly: true, output: './zen' }); - - const db = withPassword(prisma, { loadPath: './zen' }); - const r = await db.user.create({ - data: { - id: '1', - password: 'abc123', - }, - }); - expect(compareSync('abc123', r.password)).toBeTruthy(); - - const db1 = withPassword(prisma, { modelMeta: require(path.resolve('./zen/model-meta')).default }); - const r1 = await db1.user.create({ - data: { - id: '2', - password: 'abc123', - }, - }); - expect(compareSync('abc123', r1.password)).toBeTruthy(); - }); }); diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index 8f095f677..0cac82e8a 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -1,7 +1,7 @@ import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; -describe('With Policy: auth() test', () => { +describe('auth() runtime test', () => { let origDir: string; beforeAll(async () => { @@ -13,7 +13,7 @@ describe('With Policy: auth() test', () => { }); it('undefined user with string id simple', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -29,15 +29,15 @@ describe('With Policy: auth() test', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - const authDb = withPolicy({ id: 'user1' }); + const authDb = enhance({ id: 'user1' }); await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('undefined user with string id more', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -53,15 +53,15 @@ describe('With Policy: auth() test', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - const authDb = withPolicy({ id: 'user1' }); + const authDb = enhance({ id: 'user1' }); await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('undefined user with int id', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -77,15 +77,15 @@ describe('With Policy: auth() test', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - const authDb = withPolicy({ id: 'user1' }); + const authDb = enhance({ id: 'user1' }); await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('undefined user compared with field', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -106,21 +106,21 @@ describe('With Policy: auth() test', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.user.create({ data: { id: 'user1' } })).toResolveTruthy(); await expect(db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' } })).toResolveTruthy(); - const authDb = withPolicy(); + const authDb = enhance(); await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - expect(() => withPolicy({ id: null })).toThrow(/Invalid user context/); + expect(() => enhance({ id: null })).toThrow(/Invalid user context/); - const authDb2 = withPolicy({ id: 'user1' }); + const authDb2 = enhance({ id: 'user1' }); await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); it('undefined user compared with field more', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -141,18 +141,18 @@ describe('With Policy: auth() test', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.user.create({ data: { id: 'user1' } })).toResolveTruthy(); await expect(db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' } })).toResolveTruthy(); await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb2 = withPolicy({ id: 'user1' }); + const authDb2 = enhance({ id: 'user1' }); await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); it('undefined user non-id field', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -174,20 +174,20 @@ describe('With Policy: auth() test', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.user.create({ data: { id: 'user1', role: 'USER' } })).toResolveTruthy(); await expect(db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' } })).toResolveTruthy(); await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb = withPolicy({ id: 'user1', role: 'USER' }); + const authDb = enhance({ id: 'user1', role: 'USER' }); await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb1 = withPolicy({ id: 'user2', role: 'ADMIN' }); + const authDb1 = enhance({ id: 'user2', role: 'ADMIN' }); await expect(authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); it('non User auth model', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Foo { id String @id @default(uuid()) @@ -206,15 +206,15 @@ describe('With Policy: auth() test', () => { ` ); - const userDb = withPolicy({ id: 'user1', role: 'USER' }); + const userDb = enhance({ id: 'user1', role: 'USER' }); await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - const adminDb = withPolicy({ id: 'user1', role: 'ADMIN' }); + const adminDb = enhance({ id: 'user1', role: 'ADMIN' }); await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('User model ignored', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -233,15 +233,15 @@ describe('With Policy: auth() test', () => { ` ); - const userDb = withPolicy({ id: 'user1', role: 'USER' }); + const userDb = enhance({ id: 'user1', role: 'USER' }); await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - const adminDb = withPolicy({ id: 'user1', role: 'ADMIN' }); + const adminDb = enhance({ id: 'user1', role: 'ADMIN' }); await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('Auth model ignored', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Foo { id String @id @default(uuid()) @@ -261,10 +261,10 @@ describe('With Policy: auth() test', () => { ` ); - const userDb = withPolicy({ id: 'user1', role: 'USER' }); + const userDb = enhance({ id: 'user1', role: 'USER' }); await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - const adminDb = withPolicy({ id: 'user1', role: 'ADMIN' }); + const adminDb = enhance({ id: 'user1', role: 'ADMIN' }); await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); @@ -363,4 +363,441 @@ describe('With Policy: auth() test', () => { enhance({ id: '1', posts: [{ id: '1', published: true, comments: [] }] }).post.create(createPayload) ).toResolveTruthy(); }); + + it('Default auth() on literal fields', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id + name String + score Int + + } + + model Post { + id String @id @default(uuid()) + title String + score Int? @default(auth().score) + authorName String? @default(auth().name) + + @@allow('all', true) + } + ` + ); + + const userDb = enhance({ id: '1', name: 'user1', score: 10 }); + await expect(userDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); + await expect(userDb.post.findMany()).resolves.toHaveLength(1); + await expect(userDb.post.count({ where: { authorName: 'user1', score: 10 } })).resolves.toBe(1); + }); + + it('Default auth() data should not override passed args', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id + name String + + } + + model Post { + id String @id @default(uuid()) + authorName String? @default(auth().name) + + @@allow('all', true) + } + ` + ); + + const userContextName = 'user1'; + const overrideName = 'no-default-auth-name'; + const userDb = enhance({ id: '1', name: userContextName }); + await expect(userDb.post.create({ data: { authorName: overrideName } })).toResolveTruthy(); + await expect(userDb.post.count({ where: { authorName: overrideName } })).resolves.toBe(1); + }); + + it('Default auth() with foreign key', async () => { + const { enhance, prisma } = await loadSchema( + ` + model User { + id String @id + email String @unique + posts Post[] + + @@allow('all', true) + + } + + model Post { + id String @id @default(uuid()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + @@allow('all', true) + } + ` + ); + + await prisma.user.create({ data: { id: 'userId-1', email: 'user1@abc.com' } }); + await prisma.user.create({ data: { id: 'userId-2', email: 'user2@abc.com' } }); + + const db = enhance({ id: 'userId-1' }); + + // default auth effective + await expect(db.post.create({ data: { title: 'post1' } })).resolves.toMatchObject({ authorId: 'userId-1' }); + + // default auth ineffective due to explicit connect + await expect( + db.post.create({ data: { title: 'post2', author: { connect: { email: 'user1@abc.com' } } } }) + ).resolves.toMatchObject({ authorId: 'userId-1' }); + + // default auth ineffective due to explicit connect + await expect( + db.post.create({ data: { title: 'post3', author: { connect: { email: 'user2@abc.com' } } } }) + ).resolves.toMatchObject({ authorId: 'userId-2' }); + }); + + it('Default auth() with nested user context value', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id + profile Profile? + posts Post[] + + @@allow('all', true) + } + + model Profile { + id String @id @default(uuid()) + image Image? + user User @relation(fields: [userId], references: [id]) + userId String @unique + } + + model Image { + id String @id @default(uuid()) + url String + profile Profile @relation(fields: [profileId], references: [id]) + profileId String @unique + } + + model Post { + id String @id @default(uuid()) + title String + defaultImageUrl String @default(auth().profile.image.url) + author User @relation(fields: [authorId], references: [id]) + authorId String + + @@allow('all', true) + } + ` + ); + const url = 'https://zenstack.dev'; + const db = enhance({ id: 'userId-1', profile: { image: { url } } }); + + // top-level create + await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); + await expect( + db.post.create({ data: { title: 'abc', author: { connect: { id: 'userId-1' } } } }) + ).resolves.toMatchObject({ defaultImageUrl: url }); + + // nested create + let result = await db.user.create({ + data: { + id: 'userId-2', + posts: { + create: [{ title: 'p1' }, { title: 'p2' }], + }, + }, + include: { posts: true }, + }); + expect(result.posts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'p1', defaultImageUrl: url }), + expect.objectContaining({ title: 'p2', defaultImageUrl: url }), + ]) + ); + }); + + it('Default auth() without user context', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id + posts Post[] + + @@allow('all', true) + } + + model Post { + id String @id @default(uuid()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + @@allow('all', true) + } + ` + ); + + const db = enhance(); + await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); + await expect(db.post.create({ data: { title: 'title' } })).rejects.toThrow( + 'Evaluating default value of field `authorId` requires a user context' + ); + await expect(db.post.findMany({})).toResolveTruthy(); + }); + + it('Default auth() field optionality', async () => { + await loadSchema( + ` + model User { + id String @id + posts Post[] + } + + model Post { + id String @id @default(uuid()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + const prisma = new PrismaClient(); + const db = enhance(prisma, { user: { id: 'user1' } }); + + // "author" and "authorId" are optional + db.post.create({ data: { title: 'abc' } }); +`, + }, + ], + } + ); + }); + + it('Default auth() safe unsafe mix', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id + posts Post[] + + @@allow('all', true) + } + + model Post { + id String @id @default(uuid()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + stats Stats @relation(fields: [statsId], references: [id]) + statsId String @unique + + @@allow('all', true) + } + + model Stats { + id String @id @default(uuid()) + viewCount Int @default(0) + post Post? + + @@allow('all', true) + + } + ` + ); + + const db = enhance({ id: 'userId-1' }); + await db.user.create({ data: { id: 'userId-1' } }); + + // safe + await db.stats.create({ data: { id: 'stats-1', viewCount: 10 } }); + await expect(db.post.create({ data: { title: 'title', statsId: 'stats-1' } })).toResolveTruthy(); + + // unsafe + await db.stats.create({ data: { id: 'stats-2', viewCount: 10 } }); + await expect( + db.post.create({ data: { title: 'title', stats: { connect: { id: 'stats-2' } } } }) + ).toResolveTruthy(); + }); +}); + +describe('auth() compile-time test', () => { + it('default enhanced typing', async () => { + await loadSchema( + ` + model User { + id1 Int + id2 Int + age Int + + @@id([id1, id2]) + @@allow('all', true) + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { enhance } from ".zenstack/enhance"; + import { PrismaClient } from '@prisma/client'; + enhance(new PrismaClient(), { user: { id1: 1, id2: 2 } }); + `, + }, + ], + } + ); + }); + + it('custom auth model', async () => { + await loadSchema( + ` + model Foo { + id Int @id + age Int + + @@auth + @@allow('all', true) + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { enhance } from ".zenstack/enhance"; + import { PrismaClient } from '@prisma/client'; + enhance(new PrismaClient(), { user: { id: 1 } }); + `, + }, + ], + } + ); + }); + + it('auth() selection', async () => { + await loadSchema( + ` + model User { + id Int @id + age Int + email String + + @@allow('all', auth().age > 0) + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { enhance } from ".zenstack/enhance"; + import { PrismaClient } from '@prisma/client'; + enhance(new PrismaClient(), { user: { id: 1, age: 10 } }); + `, + }, + ], + } + ); + }); + + it('auth() to-one relation selection', async () => { + await loadSchema( + ` + model User { + id Int @id + email String + profile Profile? + + @@allow('all', auth().profile.age > 0 && auth().profile.job.level > 0) + } + + model Profile { + id Int @id + job Job? + age Int + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } + + model Job { + id Int @id + level Int + profile Profile @relation(fields: [profileId], references: [id]) + profileId Int @unique + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { enhance } from ".zenstack/enhance"; + import { PrismaClient } from '@prisma/client'; + enhance(new PrismaClient(), { user: { id: 1, profile: { age: 1, job: { level: 10 } } } }); + `, + }, + ], + } + ); + }); + + it('auth() to-many relation selection', async () => { + await loadSchema( + ` + model User { + id Int @id + email String + posts Post[] + + @@allow('all', auth().posts?[viewCount > 0] && auth().posts?[comments?[level > 0]]) + } + + model Post { + id Int @id + viewCount Int + comments Comment[] + user User @relation(fields: [userId], references: [id]) + userId Int + } + + model Comment { + id Int @id + level Int + post Post @relation(fields: [postId], references: [id]) + postId Int + } + `, + { + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { enhance } from ".zenstack/enhance"; + import { PrismaClient } from '@prisma/client'; + enhance(new PrismaClient(), { user: { id: 1, posts: [ { viewCount: 1, comments: [ { level: 1 } ] } ] } }); + `, + }, + ], + } + ); + }); }); diff --git a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts index cadb42767..13f05aa51 100644 --- a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts +++ b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts @@ -1,4 +1,3 @@ -import { enhance } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; @@ -14,7 +13,7 @@ describe('With Policy: client extensions', () => { }); it('all model new method', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, enhanceRaw, prismaModule } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -29,13 +28,13 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { value: 1 } }); await prisma.model.create({ data: { value: 2 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-getAll', model: { $allModels: { async getAll(this: T, args?: any) { - const context = Prisma.getExtensionContext(this); + const context = prismaModule.getExtensionContext(this); const r = await (context as any).findMany(args); console.log('getAll result:', r); return r; @@ -46,7 +45,7 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); await expect(db.model.getAll()).resolves.toHaveLength(2); // FIXME: extending an enhanced client doesn't work for this case @@ -55,7 +54,7 @@ describe('With Policy: client extensions', () => { }); it('one model new method', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, enhanceRaw, prismaModule } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -70,13 +69,13 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { value: 1 } }); await prisma.model.create({ data: { value: 2 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-getAll', model: { model: { async getAll(this: T, args?: any) { - const context = Prisma.getExtensionContext(this); + const context = prismaModule.getExtensionContext(this); const r = await (context as any).findMany(args); return r; }, @@ -86,12 +85,12 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); await expect(db.model.getAll()).resolves.toHaveLength(2); }); it('add client method', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -104,7 +103,7 @@ describe('With Policy: client extensions', () => { let logged = false; - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-log', client: { @@ -122,7 +121,7 @@ describe('With Policy: client extensions', () => { }); it('query override one model', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -138,7 +137,7 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { x: 1, y: 200 } }); await prisma.model.create({ data: { x: 2, y: 300 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-queryOverride', query: { @@ -154,12 +153,12 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); await expect(db.model.findMany()).resolves.toHaveLength(1); }); it('query override all models', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -175,7 +174,7 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { x: 1, y: 200 } }); await prisma.model.create({ data: { x: 2, y: 300 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-queryOverride', query: { @@ -192,12 +191,12 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); await expect(db.model.findMany()).resolves.toHaveLength(1); }); it('query override all operations', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -213,7 +212,7 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { x: 1, y: 200 } }); await prisma.model.create({ data: { x: 2, y: 300 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-queryOverride', query: { @@ -230,12 +229,12 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); await expect(db.model.findMany()).resolves.toHaveLength(1); }); it('query override everything', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -251,7 +250,7 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { x: 1, y: 200 } }); await prisma.model.create({ data: { x: 2, y: 300 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-queryOverride', query: { @@ -266,12 +265,12 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); await expect(db.model.findMany()).resolves.toHaveLength(1); }); it('result mutation', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -285,7 +284,7 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { value: 0 } }); await prisma.model.create({ data: { value: 1 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-resultMutation', query: { @@ -303,14 +302,14 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); const r = await db.model.findMany(); expect(r).toHaveLength(1); expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ value: 2 })])); }); it('result custom fields', async () => { - const { prisma, Prisma } = await loadSchema( + const { prisma, prismaModule, enhanceRaw } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -324,7 +323,7 @@ describe('With Policy: client extensions', () => { await prisma.model.create({ data: { value: 0 } }); await prisma.model.create({ data: { value: 1 } }); - const ext = Prisma.defineExtension((_prisma: any) => { + const ext = prismaModule.defineExtension((_prisma: any) => { return _prisma.$extends({ name: 'prisma-extension-resultNewFields', result: { @@ -341,7 +340,7 @@ describe('With Policy: client extensions', () => { }); const xprisma = prisma.$extends(ext); - const db = enhance(xprisma); + const db = enhanceRaw(xprisma); const r = await db.model.findMany(); expect(r).toHaveLength(1); expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ doubleValue: 2 })])); diff --git a/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts b/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts index 99ae6d626..7bc4a9ed9 100644 --- a/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts +++ b/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts @@ -47,9 +47,9 @@ describe('With Policy: connect-disconnect', () => { `; it('simple to-many', async () => { - const { withPolicy, prisma } = await loadSchema(modelToMany); + const { enhance, prisma } = await loadSchema(modelToMany); - const db = withPolicy(); + const db = enhance(); // m1-1 -> m2-1 await db.m2.create({ data: { id: 'm2-1', value: 1, deleted: false } }); @@ -164,9 +164,9 @@ describe('With Policy: connect-disconnect', () => { }); it('nested to-many', async () => { - const { withPolicy } = await loadSchema(modelToMany); + const { enhance } = await loadSchema(modelToMany); - const db = withPolicy(); + const db = enhance(); await db.m3.create({ data: { id: 'm3-1', value: 1, deleted: false } }); await expect( @@ -219,9 +219,9 @@ describe('With Policy: connect-disconnect', () => { `; it('to-one', async () => { - const { withPolicy, prisma } = await loadSchema(modelToOne); + const { enhance, prisma } = await loadSchema(modelToOne); - const db = withPolicy(); + const db = enhance(); await db.m2.create({ data: { id: 'm2-1', value: 1, deleted: false } }); await db.m1.create({ @@ -314,9 +314,9 @@ describe('With Policy: connect-disconnect', () => { `; it('implicit many-to-many', async () => { - const { withPolicy, prisma } = await loadSchema(modelImplicitManyToMany); + const { enhance, prisma } = await loadSchema(modelImplicitManyToMany); - const db = withPolicy(); + const db = enhance(); // await prisma.m1.create({ data: { id: 'm1-1', value: 1 } }); // await prisma.m2.create({ data: { id: 'm2-1', value: 1 } }); @@ -379,9 +379,9 @@ describe('With Policy: connect-disconnect', () => { `; it('explicit many-to-many', async () => { - const { withPolicy, prisma } = await loadSchema(modelExplicitManyToMany); + const { enhance, prisma } = await loadSchema(modelExplicitManyToMany); - const db = withPolicy(); + const db = enhance(); await prisma.m1.create({ data: { id: 'm1-1', value: 1 } }); await prisma.m2.create({ data: { id: 'm2-1', value: 1 } }); diff --git a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts index d80c3c311..0a57bb6bb 100644 --- a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts +++ b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts @@ -71,7 +71,7 @@ describe('With Policy:deep nested', () => { beforeEach(async () => { const params = await loadSchema(model); - db = params.withPolicy(); + db = params.enhance(); prisma = params.prisma; }); diff --git a/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts b/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts index 4a1a4d0c5..ee0b61850 100644 --- a/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts +++ b/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts @@ -13,7 +13,7 @@ describe('With Policy:empty policy', () => { }); it('direct operations', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -22,7 +22,7 @@ describe('With Policy:empty policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await prisma.model.create({ data: { id: '1', value: 0 } }); await expect(db.model.create({ data: {} })).toBeRejectedByPolicy(); @@ -57,7 +57,7 @@ describe('With Policy:empty policy', () => { }); it('to-many write', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -74,7 +74,7 @@ describe('With Policy:empty policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ @@ -88,7 +88,7 @@ describe('With Policy:empty policy', () => { }); it('to-one write', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -105,7 +105,7 @@ describe('With Policy:empty policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ diff --git a/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts b/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts index 4f014d2f2..f130b2c94 100644 --- a/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts @@ -3,7 +3,7 @@ import path from 'path'; const DB_NAME = 'field-comparison'; -describe('WithPolicy: field comparison tests', () => { +describe('Policy: field comparison tests', () => { let origDir: string; let dbUrl: string; let prisma: any; @@ -41,7 +41,7 @@ describe('WithPolicy: field comparison tests', () => { ); prisma = r.prisma; - const db = r.withPolicy(); + const db = r.enhance(); await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); }); @@ -62,7 +62,7 @@ describe('WithPolicy: field comparison tests', () => { ); prisma = r.prisma; - const db = r.withPolicy(); + const db = r.enhance(); await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); }); @@ -83,7 +83,7 @@ describe('WithPolicy: field comparison tests', () => { ); prisma = r.prisma; - const db = r.withPolicy(); + const db = r.enhance(); await expect(db.model.create({ data: { x: 'a', y: ['b', 'c'] } })).toBeRejectedByPolicy(); await expect(db.model.create({ data: { x: 'a', y: ['a', 'c'] } })).toResolveTruthy(); }); @@ -104,7 +104,7 @@ describe('WithPolicy: field comparison tests', () => { ); prisma = r.prisma; - const db = r.withPolicy(); + const db = r.enhance(); await expect(db.model.create({ data: { x: 'a', y: ['b', 'c'] } })).toBeRejectedByPolicy(); await expect(db.model.create({ data: { x: 'a', y: ['a', 'c'] } })).toResolveTruthy(); }); diff --git a/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts b/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts index ee89c58e7..de778e8e8 100644 --- a/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts @@ -1,7 +1,7 @@ import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; -describe('With Policy: field-level policy', () => { +describe('Policy: field-level policy', () => { let origDir: string; beforeAll(async () => { @@ -13,7 +13,7 @@ describe('With Policy: field-level policy', () => { }); it('read simple', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -37,7 +37,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 1, admin: true } }); - const db = withPolicy(); + const db = enhance(); let r; // y is unreadable @@ -51,6 +51,18 @@ describe('With Policy: field-level policy', () => { r = await db.model.findUnique({ where: { id: 1 } }); expect(r.y).toBeUndefined(); + r = await db.user.findUnique({ where: { id: 1 }, select: { models: true } }); + expect(r.models[0].y).toBeUndefined(); + + r = await db.user.findUnique({ where: { id: 1 }, select: { models: { select: { y: true } } } }); + expect(r.models[0].y).toBeUndefined(); + + r = await db.user.findUnique({ where: { id: 1 } }).models(); + expect(r[0].y).toBeUndefined(); + + r = await db.user.findUnique({ where: { id: 1 } }).models({ select: { y: true } }); + expect(r[0].y).toBeUndefined(); + r = await db.model.findUnique({ select: { x: true }, where: { id: 1 } }); expect(r.x).toEqual(0); expect(r.y).toBeUndefined(); @@ -82,6 +94,21 @@ describe('With Policy: field-level policy', () => { r = await db.model.findUnique({ where: { id: 2 } }); expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); + r = await db.user.findUnique({ where: { id: 1 }, select: { models: { where: { id: 2 } } } }); + expect(r.models[0]).toEqual(expect.objectContaining({ x: 1, y: 0 })); + + r = await db.user.findUnique({ + where: { id: 1 }, + select: { models: { where: { id: 2 }, select: { y: true } } }, + }); + expect(r.models[0]).toEqual(expect.objectContaining({ y: 0 })); + + r = await db.user.findUnique({ where: { id: 1 } }).models({ where: { id: 2 } }); + expect(r[0]).toEqual(expect.objectContaining({ x: 1, y: 0 })); + + r = await db.user.findUnique({ where: { id: 1 } }).models({ where: { id: 2 }, select: { y: true } }); + expect(r[0]).toEqual(expect.objectContaining({ y: 0 })); + r = await db.model.findUnique({ select: { x: true }, where: { id: 2 } }); expect(r.x).toEqual(1); expect(r.y).toBeUndefined(); @@ -103,7 +130,7 @@ describe('With Policy: field-level policy', () => { }); it('read override', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -128,7 +155,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 1, admin: true } }); - const db = withPolicy(); + const db = enhance(); // created but can't read back await expect( @@ -181,7 +208,7 @@ describe('With Policy: field-level policy', () => { }); it('read filter with auth', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -205,7 +232,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 1, admin: true } }); - let db = withPolicy({ id: 1, admin: false }); + let db = enhance({ id: 1, admin: false }); let r; // y is unreadable @@ -246,7 +273,7 @@ describe('With Policy: field-level policy', () => { expect(r.y).toBeUndefined(); // y is readable - db = withPolicy({ id: 1, admin: true }); + db = enhance({ id: 1, admin: true }); r = await db.model.create({ data: { id: 2, @@ -281,7 +308,7 @@ describe('With Policy: field-level policy', () => { }); it('read filter with relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -306,7 +333,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 1, admin: false } }); await prisma.user.create({ data: { id: 2, admin: true } }); - const db = withPolicy(); + const db = enhance(); let r; // y is unreadable @@ -381,7 +408,7 @@ describe('With Policy: field-level policy', () => { }); it('read coverage', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id Int @id @default(autoincrement()) @@ -393,7 +420,7 @@ describe('With Policy: field-level policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); let r; // y is unreadable @@ -430,7 +457,7 @@ describe('With Policy: field-level policy', () => { }); it('read relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -472,7 +499,7 @@ describe('With Policy: field-level policy', () => { }, }); - const db = withPolicy(); + const db = enhance(); // read to-many relation let r = await db.user.findUnique({ @@ -498,7 +525,7 @@ describe('With Policy: field-level policy', () => { }); it('update simple', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -523,7 +550,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 1 }, }); - const db = withPolicy(); + const db = enhance(); await db.model.create({ data: { id: 1, x: 0, y: 0, ownerId: 1 }, @@ -569,7 +596,7 @@ describe('With Policy: field-level policy', () => { }); it('update with override', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id Int @id @default(autoincrement()) @@ -583,7 +610,7 @@ describe('With Policy: field-level policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.model.create({ data: { id: 1, x: 0, y: 0, z: 0 }, @@ -648,7 +675,7 @@ describe('With Policy: field-level policy', () => { }); it('update filter with relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -676,7 +703,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 2, admin: true }, }); - const db = withPolicy(); + const db = enhance(); await db.model.create({ data: { id: 1, x: 0, y: 0, ownerId: 1 }, @@ -706,7 +733,7 @@ describe('With Policy: field-level policy', () => { }); it('update with nested to-many relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -734,7 +761,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 2, admin: true, models: { create: { id: 2, x: 0, y: 0 } } }, }); - const db = withPolicy(); + const db = enhance(); await expect( db.user.update({ @@ -758,7 +785,7 @@ describe('With Policy: field-level policy', () => { }); it('update with nested to-one relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -786,7 +813,7 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 2, admin: true, model: { create: { id: 2, x: 0, y: 0 } } }, }); - const db = withPolicy(); + const db = enhance(); await expect( db.user.update({ @@ -828,7 +855,7 @@ describe('With Policy: field-level policy', () => { }); it('update with connect to-many relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -854,7 +881,7 @@ describe('With Policy: field-level policy', () => { await prisma.model.create({ data: { id: 1, value: 0 } }); await prisma.model.create({ data: { id: 2, value: 1 } }); - const db = withPolicy(); + const db = enhance(); await expect( db.model.update({ @@ -922,7 +949,7 @@ describe('With Policy: field-level policy', () => { }); it('update with connect to-one relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -948,7 +975,7 @@ describe('With Policy: field-level policy', () => { await prisma.model.create({ data: { id: 1, value: 0 } }); await prisma.model.create({ data: { id: 2, value: 1 } }); - const db = withPolicy(); + const db = enhance(); await expect( db.model.update({ @@ -1010,7 +1037,7 @@ describe('With Policy: field-level policy', () => { }); it('updateMany simple', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -1042,7 +1069,7 @@ describe('With Policy: field-level policy', () => { }, }, }); - const db = withPolicy(); + const db = enhance(); await expect(db.model.updateMany({ data: { y: 2 } })).resolves.toEqual({ count: 1 }); await expect(db.model.findUnique({ where: { id: 1 } })).resolves.toEqual( @@ -1054,7 +1081,7 @@ describe('With Policy: field-level policy', () => { }); it('updateMany override', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id Int @id @default(autoincrement()) @@ -1067,7 +1094,7 @@ describe('With Policy: field-level policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.model.create({ data: { id: 1, x: 0, y: 0 } }); await db.model.create({ data: { id: 2, x: 1, y: 0 } }); @@ -1084,7 +1111,7 @@ describe('With Policy: field-level policy', () => { }); it('updateMany nested', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -1116,7 +1143,7 @@ describe('With Policy: field-level policy', () => { }, }, }); - const db = withPolicy(); + const db = enhance(); await expect( db.user.update({ where: { id: 1 }, data: { models: { updateMany: { data: { y: 2 } } } } }) @@ -1144,7 +1171,7 @@ describe('With Policy: field-level policy', () => { }); it('this expression', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @@ -1157,24 +1184,24 @@ describe('With Policy: field-level policy', () => { await prisma.user.create({ data: { id: 1, username: 'test' } }); // admin - let r = await withPolicy({ id: 1, admin: true }).user.findFirst(); + let r = await enhance({ id: 1, admin: true }).user.findFirst(); expect(r.username).toEqual('test'); // owner - r = await withPolicy({ id: 1 }).user.findFirst(); + r = await enhance({ id: 1 }).user.findFirst(); expect(r.username).toEqual('test'); // anonymous - r = await withPolicy().user.findFirst(); + r = await enhance().user.findFirst(); expect(r.username).toBeUndefined(); // non-owner - r = await withPolicy({ id: 2 }).user.findFirst(); + r = await enhance({ id: 2 }).user.findFirst(); expect(r.username).toBeUndefined(); }); it('collection predicate', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -1206,7 +1233,7 @@ describe('With Policy: field-level policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await prisma.user.create({ data: { @@ -1269,7 +1296,7 @@ describe('With Policy: field-level policy', () => { }); it('deny only without field access', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -1285,14 +1312,14 @@ describe('With Policy: field-level policy', () => { }); await expect( - withPolicy({ id: 1, role: 'ADMIN' }).user.update({ + enhance({ id: 1, role: 'ADMIN' }).user.update({ where: { id: user.id }, data: { role: 'ADMIN' }, }) ).toResolveTruthy(); await expect( - withPolicy({ id: 1, role: 'USER' }).user.update({ + enhance({ id: 1, role: 'USER' }).user.update({ where: { id: user.id }, data: { role: 'ADMIN' }, }) @@ -1300,7 +1327,7 @@ describe('With Policy: field-level policy', () => { }); it('deny only with field access', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -1317,14 +1344,14 @@ describe('With Policy: field-level policy', () => { }); await expect( - withPolicy({ id: 1, role: 'ADMIN' }).user.update({ + enhance({ id: 1, role: 'ADMIN' }).user.update({ where: { id: user1.id }, data: { role: 'ADMIN' }, }) ).toResolveTruthy(); await expect( - withPolicy({ id: 1, role: 'USER' }).user.update({ + enhance({ id: 1, role: 'USER' }).user.update({ where: { id: user1.id }, data: { role: 'ADMIN' }, }) @@ -1335,7 +1362,7 @@ describe('With Policy: field-level policy', () => { }); await expect( - withPolicy({ id: 1, role: 'ADMIN' }).user.update({ + enhance({ id: 1, role: 'ADMIN' }).user.update({ where: { id: user2.id }, data: { role: 'ADMIN' }, }) diff --git a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts index d54913bb3..e4cf21825 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -1,11 +1,11 @@ import { CrudFailureReason, isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; import { FullDbClientContract, createPostgresDb, dropPostgresDb, loadSchema, run } from '@zenstackhq/testtools'; -describe('With Policy: field validation', () => { +describe('Field validation', () => { let db: FullDbClientContract; beforeAll(async () => { - const { withPolicy, prisma: _prisma } = await loadSchema( + const { enhance, prisma: _prisma } = await loadSchema( ` model User { id String @id @default(cuid()) @@ -37,8 +37,6 @@ describe('With Policy: field validation', () => { text5 String? @endsWith('xyz') text6 String? @trim @lower text7 String? @upper - - @@allow('all', true) } model Task { @@ -46,12 +44,11 @@ describe('With Policy: field validation', () => { user User @relation(fields: [userId], references: [id]) userId String slug String @regex("^[0-9a-zA-Z]{4,16}$") @lower - - @@allow('all', true) } -` +`, + { enhancements: ['validation'] } ); - db = withPolicy(); + db = enhance(); }); beforeEach(() => { @@ -610,9 +607,10 @@ describe('With Policy: field validation', () => { }); }); -describe('With Policy: model-level validation', () => { +describe('Model-level validation', () => { it('create', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int @@ -620,9 +618,10 @@ describe('With Policy: model-level validation', () => { @@validate(x > 0) @@validate(x >= y) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -632,16 +631,18 @@ describe('With Policy: model-level validation', () => { }); it('update', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int y Int @@validate(x >= y) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -650,15 +651,17 @@ describe('With Policy: model-level validation', () => { }); it('int optionality', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int? @@validate(x > 0) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -668,15 +671,17 @@ describe('With Policy: model-level validation', () => { }); it('boolean optionality', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Boolean? @@validate(x) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -686,16 +691,18 @@ describe('With Policy: model-level validation', () => { }); it('optionality with binary', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int? y Int? @@validate(x > y) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -706,15 +713,17 @@ describe('With Policy: model-level validation', () => { }); it('optionality with in operator lhs', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x String? @@validate(x in ['foo', 'bar']) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -734,12 +743,12 @@ describe('With Policy: model-level validation', () => { x String[] @@validate('foo' in x) - @@allow('all', true) } `, { provider: 'postgresql', dbUrl, + enhancements: ['validation'], } ); @@ -756,16 +765,18 @@ describe('With Policy: model-level validation', () => { }); it('optionality with complex expression', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int? y Int? @@validate(y > 1 && x > y) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -777,15 +788,17 @@ describe('With Policy: model-level validation', () => { }); it('optionality with negation', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Boolean? @@validate(!x) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -795,16 +808,18 @@ describe('With Policy: model-level validation', () => { }); it('update implied optionality', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int y Int @@validate(x > y) - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -814,7 +829,8 @@ describe('With Policy: model-level validation', () => { }); it('optionality with scalar functions', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) s String @@ -832,10 +848,10 @@ describe('With Policy: model-level validation', () => { @@validate(email(e), 'invalid e') @@validate(url(u), 'invalid u') @@validate(datetime(d), 'invalid d') - - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -887,13 +903,12 @@ describe('With Policy: model-level validation', () => { hasSome(x, ['x', 'y']) && (y == null || !isEmpty(y)) ) - - @@allow('all', true) } `, { provider: 'postgresql', dbUrl, + enhancements: ['validation'], } ); @@ -912,7 +927,8 @@ describe('With Policy: model-level validation', () => { }); it('null comparison', async () => { - const { enhance } = await loadSchema(` + const { enhance } = await loadSchema( + ` model Model { id Int @id @default(autoincrement()) x Int @@ -920,10 +936,10 @@ describe('With Policy: model-level validation', () => { @@validate(x == null || !(x <= 0)) @@validate(y != null && !(y > 1)) - - @@allow('all', true) } - `); + `, + { enhancements: ['validation'] } + ); const db = enhance(); @@ -938,3 +954,48 @@ describe('With Policy: model-level validation', () => { await expect(db.model.update({ where: { id: 1 }, data: { x: 2, y: 1 } })).toResolveTruthy(); }); }); + +describe('Policy and validation interaction', () => { + it('test', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id @default(cuid()) + email String? @email + age Int + + @@allow('all', age > 0) + } + ` + ); + + const db = enhance(); + + await expect( + db.user.create({ + data: { + email: 'hello', + age: 18, + }, + }) + ).toBeRejectedByPolicy(['Invalid email at "email"']); + + await expect( + db.user.create({ + data: { + email: 'user@abc.com', + age: 0, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.user.create({ + data: { + email: 'user@abc.com', + age: 18, + }, + }) + ).toResolveTruthy(); + }); +}); diff --git a/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts b/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts index 264c5da28..9dd247d65 100644 --- a/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts +++ b/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts @@ -12,33 +12,181 @@ describe('With Policy: fluent API', () => { process.chdir(origDir); }); - it('fluent api', async () => { - const { withPolicy, prisma } = await loadSchema( + it('policy tests', async () => { + const { enhance, prisma } = await loadSchema( ` model User { id Int @id email String @unique + profile Profile? posts Post[] @@allow('all', true) } +model Profile { + id Int @id + age Int + user User @relation(fields: [userId], references: [id]) + userId Int @unique + @@allow('all', auth() == user) +} + model Post { id Int @id title String author User? @relation(fields: [authorId], references: [id]) authorId Int? published Boolean @default(false) - secret String @default("secret") @allow('read', published == false) + secret String @default("secret") @allow('read', published == false, true) + + @@allow('read', published) +}`, + { logPrismaQuery: true } + ); + + await prisma.user.create({ + data: { + id: 1, + email: 'a@test.com', + profile: { + create: { id: 1, age: 18 }, + }, + posts: { + create: [ + { id: 1, title: 'post1', published: true }, + { id: 2, title: 'post2', published: true }, + { id: 3, title: 'post3', published: false }, + ], + }, + }, + }); + + await prisma.user.create({ + data: { + id: 2, + email: 'b@test.com', + posts: { + create: [{ id: 4, title: 'post4' }], + }, + }, + }); + + const db1 = enhance({ id: 1 }); + const db2 = enhance({ id: 2 }); + + // check policies + await expect(db1.user.findUnique({ where: { id: 1 } }).posts()).resolves.toHaveLength(2); + await expect(db2.user.findUnique({ where: { id: 2 } }).posts()).resolves.toHaveLength(0); + await expect( + db1.user.findUnique({ where: { id: 1 } }).posts({ where: { published: true } }) + ).resolves.toHaveLength(2); + await expect(db1.user.findUnique({ where: { id: 1 } }).posts({ take: 1 })).resolves.toHaveLength(1); + + // field-level policies + let p = ( + await db1.user + .findUnique({ where: { id: 1 } }) + .posts({ where: { published: true }, select: { secret: true } }) + )[0]; + expect(p.secret).toBeUndefined(); + p = ( + await db1.user + .findUnique({ where: { id: 1 } }) + .posts({ where: { published: false }, select: { secret: true } }) + )[0]; + expect(p.secret).toBeTruthy(); + + // to-one optional + await expect(db1.post.findFirst({ where: { id: 1 } }).author()).resolves.toMatchObject({ + id: 1, + email: 'a@test.com', + }); + await expect(db1.post.findFirst({ where: { id: 1 } }).author({ where: { id: 1 } })).resolves.toMatchObject({ + id: 1, + email: 'a@test.com', + }); + await expect(db1.post.findFirst({ where: { id: 1 } }).author({ where: { id: 2 } })).toResolveNull(); + + // to-one required + await expect(db1.profile.findUnique({ where: { userId: 1 } }).user()).resolves.toMatchObject({ + id: 1, + email: 'a@test.com', + }); + // not found + await expect(db1.profile.findUnique({ where: { userId: 2 } }).user()).toResolveNull(); + // not readable + await expect(db2.profile.findUnique({ where: { userId: 1 } }).user()).toResolveNull(); + + // unresolved promise + db1.user.findUniqueOrThrow({ where: { id: 5 } }); + db1.user.findUniqueOrThrow({ where: { id: 5 } }).posts(); + + // not-found + await expect(db1.user.findUniqueOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); + await expect(db1.user.findFirstOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); + await expect(db1.post.findUniqueOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); + await expect(db1.post.findFirstOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); + + // chaining + await expect( + db1.post + .findFirst({ where: { id: 1 } }) + .author() + .posts() + ).resolves.toHaveLength(2); + await expect( + db1.post + .findFirst({ where: { id: 1 } }) + .author() + .posts({ where: { published: true } }) + ).resolves.toHaveLength(2); + + // chaining broken + expect((db1.post.findMany() as any).author).toBeUndefined(); + expect( + db1.post + .findFirst({ where: { id: 1 } }) + .author() + .posts().author + ).toBeUndefined(); + }); + + it('non-policy tests', async () => { + const { enhance, prisma } = await loadSchema( + ` +model User { + id Int @id + email String @unique + password String? @omit + profile Profile? + posts Post[] +} + +model Profile { + id Int @id + age Int + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} - @@allow('all', author == auth()) -}` +model Post { + id Int @id + title String + author User? @relation(fields: [authorId], references: [id]) + authorId Int? + published Boolean @default(false) +}`, + { enhancements: ['omit'] } ); await prisma.user.create({ data: { id: 1, email: 'a@test.com', + profile: { + create: { id: 1, age: 18 }, + }, posts: { create: [ { id: 1, title: 'post1', published: true }, @@ -58,7 +206,7 @@ model Post { }, }); - const db = withPolicy({ id: 1 }); + const db = enhance(); // check policies await expect(db.user.findUnique({ where: { id: 1 } }).posts()).resolves.toHaveLength(2); @@ -67,16 +215,24 @@ model Post { ).resolves.toHaveLength(1); await expect(db.user.findUnique({ where: { id: 1 } }).posts({ take: 1 })).resolves.toHaveLength(1); - // field-level policies - let p = (await db.user.findUnique({ where: { id: 1 } }).posts({ where: { published: true } }))[0]; - expect(p.secret).toBeUndefined(); - p = (await db.user.findUnique({ where: { id: 1 } }).posts({ where: { published: false } }))[0]; - expect(p.secret).toBeTruthy(); + // to-one optional + await expect(db.post.findFirst({ where: { id: 1 } }).author()).resolves.toMatchObject({ + id: 1, + email: 'a@test.com', + }); + await expect(db.post.findFirst({ where: { id: 1 } }).author({ where: { id: 1 } })).resolves.toMatchObject({ + id: 1, + email: 'a@test.com', + }); + await expect(db.post.findFirst({ where: { id: 1 } }).author({ where: { id: 2 } })).toResolveNull(); - // to-one - await expect(db.post.findFirst({ where: { id: 1 } }).author()).resolves.toEqual( - expect.objectContaining({ id: 1, email: 'a@test.com' }) - ); + // to-one required + await expect(db.profile.findUnique({ where: { userId: 1 } }).user()).resolves.toMatchObject({ + id: 1, + email: 'a@test.com', + }); + // not found + await expect(db.profile.findUnique({ where: { userId: 2 } }).user()).toResolveNull(); // not-found await expect(db.user.findUniqueOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); @@ -91,6 +247,12 @@ model Post { .author() .posts() ).resolves.toHaveLength(2); + await expect( + db.post + .findFirst({ where: { id: 1 } }) + .author() + .posts({ where: { published: true } }) + ).resolves.toHaveLength(1); // chaining broken expect((db.post.findMany() as any).author).toBeUndefined(); diff --git a/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts b/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts index 3dcc07850..f0eeb1a8a 100644 --- a/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts +++ b/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts @@ -13,7 +13,7 @@ describe('With Policy: multi-field unique', () => { }); it('toplevel crud test unnamed constraint', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -28,7 +28,7 @@ describe('With Policy: multi-field unique', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 2 } })).toBeRejectedWithCode('P2002'); @@ -43,7 +43,7 @@ describe('With Policy: multi-field unique', () => { }); it('toplevel crud test named constraint', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -58,7 +58,7 @@ describe('With Policy: multi-field unique', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); @@ -73,7 +73,7 @@ describe('With Policy: multi-field unique', () => { }); it('nested crud test', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -95,7 +95,7 @@ describe('With Policy: multi-field unique', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.m1.create({ data: { id: '1', m2: { create: { a: 'a1', b: 'b1', x: 1 } } } })).toResolveTruthy(); await expect( diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts index 664f3256f..01d2c36e2 100644 --- a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts +++ b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts @@ -13,7 +13,7 @@ describe('With Policy:nested to-many', () => { }); it('read filtering', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -34,7 +34,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); let read = await db.m1.create({ include: { m2: true }, @@ -62,7 +62,7 @@ describe('With Policy:nested to-many', () => { }); it('read condition hoisting', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -108,7 +108,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ include: { m2: true }, @@ -144,7 +144,7 @@ describe('With Policy:nested to-many', () => { }); it('create simple', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -165,7 +165,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); // single create denied await expect( @@ -211,7 +211,7 @@ describe('With Policy:nested to-many', () => { }); it('update simple', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -233,7 +233,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -285,7 +285,7 @@ describe('With Policy:nested to-many', () => { }); it('update id field', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -307,7 +307,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -380,7 +380,7 @@ describe('With Policy:nested to-many', () => { }); it('update with create from one to many', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -402,7 +402,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -437,7 +437,7 @@ describe('With Policy:nested to-many', () => { }); it('update with create from many to one', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -459,7 +459,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m2.create({ data: { id: '1' } }); @@ -487,7 +487,7 @@ describe('With Policy:nested to-many', () => { }); it('update with delete', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -510,7 +510,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -591,7 +591,7 @@ describe('With Policy:nested to-many', () => { }); it('create with nested read', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -625,7 +625,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ @@ -684,7 +684,7 @@ describe('With Policy:nested to-many', () => { }); it('update with nested read', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -716,7 +716,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { id: '1', diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts index c510e6bb5..e215a917b 100644 --- a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts +++ b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts @@ -13,7 +13,7 @@ describe('With Policy:nested to-one', () => { }); it('read filtering for optional relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -34,7 +34,7 @@ describe('With Policy:nested to-one', () => { ` ); - const db = withPolicy(); + const db = enhance(); let read = await db.m1.create({ include: { m2: true }, @@ -60,7 +60,7 @@ describe('With Policy:nested to-one', () => { }); it('read rejection for non-optional relation', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -91,7 +91,7 @@ describe('With Policy:nested to-one', () => { }, }); - const db = withPolicy(); + const db = enhance(); await expect(db.m2.findUnique({ where: { id: '1' }, include: { m1: true } })).toResolveFalsy(); await expect(db.m2.findMany({ include: { m1: true } })).resolves.toHaveLength(0); @@ -100,7 +100,7 @@ describe('With Policy:nested to-one', () => { }); it('read condition hoisting', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -134,7 +134,7 @@ describe('With Policy:nested to-one', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ include: { m2: true }, @@ -153,7 +153,7 @@ describe('With Policy:nested to-one', () => { }); it('create and update tests', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -175,7 +175,7 @@ describe('With Policy:nested to-one', () => { ` ); - const db = withPolicy(); + const db = enhance(); // create denied await expect( @@ -213,7 +213,7 @@ describe('With Policy:nested to-one', () => { }); it('nested update id tests', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -235,7 +235,7 @@ describe('With Policy:nested to-one', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -271,7 +271,7 @@ describe('With Policy:nested to-one', () => { }); it('nested create', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -294,7 +294,7 @@ describe('With Policy:nested to-one', () => { { logPrismaQuery: true } ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -327,7 +327,7 @@ describe('With Policy:nested to-one', () => { }); it('nested delete', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -350,7 +350,7 @@ describe('With Policy:nested to-one', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -393,7 +393,7 @@ describe('With Policy:nested to-one', () => { }); it('nested relation delete', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -414,7 +414,7 @@ describe('With Policy:nested to-one', () => { ` ); - await withPolicy({ id: 'user1' }).m1.create({ + await enhance({ id: 'user1' }).m1.create({ data: { id: 'm1', value: 1, @@ -422,7 +422,7 @@ describe('With Policy:nested to-one', () => { }); await expect( - withPolicy({ id: 'user2' }).user.create({ + enhance({ id: 'user2' }).user.create({ data: { id: 'user2', m1: { @@ -433,7 +433,7 @@ describe('With Policy:nested to-one', () => { ).toResolveTruthy(); await expect( - withPolicy({ id: 'user2' }).user.update({ + enhance({ id: 'user2' }).user.update({ where: { id: 'user2' }, data: { m1: { delete: true }, @@ -442,7 +442,7 @@ describe('With Policy:nested to-one', () => { ).toBeRejectedByPolicy(); await expect( - withPolicy({ id: 'user1' }).user.create({ + enhance({ id: 'user1' }).user.create({ data: { id: 'user1', m1: { @@ -453,7 +453,7 @@ describe('With Policy:nested to-one', () => { ).toResolveTruthy(); await expect( - withPolicy({ id: 'user1' }).user.update({ + enhance({ id: 'user1' }).user.update({ where: { id: 'user1' }, data: { m1: { delete: true }, diff --git a/tests/integration/tests/enhancements/with-policy/options.test.ts b/tests/integration/tests/enhancements/with-policy/options.test.ts index 9c82e4305..3f63f54a3 100644 --- a/tests/integration/tests/enhancements/with-policy/options.test.ts +++ b/tests/integration/tests/enhancements/with-policy/options.test.ts @@ -1,20 +1,9 @@ -import { enhance } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; describe('Password test', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - it('load path', async () => { - const { prisma } = await loadSchema( + const { prisma, projectDir } = await loadSchema( ` model Foo { id String @id @default(cuid()) @@ -26,32 +15,8 @@ describe('Password test', () => { { getPrismaOnly: true, output: './zen' } ); - const db = enhance(prisma, undefined, { loadPath: './zen' }); - await expect( - db.foo.create({ - data: { x: 0 }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ - data: { x: 1 }, - }) - ).toResolveTruthy(); - }); - - it('prisma module', async () => { - const { prisma, Prisma, modelMeta, policy } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - x Int - - @@allow('read', true) - @@allow('create', x > 0) - }` - ); - - const db = enhance(prisma, undefined, { modelMeta, policy, prismaModule: Prisma }); + const enhance = require(path.join(projectDir, 'zen/enhance')).enhance; + const db = enhance(prisma); await expect( db.foo.create({ data: { x: 0 }, @@ -65,7 +30,7 @@ describe('Password test', () => { }); it('overrides', async () => { - const { prisma } = await loadSchema( + const { prisma, projectDir } = await loadSchema( ` model Foo { id String @id @default(cuid()) @@ -76,9 +41,10 @@ describe('Password test', () => { { getPrismaOnly: true, output: './zen' } ); - const db = enhance(prisma, undefined, { - modelMeta: require(path.resolve('./zen/model-meta')).default, - policy: require(path.resolve('./zen/policy')).default, + const enhance = require(path.join(projectDir, 'zen/enhance')).enhance; + const db = enhance(prisma, { + modelMeta: require(path.join(projectDir, 'zen/model-meta')).default, + policy: require(path.resolve(projectDir, 'zen/policy')).default, }); await expect( db.foo.create({ diff --git a/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts b/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts index 9c251faf5..691c6176a 100644 --- a/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts +++ b/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts @@ -7,11 +7,11 @@ describe('Pet Store Policy Tests', () => { let prisma: FullDbClientContract; beforeAll(async () => { - const { withPolicy, prisma: _prisma } = await loadSchemaFromFile( + const { enhance, prisma: _prisma } = await loadSchemaFromFile( path.join(__dirname, '../../schema/petstore.zmodel'), { addPrelude: false } ); - getDb = withPolicy; + getDb = enhance; prisma = _prisma; }); diff --git a/tests/integration/tests/enhancements/with-policy/post-update.test.ts b/tests/integration/tests/enhancements/with-policy/post-update.test.ts index c40d338a3..e2d7e0156 100644 --- a/tests/integration/tests/enhancements/with-policy/post-update.test.ts +++ b/tests/integration/tests/enhancements/with-policy/post-update.test.ts @@ -13,7 +13,7 @@ describe('With Policy: post update', () => { }); it('simple allow', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -25,7 +25,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.model.create({ data: { id: '1', value: 0 } })).toResolveTruthy(); await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); @@ -33,7 +33,7 @@ describe('With Policy: post update', () => { }); it('simple deny', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -45,7 +45,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.model.create({ data: { id: '1', value: 0 } })).toResolveTruthy(); await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); @@ -53,7 +53,7 @@ describe('With Policy: post update', () => { }); it('mixed pre and post', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -65,7 +65,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.model.create({ data: { id: '1', value: 0 } })).toResolveTruthy(); await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); @@ -76,7 +76,7 @@ describe('With Policy: post update', () => { }); it('functions pre-update', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -89,7 +89,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await prisma.model.create({ data: { id: '1', value: 'good', x: 1 } }); await expect(db.model.update({ where: { id: '1' }, data: { value: 'hello' } })).toBeRejectedByPolicy(); @@ -100,7 +100,7 @@ describe('With Policy: post update', () => { }); it('functions post-update', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -114,7 +114,7 @@ describe('With Policy: post update', () => { { logPrismaQuery: true } ); - const db = withPolicy(); + const db = enhance(); await prisma.model.create({ data: { id: '1', value: 'good', x: 1 } }); await expect(db.model.update({ where: { id: '1' }, data: { value: 'nice' } })).toBeRejectedByPolicy(); @@ -124,7 +124,7 @@ describe('With Policy: post update', () => { }); it('collection predicate pre-update', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -145,7 +145,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await prisma.m1.create({ data: { @@ -181,7 +181,7 @@ describe('With Policy: post update', () => { }); it('collection predicate post-update', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -202,7 +202,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await prisma.m1.create({ data: { @@ -238,7 +238,7 @@ describe('With Policy: post update', () => { }); it('nested to-many', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -258,7 +258,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ @@ -297,7 +297,7 @@ describe('With Policy: post update', () => { }); it('nested to-one', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -317,7 +317,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ @@ -350,7 +350,7 @@ describe('With Policy: post update', () => { }); it('nested select', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -370,7 +370,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ @@ -401,7 +401,7 @@ describe('With Policy: post update', () => { }); it('deep nesting', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -432,7 +432,7 @@ describe('With Policy: post update', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ diff --git a/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts b/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts index 1654fba96..264119453 100644 --- a/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts +++ b/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts @@ -13,7 +13,7 @@ describe('With Policy: query reduction', () => { }); it('test query reduction', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -65,8 +65,8 @@ describe('With Policy: query reduction', () => { }, }); - const dbUser1 = withPolicy({ id: 1 }); - const dbUser2 = withPolicy({ id: 2 }); + const dbUser1 = enhance({ id: 1 }); + const dbUser2 = enhance({ id: 2 }); await expect( dbUser1.user.findMany({ diff --git a/tests/integration/tests/enhancements/with-policy/refactor.test.ts b/tests/integration/tests/enhancements/with-policy/refactor.test.ts index 6a329a739..3c725697d 100644 --- a/tests/integration/tests/enhancements/with-policy/refactor.test.ts +++ b/tests/integration/tests/enhancements/with-policy/refactor.test.ts @@ -21,7 +21,7 @@ describe('With Policy: refactor tests', () => { beforeEach(async () => { dbUrl = await createPostgresDb(DB_NAME); - const { prisma: _prisma, withPolicy } = await loadSchemaFromFile( + const { prisma: _prisma, enhance } = await loadSchemaFromFile( path.join(__dirname, '../../schema/refactor-pg.zmodel'), { provider: 'postgresql', @@ -29,7 +29,7 @@ describe('With Policy: refactor tests', () => { logPrismaQuery: true, } ); - getDb = withPolicy; + getDb = enhance; prisma = _prisma; anonDb = getDb(); user1Db = getDb({ id: 1 }); diff --git a/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts b/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts index fe0c686db..e7ddb043e 100644 --- a/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts +++ b/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts @@ -35,9 +35,9 @@ describe('With Policy: relation many-to-many filter', () => { `; it('some filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -128,9 +128,9 @@ describe('With Policy: relation many-to-many filter', () => { }); it('none filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -211,9 +211,9 @@ describe('With Policy: relation many-to-many filter', () => { }); it('every filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { diff --git a/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts b/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts index 3737bbf4c..1a1c40406 100644 --- a/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts +++ b/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts @@ -45,9 +45,9 @@ describe('With Policy: relation one-to-many filter', () => { `; it('some filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ @@ -163,9 +163,9 @@ describe('With Policy: relation one-to-many filter', () => { }); it('none filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ @@ -281,9 +281,9 @@ describe('With Policy: relation one-to-many filter', () => { }); it('every filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ @@ -399,9 +399,9 @@ describe('With Policy: relation one-to-many filter', () => { }); it('_count filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ diff --git a/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts b/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts index 7c26bc854..d076e18e5 100644 --- a/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts +++ b/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts @@ -45,9 +45,9 @@ describe('With Policy: relation one-to-one filter', () => { `; it('is filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ @@ -152,9 +152,9 @@ describe('With Policy: relation one-to-one filter', () => { }); it('isNot filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ @@ -261,9 +261,9 @@ describe('With Policy: relation one-to-one filter', () => { }); it('direct object filter', async () => { - const { withPolicy } = await loadSchema(model); + const { enhance } = await loadSchema(model); - const db = withPolicy(); + const db = enhance(); // m1 with m2 and m3 await db.m1.create({ diff --git a/tests/integration/tests/enhancements/with-policy/self-relation.test.ts b/tests/integration/tests/enhancements/with-policy/self-relation.test.ts index dc7cb96ca..525d30043 100644 --- a/tests/integration/tests/enhancements/with-policy/self-relation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/self-relation.test.ts @@ -13,7 +13,7 @@ describe('With Policy: self relations', () => { }); it('one-to-one', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -28,7 +28,7 @@ describe('With Policy: self relations', () => { ` ); - const db = withPolicy(); + const db = enhance(); // create denied await expect( @@ -90,7 +90,7 @@ describe('With Policy: self relations', () => { }); it('one-to-many', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -105,7 +105,7 @@ describe('With Policy: self relations', () => { ` ); - const db = withPolicy(); + const db = enhance(); // create denied await expect( @@ -157,7 +157,7 @@ describe('With Policy: self relations', () => { }); it('many-to-many', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -171,7 +171,7 @@ describe('With Policy: self relations', () => { ` ); - const db = withPolicy(); + const db = enhance(); // create denied await expect( diff --git a/tests/integration/tests/enhancements/with-policy/subscription.test.ts b/tests/integration/tests/enhancements/with-policy/subscription.test.ts index 2befdd42a..a4dccf807 100644 --- a/tests/integration/tests/enhancements/with-policy/subscription.test.ts +++ b/tests/integration/tests/enhancements/with-policy/subscription.test.ts @@ -17,7 +17,7 @@ describe.skip('With Policy: subscription test', () => { }); it('subscribe auth check', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -42,11 +42,11 @@ describe.skip('With Policy: subscription test', () => { const rawSub = await prisma.model.subscribe(); - const anonDb = withPolicy(); + const anonDb = enhance(); console.log('Anonymous db subscribing'); const anonSub = await anonDb.model.subscribe(); - const authDb = withPolicy({ id: 1 }); + const authDb = enhance({ id: 1 }); console.log('Auth db subscribing'); const authSub = await authDb.model.subscribe(); @@ -75,7 +75,7 @@ describe.skip('With Policy: subscription test', () => { }); it('subscribe model check', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id Int @id @default(autoincrement()) @@ -96,7 +96,7 @@ describe.skip('With Policy: subscription test', () => { const rawSub = await prisma.model.subscribe(); - const enhanced = withPolicy(); + const enhanced = enhance(); console.log('Auth db subscribing'); const enhancedSub = await enhanced.model.subscribe(); @@ -130,7 +130,7 @@ describe.skip('With Policy: subscription test', () => { }); it('subscribe partial', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id Int @id @default(autoincrement()) @@ -151,7 +151,7 @@ describe.skip('With Policy: subscription test', () => { const rawSub = await prisma.model.subscribe({ create: {} }); - const enhanced = withPolicy(); + const enhanced = enhance(); console.log('Auth db subscribing'); const enhancedSub = await enhanced.model.subscribe({ create: {} }); @@ -185,7 +185,7 @@ describe.skip('With Policy: subscription test', () => { }); it('subscribe mixed model check', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model Model { id Int @id @default(autoincrement()) @@ -210,7 +210,7 @@ describe.skip('With Policy: subscription test', () => { delete: { before: { name: { contains: 'world' } } }, }); - const enhanced = withPolicy(); + const enhanced = enhance(); console.log('Auth db subscribing'); const enhancedSub = await enhanced.model.subscribe({ create: { after: { name: { contains: 'world' } } }, diff --git a/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts b/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts index 2b7dd416b..fe26dd561 100644 --- a/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts +++ b/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts @@ -7,11 +7,11 @@ describe('Todo Policy Tests', () => { let prisma: FullDbClientContract; beforeAll(async () => { - const { withPolicy, prisma: _prisma } = await loadSchemaFromFile( + const { enhance, prisma: _prisma } = await loadSchemaFromFile( path.join(__dirname, '../../schema/todo.zmodel'), { addPrelude: false } ); - getDb = withPolicy; + getDb = enhance; prisma = _prisma; }); diff --git a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts index 0ebf2a182..3543dd7b5 100644 --- a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts +++ b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts @@ -13,7 +13,7 @@ describe('With Policy: toplevel operations', () => { }); it('read tests', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -25,7 +25,7 @@ describe('With Policy: toplevel operations', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.model.create({ @@ -62,7 +62,7 @@ describe('With Policy: toplevel operations', () => { }); it('write tests', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -75,7 +75,7 @@ describe('With Policy: toplevel operations', () => { ` ); - const db = withPolicy(); + const db = enhance(); // create denied await expect( @@ -148,7 +148,7 @@ describe('With Policy: toplevel operations', () => { }); it('update id tests', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -161,7 +161,7 @@ describe('With Policy: toplevel operations', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.model.create({ data: { @@ -224,7 +224,7 @@ describe('With Policy: toplevel operations', () => { }); it('delete tests', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -237,7 +237,7 @@ describe('With Policy: toplevel operations', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.model.delete({ where: { id: '1' } })).toBeNotFound(); diff --git a/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts b/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts index e4d399204..a7ec74fa5 100644 --- a/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts +++ b/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts @@ -13,7 +13,7 @@ describe('With Policy: unique as id', () => { }); it('unique fields', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model A { x String @unique @@ -38,7 +38,7 @@ describe('With Policy: unique as id', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); @@ -64,7 +64,7 @@ describe('With Policy: unique as id', () => { }); it('unique fields mixed with id', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model A { id Int @id @default(autoincrement()) @@ -91,7 +91,7 @@ describe('With Policy: unique as id', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); @@ -117,7 +117,7 @@ describe('With Policy: unique as id', () => { }); it('model-level unique fields', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model A { x String @@ -147,7 +147,7 @@ describe('With Policy: unique as id', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); diff --git a/tests/integration/tests/enhancements/with-policy/view.test.ts b/tests/integration/tests/enhancements/with-policy/view.test.ts index f5abe6439..3c541d2b0 100644 --- a/tests/integration/tests/enhancements/with-policy/view.test.ts +++ b/tests/integration/tests/enhancements/with-policy/view.test.ts @@ -13,7 +13,7 @@ describe('View Policy Test', () => { }); it('view policy', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` datasource db { provider = "sqlite" @@ -91,7 +91,7 @@ describe('View Policy Test', () => { }, }); - const db = withPolicy(); + const db = enhance(); await expect(prisma.userInfo.findMany()).resolves.toHaveLength(2); await expect(db.userInfo.findMany()).resolves.toHaveLength(1); diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json index 1461849f1..96500688a 100644 --- a/tests/integration/tests/frameworks/nextjs/test-project/package.json +++ b/tests/integration/tests/frameworks/nextjs/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "^4.8.0", + "@prisma/client": "5.12.0", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -22,6 +22,6 @@ "zod": "^3.22.4" }, "devDependencies": { - "prisma": "^4.8.0" + "prisma": "5.12.0" } } diff --git a/tests/integration/tests/frameworks/trpc/generation.test.ts b/tests/integration/tests/frameworks/trpc/generation.test.ts index e4cd0ede1..a58f5965d 100644 --- a/tests/integration/tests/frameworks/trpc/generation.test.ts +++ b/tests/integration/tests/frameworks/trpc/generation.test.ts @@ -36,7 +36,9 @@ describe('tRPC Routers Generation Tests', () => { process.chdir(testDir); run('npm install'); run('npm install ' + deps); - run('npx zenstack generate --no-dependency-check --schema ./todo.zmodel', { NODE_PATH: 'node_modules' }); + run('npx zenstack generate --no-dependency-check --schema ./todo.zmodel', { + NODE_PATH: 'node_modules', + }); run('npm run build', { NODE_PATH: 'node_modules' }); }); }); diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json index f27687e63..428790638 100644 --- a/tests/integration/tests/frameworks/trpc/test-project/package.json +++ b/tests/integration/tests/frameworks/trpc/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "^4.8.0", + "@prisma/client": "5.12.0", "@tanstack/react-query": "^4.22.4", "@trpc/client": "^10.34.0", "@trpc/next": "^10.34.0", @@ -26,6 +26,6 @@ "zod": "^3.22.4" }, "devDependencies": { - "prisma": "^4.8.0" + "prisma": "5.12.0" } } diff --git a/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel b/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel index 6840f8978..92363c825 100644 --- a/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel +++ b/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel @@ -7,16 +7,6 @@ generator js { provider = 'prisma-client-js' } -plugin meta { - provider = '@core/model-meta' - output = '.zenstack' -} - -plugin policy { - provider = '@core/access-policy' - output = '.zenstack' -} - plugin trpc { provider = '@zenstackhq/trpc' output = 'server/routers/generated' diff --git a/tests/integration/tests/misc/stacktrace.test.ts b/tests/integration/tests/misc/stacktrace.test.ts index 6573ed088..f652c5514 100644 --- a/tests/integration/tests/misc/stacktrace.test.ts +++ b/tests/integration/tests/misc/stacktrace.test.ts @@ -13,7 +13,7 @@ describe('Stack trace tests', () => { }); it('stack trace', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model Model { id String @id @default(uuid()) @@ -21,7 +21,7 @@ describe('Stack trace tests', () => { ` ); - const db = withPolicy(); + const db = enhance(); let error: Error | undefined = undefined; try { @@ -31,7 +31,7 @@ describe('Stack trace tests', () => { } expect(error?.stack).toContain( - "Error calling enhanced Prisma method `create`: denied by policy: model entities failed 'create' check" + "Error calling enhanced Prisma method `model.create`: denied by policy: model entities failed 'create' check" ); expect(error?.stack).toContain(`misc/stacktrace.test.ts`); expect((error as any).internalStack).toBeTruthy(); diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index d27d2bb45..00d40b755 100644 --- a/tests/integration/tests/plugins/zod.test.ts +++ b/tests/integration/tests/plugins/zod.test.ts @@ -5,6 +5,7 @@ import { loadSchema } from '@zenstackhq/testtools'; import { randomUUID } from 'crypto'; import fs from 'fs'; import path from 'path'; +import tmp from 'tmp'; describe('Zod plugin tests', () => { let origDir: string; @@ -46,6 +47,9 @@ describe('Zod plugin tests', () => { password String @omit role Role @default(USER) posts Post[] + age Int? + + @@validate(length(password, 6, 20)) } model Post { @@ -62,8 +66,14 @@ describe('Zod plugin tests', () => { { addPrelude: false, pushDb: false } ); const schemas = zodSchemas.models; + expect(schemas.UserScalarSchema).toBeTruthy(); + expect(schemas.UserWithoutRefineSchema).toBeTruthy(); expect(schemas.UserSchema).toBeTruthy(); + expect(schemas.UserCreateScalarSchema).toBeTruthy(); + expect(schemas.UserCreateWithoutRefineSchema).toBeTruthy(); expect(schemas.UserCreateSchema).toBeTruthy(); + expect(schemas.UserUpdateScalarSchema).toBeTruthy(); + expect(schemas.UserUpdateWithoutRefineSchema).toBeTruthy(); expect(schemas.UserUpdateSchema).toBeTruthy(); expect(schemas.UserPrismaCreateSchema).toBeTruthy(); expect(schemas.UserPrismaUpdateSchema).toBeTruthy(); @@ -76,6 +86,16 @@ describe('Zod plugin tests', () => { expect( schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123' }).success ).toBeTruthy(); + expect( + schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN', password: 'abc' }).success + ).toBeFalsy(); + expect( + schemas.UserCreateWithoutRefineSchema.safeParse({ + email: 'abc@zenstack.dev', + role: 'ADMIN', + password: 'abc', + }).success + ).toBeTruthy(); expect( schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN', password: 'abc123' }).success ).toBeTruthy(); @@ -91,6 +111,8 @@ describe('Zod plugin tests', () => { expect(schemas.UserUpdateSchema.safeParse({}).success).toBeTruthy(); expect(schemas.UserUpdateSchema.safeParse({ email: 'abc@def.com' }).success).toBeFalsy(); expect(schemas.UserUpdateSchema.safeParse({ email: 'def@zenstack.dev' }).success).toBeTruthy(); + expect(schemas.UserUpdateSchema.safeParse({ password: 'pas' }).success).toBeFalsy(); + expect(schemas.UserUpdateWithoutRefineSchema.safeParse({ password: 'pas' }).success).toBeTruthy(); expect(schemas.UserUpdateSchema.safeParse({ password: 'password456' }).success).toBeTruthy(); // update unchecked @@ -99,7 +121,25 @@ describe('Zod plugin tests', () => { ).toBeTruthy(); // model schema - expect(schemas.UserSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN' }).success).toBeTruthy(); + + // missing fields + expect( + schemas.UserSchema.safeParse({ + id: 1, + email: 'abc@zenstack.dev', + }).success + ).toBeFalsy(); + + expect( + schemas.UserSchema.safeParse({ + id: 1, + createdAt: new Date(), + updatedAt: new Date(), + email: 'abc@zenstack.dev', + role: 'ADMIN', + }).success + ).toBeTruthy(); + // without omitted field expect( schemas.UserSchema.safeParse({ @@ -110,6 +150,19 @@ describe('Zod plugin tests', () => { updatedAt: new Date(), }).success ).toBeTruthy(); + + // with optional field + expect( + schemas.UserSchema.safeParse({ + id: 1, + email: 'abc@zenstack.dev', + role: 'ADMIN', + createdAt: new Date(), + updatedAt: new Date(), + age: 18, + }).success + ).toBeTruthy(); + // with omitted field const withPwd = schemas.UserSchema.safeParse({ id: 1, @@ -674,4 +727,73 @@ describe('Zod plugin tests', () => { expect(fs.existsSync(path.join(projectDir, 'zod/models/Foo.schema.js'))).toBeFalsy(); expect(fs.existsSync(path.join(projectDir, 'zod/models/Bar.schema.js'))).toBeFalsy(); }); + + it('clear output', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.mkdirSync(path.join(projectDir, 'zod'), { recursive: true }); + fs.writeFileSync(path.join(projectDir, 'zod', 'test.txt'), 'hello'); + + await loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator js { + provider = 'prisma-client-js' + } + + plugin zod { + provider = "@core/zod" + output = "$projectRoot/zod" + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email @endsWith('@zenstack.dev') + password String @omit + } + `, + { addPrelude: false, pushDb: false, projectDir } + ); + + expect(fs.existsSync(path.join(projectDir, 'zod', 'test.txt'))).toBeFalsy(); + }); + + it('existing output as file', async () => { + const { name: projectDir } = tmp.dirSync(); + fs.writeFileSync(path.join(projectDir, 'zod'), 'hello'); + + await expect( + loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator js { + provider = 'prisma-client-js' + } + + plugin zod { + provider = "@core/zod" + output = "$projectRoot/zod" + } + + model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email @endsWith('@zenstack.dev') + password String @omit + } + `, + { addPrelude: false, pushDb: false, projectDir } + ) + ).rejects.toThrow('already exists and is not a directory'); + }); }); diff --git a/tests/integration/tests/schema/petstore.zmodel b/tests/integration/tests/schema/petstore.zmodel index 77ec1e643..42a279550 100644 --- a/tests/integration/tests/schema/petstore.zmodel +++ b/tests/integration/tests/schema/petstore.zmodel @@ -5,7 +5,6 @@ datasource db { generator js { provider = 'prisma-client-js' - previewFeatures = ['clientExtensions'] } plugin zod { diff --git a/tests/integration/tests/schema/todo.zmodel b/tests/integration/tests/schema/todo.zmodel index 079e3b1ef..524f3152c 100644 --- a/tests/integration/tests/schema/todo.zmodel +++ b/tests/integration/tests/schema/todo.zmodel @@ -9,7 +9,6 @@ datasource db { generator js { provider = 'prisma-client-js' - previewFeatures = ['clientExtensions'] } plugin zod { diff --git a/tests/integration/tsconfig.json b/tests/integration/tsconfig.json index 2771cd805..c6cc8d4a7 100644 --- a/tests/integration/tsconfig.json +++ b/tests/integration/tsconfig.json @@ -8,5 +8,5 @@ "skipLibCheck": true, "experimentalDecorators": true }, - "include": ["**/*.ts", "**/*.d.ts"] + "include": ["**/*.ts", "**/*.d.ts", "../regression/tests/issue-177.test.ts", "../regression/tests/issue-416.test.ts", "../regression/tests/issue-646.test.ts", "../regression/tests/issue-657.test.ts", "../regression/tests/issue-665.test.ts", "../regression/tests/issue-674.test.ts", "../regression/tests/issue-689.test.ts", "../regression/tests/issue-703.test.ts", "../regression/tests/issue-714.test.ts", "../regression/tests/issue-724.test.ts", "../regression/tests/issue-735.test.ts", "../regression/tests/issue-744.test.ts", "../regression/tests/issue-756.test.ts", "../regression/tests/issue-764.test.ts", "../regression/tests/issue-765.test.ts", "../regression/tests/issue-804.test.ts", "../regression/tests/issue-811.test.ts", "../regression/tests/issue-814.test.ts", "../regression/tests/issue-825.test.ts", "../regression/tests/issue-864.test.ts", "../regression/tests/issue-886.test.ts", "../regression/tests/issue-925.test.ts", "../regression/tests/issue-947.test.ts", "../regression/tests/issue-961.test.ts", "../regression/tests/issue-965.test.ts", "../regression/tests/issue-971.test.ts", "../regression/tests/issue-992.test.ts", "../regression/tests/issue-1014.test.ts", "../regression/tests/issue-1078.test.ts", "../regression/tests/issue-1080.test.ts", "../regression/tests/issue-1129.test.ts", "../regression/tests/issue-1167.test.ts", "../regression/tests/issue-1179.test.ts", "../regression/tests/issue-1186.test.ts", "../regression/tests/issue-1210.test.ts", "../regression/tests/issue-1235.test.ts", "../regression/tests/issue-1241.test.ts", "../regression/tests/issue-1257.test.ts", "../regression/tests/issue-1265.test.ts", "../regression/tests/issues.test.ts"] } diff --git a/tests/integration/utils/index.ts b/tests/integration/utils/index.ts deleted file mode 100644 index 04bca77e0..000000000 --- a/tests/integration/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils'; diff --git a/tests/regression/.eslintrc.json b/tests/regression/.eslintrc.json new file mode 100644 index 000000000..24ebad85a --- /dev/null +++ b/tests/regression/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "extends": ["plugin:jest/recommended"], + "rules": { + "jest/expect-expect": "off" + } +} diff --git a/tests/regression/jest.config.ts b/tests/regression/jest.config.ts new file mode 100644 index 000000000..67a118269 --- /dev/null +++ b/tests/regression/jest.config.ts @@ -0,0 +1,10 @@ +import baseConfig from '../../jest.config'; + +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ +export default { + ...baseConfig, + setupFilesAfterEnv: ['./test-setup.ts'], +}; diff --git a/tests/regression/jest.d.ts b/tests/regression/jest.d.ts new file mode 100644 index 000000000..ff066029a --- /dev/null +++ b/tests/regression/jest.d.ts @@ -0,0 +1,16 @@ +interface CustomMatchers { + toBeRejectedByPolicy(expectedMessages?: string[]): Promise; + toBeNotFound(): Promise; + toResolveTruthy(): Promise; + toResolveFalsy(): Promise; + toResolveNull(): Promise; + toBeRejectedWithCode(code: string): Promise; +} +declare global { + namespace jest { + interface Expect extends CustomMatchers {} + interface Matchers extends CustomMatchers {} + interface InverseAsymmetricMatchers extends CustomMatchers {} + } +} +export {}; diff --git a/tests/regression/package.json b/tests/regression/package.json new file mode 100644 index 000000000..20f5cbc30 --- /dev/null +++ b/tests/regression/package.json @@ -0,0 +1,24 @@ +{ + "name": "regression", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "lint": "eslint . --ext .ts", + "test": "jest" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@zenstackhq/runtime": "workspace:*", + "@zenstackhq/server": "workspace:*", + "zenstack": "workspace: *" + }, + "dependencies": { + "@types/node": "^18.0.0", + "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/testtools": "workspace:*", + "decimal.js": "^10.4.2" + } +} diff --git a/tests/regression/test-setup.ts b/tests/regression/test-setup.ts new file mode 100644 index 000000000..89142216d --- /dev/null +++ b/tests/regression/test-setup.ts @@ -0,0 +1,17 @@ +import { + toBeNotFound, + toBeRejectedByPolicy, + toBeRejectedWithCode, + toResolveFalsy, + toResolveNull, + toResolveTruthy, +} from '@zenstackhq/testtools/jest-ext'; + +expect.extend({ + toBeRejectedByPolicy, + toBeNotFound, + toResolveTruthy, + toResolveFalsy, + toResolveNull, + toBeRejectedWithCode, +}); diff --git a/tests/integration/tests/regression/issue-1014.test.ts b/tests/regression/tests/issue-1014.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1014.test.ts rename to tests/regression/tests/issue-1014.test.ts diff --git a/tests/integration/tests/regression/issue-1078.test.ts b/tests/regression/tests/issue-1078.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1078.test.ts rename to tests/regression/tests/issue-1078.test.ts diff --git a/tests/integration/tests/regression/issue-1080.test.ts b/tests/regression/tests/issue-1080.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1080.test.ts rename to tests/regression/tests/issue-1080.test.ts diff --git a/tests/integration/tests/regression/issue-1129.test.ts b/tests/regression/tests/issue-1129.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1129.test.ts rename to tests/regression/tests/issue-1129.test.ts diff --git a/tests/regression/tests/issue-1167.test.ts b/tests/regression/tests/issue-1167.test.ts new file mode 100644 index 000000000..29b81adbf --- /dev/null +++ b/tests/regression/tests/issue-1167.test.ts @@ -0,0 +1,20 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1167', () => { + it('regression', 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/tests/issue-1179.test.ts b/tests/regression/tests/issue-1179.test.ts new file mode 100644 index 000000000..3d5fd8d99 --- /dev/null +++ b/tests/regression/tests/issue-1179.test.ts @@ -0,0 +1,27 @@ +import { loadModel } from '@zenstackhq/testtools'; + +describe('issue 1179', () => { + it('regression', async () => { + await loadModel( + ` + abstract model Base { + id String @id @default(uuid()) + } + + model User extends 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/tests/issue-1186.test.ts b/tests/regression/tests/issue-1186.test.ts new file mode 100644 index 000000000..d36efcd57 --- /dev/null +++ b/tests/regression/tests/issue-1186.test.ts @@ -0,0 +1,51 @@ +import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1186', () => { + it('regression', async () => { + await loadSchema( + `schema.zmodel + import "model" + + ${FILE_SPLITTER}model.zmodel + generator client { + provider = "prisma-client-js" + binaryTargets = ["native"] + previewFeatures = ["postgresqlExtensions"] + } + + datasource db { + provider = "postgresql" + extensions = [citext] + + url = env("DATABASE_URL") + } + enum UserRole { + USER + ADMIN + } + + model User { + id String @id @default(uuid()) + role UserRole @default(USER) @deny('read,update', auth().role != ADMIN) + post Post[] + } + + abstract model Base { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id]) + + @@allow('create', userId == auth().id) + @@allow('update', userId == auth().id && future().userId == auth().id) + + @@allow('all', auth().role == ADMIN) + } + + model Post extends Base { + description String + } + `, + { addPrelude: false, pushDb: false } + ); + }); +}); diff --git a/tests/regression/tests/issue-1210.test.ts b/tests/regression/tests/issue-1210.test.ts new file mode 100644 index 000000000..ef1d407e1 --- /dev/null +++ b/tests/regression/tests/issue-1210.test.ts @@ -0,0 +1,92 @@ +import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1210', () => { + it('regression', async () => { + await loadSchema( + `schema.zmodel + import "./user" + import "./tokens" + + generator client { + provider = "prisma-client-js" + binaryTargets = ["native"] + previewFeatures = ["postgresqlExtensions"] + } + + datasource db { + provider = "postgresql" + extensions = [citext] + + url = env("DATABASE_URL") + } + + plugin zod { + provider = '@core/zod' + } + + ${FILE_SPLITTER}base.zmodel + abstract model Base { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @omit + + @@deny('read', deletedAt != null) + @@deny('delete', true) + } + + ${FILE_SPLITTER}tokens.zmodel + import "base" + import "user" + + model Session extends Base { + expiresAt DateTime + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) + } + + ${FILE_SPLITTER}user.zmodel + import "base" + import "tokens" + enum UserRole { + USER + ADMIN + } + + model User extends Base { + email String @unique @db.Citext @email @trim @lower + role UserRole @default(USER) @deny('read,update', auth().role != ADMIN) + + sessions Session[] + posts Post[] + + @@allow('read,create', auth() == this) + @@allow('all', auth().role == ADMIN) + } + + abstract model UserEntity extends Base { + userId String + user User @relation(fields: [userId], references: [id]) + + @@allow('create', userId == auth().id) + @@allow('update', userId == auth().id && future().userId == auth().id) + + @@allow('all', auth().role == ADMIN) + } + + abstract model PrivateUserEntity extends UserEntity { + @@allow('read', userId == auth().id) + } + + abstract model PublicUserEntity extends UserEntity { + @@allow('read', true) + } + + model Post extends PublicUserEntity { + title String + } + `, + { addPrelude: false, pushDb: false } + ); + }); +}); diff --git a/tests/integration/tests/regression/issue-1235.test.ts b/tests/regression/tests/issue-1235.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1235.test.ts rename to tests/regression/tests/issue-1235.test.ts diff --git a/tests/integration/tests/regression/issue-1241.test.ts b/tests/regression/tests/issue-1241.test.ts similarity index 57% rename from tests/integration/tests/regression/issue-1241.test.ts rename to tests/regression/tests/issue-1241.test.ts index 3a53f567c..e5d94c9b7 100644 --- a/tests/integration/tests/regression/issue-1241.test.ts +++ b/tests/regression/tests/issue-1241.test.ts @@ -5,40 +5,40 @@ describe('issue 1241', () => { it('regression', async () => { const { enhance, prisma } = await loadSchema( ` - 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) - } - `, + 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) + } + `, { logPrismaQuery: true } ); diff --git a/tests/regression/tests/issue-1257.test.ts b/tests/regression/tests/issue-1257.test.ts new file mode 100644 index 000000000..a692d0464 --- /dev/null +++ b/tests/regression/tests/issue-1257.test.ts @@ -0,0 +1,53 @@ +import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1210', () => { + it('regression', async () => { + await loadSchema( + `schema.zmodel + import "./user" + import "./image" + + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + ${FILE_SPLITTER}base.zmodel + abstract model Base { + id Int @id @default(autoincrement()) + } + + ${FILE_SPLITTER}user.zmodel + import "./base" + import "./image" + + enum Role { + Admin + } + + model User extends Base { + email String @unique + role Role + @@auth + } + + ${FILE_SPLITTER}image.zmodel + import "./user" + import "./base" + + model Image extends Base { + width Int @default(0) + height Int @default(0) + + @@allow('read', true) + @@allow('all', auth().role == Admin) + } + `, + { addPrelude: false, pushDb: false } + ); + }); +}); diff --git a/tests/regression/tests/issue-1265.test.ts b/tests/regression/tests/issue-1265.test.ts new file mode 100644 index 000000000..cd7df4636 --- /dev/null +++ b/tests/regression/tests/issue-1265.test.ts @@ -0,0 +1,27 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1265', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` + 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) + } + `, + { fullZod: true, pushDb: false } + ); + + 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/tests/issue-1268.test.ts b/tests/regression/tests/issue-1268.test.ts new file mode 100644 index 000000000..b51d954f7 --- /dev/null +++ b/tests/regression/tests/issue-1268.test.ts @@ -0,0 +1,32 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1268', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + bytes Bytes + } + `, + { + fullZod: true, + pushDb: false, + compile: true, + extraSourceFiles: [ + { + name: 'test.ts', + content: ` +import { ModelCreateInputObjectSchema } from '.zenstack/zod/objects'; +ModelCreateInputObjectSchema.parse({ bytes: new Uint8Array(0) }); + `, + }, + ], + } + ); + + expect( + zodSchemas.objects.ModelCreateInputObjectSchema.safeParse({ bytes: new Uint8Array(0) }).success + ).toBeTruthy(); + }); +}); diff --git a/tests/integration/tests/regression/issue-1271.test.ts b/tests/regression/tests/issue-1271.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1271.test.ts rename to tests/regression/tests/issue-1271.test.ts diff --git a/tests/integration/tests/regression/issue-177.test.ts b/tests/regression/tests/issue-177.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-177.test.ts rename to tests/regression/tests/issue-177.test.ts diff --git a/tests/integration/tests/regression/issue-416.test.ts b/tests/regression/tests/issue-416.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-416.test.ts rename to tests/regression/tests/issue-416.test.ts diff --git a/tests/integration/tests/regression/issue-646.test.ts b/tests/regression/tests/issue-646.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-646.test.ts rename to tests/regression/tests/issue-646.test.ts diff --git a/tests/integration/tests/regression/issue-657.test.ts b/tests/regression/tests/issue-657.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-657.test.ts rename to tests/regression/tests/issue-657.test.ts diff --git a/tests/integration/tests/regression/issue-665.test.ts b/tests/regression/tests/issue-665.test.ts similarity index 77% rename from tests/integration/tests/regression/issue-665.test.ts rename to tests/regression/tests/issue-665.test.ts index 8bd9f717b..b6552fd2b 100644 --- a/tests/integration/tests/regression/issue-665.test.ts +++ b/tests/regression/tests/issue-665.test.ts @@ -2,7 +2,7 @@ import { loadSchema } from '@zenstackhq/testtools'; describe('Regression: issue 665', () => { it('regression', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id Int @id @default(autoincrement()) @@ -20,19 +20,19 @@ describe('Regression: issue 665', () => { await prisma.user.create({ data: { id: 1, username: 'test', password: 'test', admin: true } }); // admin - let r = await withPolicy({ id: 1, admin: true }).user.findFirst(); + let r = await enhance({ id: 1, admin: true }).user.findFirst(); expect(r.username).toEqual('test'); // owner - r = await withPolicy({ id: 1 }).user.findFirst(); + r = await enhance({ id: 1 }).user.findFirst(); expect(r.username).toEqual('test'); // anonymous - r = await withPolicy().user.findFirst(); + r = await enhance().user.findFirst(); expect(r.username).toBeUndefined(); // non-owner - r = await withPolicy({ id: 2 }).user.findFirst(); + r = await enhance({ id: 2 }).user.findFirst(); expect(r.username).toBeUndefined(); }); }); diff --git a/tests/integration/tests/regression/issue-674.test.ts b/tests/regression/tests/issue-674.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-674.test.ts rename to tests/regression/tests/issue-674.test.ts diff --git a/tests/integration/tests/regression/issue-689.test.ts b/tests/regression/tests/issue-689.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-689.test.ts rename to tests/regression/tests/issue-689.test.ts diff --git a/tests/integration/tests/regression/issue-703.test.ts b/tests/regression/tests/issue-703.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-703.test.ts rename to tests/regression/tests/issue-703.test.ts diff --git a/tests/integration/tests/regression/issue-714.test.ts b/tests/regression/tests/issue-714.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-714.test.ts rename to tests/regression/tests/issue-714.test.ts diff --git a/tests/integration/tests/regression/issue-724.test.ts b/tests/regression/tests/issue-724.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-724.test.ts rename to tests/regression/tests/issue-724.test.ts diff --git a/tests/integration/tests/regression/issue-735.test.ts b/tests/regression/tests/issue-735.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-735.test.ts rename to tests/regression/tests/issue-735.test.ts diff --git a/tests/integration/tests/regression/issue-744.test.ts b/tests/regression/tests/issue-744.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-744.test.ts rename to tests/regression/tests/issue-744.test.ts diff --git a/tests/integration/tests/regression/issue-756.test.ts b/tests/regression/tests/issue-756.test.ts similarity index 90% rename from tests/integration/tests/regression/issue-756.test.ts rename to tests/regression/tests/issue-756.test.ts index b10e60af2..9f6750ea9 100644 --- a/tests/integration/tests/regression/issue-756.test.ts +++ b/tests/regression/tests/issue-756.test.ts @@ -28,6 +28,6 @@ describe('Regression: issue 756', () => { } ` ) - ).toContain('expression cannot be resolved'); + ).toContain(`Could not resolve reference to DataModelField named 'authorId'.`); }); }); diff --git a/tests/integration/tests/regression/issue-764.test.ts b/tests/regression/tests/issue-764.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-764.test.ts rename to tests/regression/tests/issue-764.test.ts diff --git a/tests/integration/tests/regression/issue-765.test.ts b/tests/regression/tests/issue-765.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-765.test.ts rename to tests/regression/tests/issue-765.test.ts diff --git a/tests/integration/tests/regression/issue-804.test.ts b/tests/regression/tests/issue-804.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-804.test.ts rename to tests/regression/tests/issue-804.test.ts diff --git a/tests/integration/tests/regression/issue-811.test.ts b/tests/regression/tests/issue-811.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-811.test.ts rename to tests/regression/tests/issue-811.test.ts diff --git a/tests/integration/tests/regression/issue-814.test.ts b/tests/regression/tests/issue-814.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-814.test.ts rename to tests/regression/tests/issue-814.test.ts diff --git a/tests/integration/tests/regression/issue-825.test.ts b/tests/regression/tests/issue-825.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-825.test.ts rename to tests/regression/tests/issue-825.test.ts diff --git a/tests/integration/tests/regression/issue-864.test.ts b/tests/regression/tests/issue-864.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-864.test.ts rename to tests/regression/tests/issue-864.test.ts diff --git a/tests/integration/tests/regression/issue-886.test.ts b/tests/regression/tests/issue-886.test.ts similarity index 84% rename from tests/integration/tests/regression/issue-886.test.ts rename to tests/regression/tests/issue-886.test.ts index a749db61e..4f20d9817 100644 --- a/tests/integration/tests/regression/issue-886.test.ts +++ b/tests/regression/tests/issue-886.test.ts @@ -13,10 +13,10 @@ describe('Regression: issue 886', () => { ` ); - const r = zodSchemas.models.ModelSchema.parse({}); + 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).toBeUndefined(); + expect(r.id).toBe(1); }); }); diff --git a/tests/integration/tests/regression/issue-925.test.ts b/tests/regression/tests/issue-925.test.ts similarity index 85% rename from tests/integration/tests/regression/issue-925.test.ts rename to tests/regression/tests/issue-925.test.ts index 34b1ac434..19ef210bf 100644 --- a/tests/integration/tests/regression/issue-925.test.ts +++ b/tests/regression/tests/issue-925.test.ts @@ -1,7 +1,7 @@ -import { loadModelWithError } from '@zenstackhq/testtools'; +import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; describe('Regression: issue 925', () => { - it('member reference from this', async () => { + it('member reference without using this', async () => { await expect( loadModelWithError( ` @@ -10,7 +10,7 @@ describe('Regression: issue 925', () => { company Company[] test Int - @@allow('read', auth().company?[staff?[companyId == this.test]]) + @@allow('read', auth().company?[staff?[companyId == test]]) } model Company { @@ -32,19 +32,19 @@ describe('Regression: issue 925', () => { } ` ) - ).resolves.toContain("Could not resolve reference to DataModelField named 'test'."); + ).resolves.toContain("Could not resolve reference to ReferenceTarget named 'test'."); }); - it('simple reference', async () => { - await expect( - loadModelWithError( - ` + // eslint-disable-next-line jest/no-disabled-tests + it.skip('reference with this', async () => { + await loadModel( + ` model User { id Int @id @default(autoincrement()) company Company[] test Int - @@allow('read', auth().company?[staff?[companyId == test]]) + @@allow('read', auth().company?[staff?[companyId == this.test]]) } model Company { @@ -65,7 +65,6 @@ describe('Regression: issue 925', () => { @@allow('read', true) } ` - ) - ).resolves.toContain("Could not resolve reference to ReferenceTarget named 'test'."); + ); }); }); diff --git a/tests/integration/tests/regression/issue-947.test.ts b/tests/regression/tests/issue-947.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-947.test.ts rename to tests/regression/tests/issue-947.test.ts diff --git a/tests/integration/tests/regression/issue-961.test.ts b/tests/regression/tests/issue-961.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-961.test.ts rename to tests/regression/tests/issue-961.test.ts diff --git a/tests/integration/tests/regression/issue-965.test.ts b/tests/regression/tests/issue-965.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-965.test.ts rename to tests/regression/tests/issue-965.test.ts diff --git a/tests/integration/tests/regression/issue-971.test.ts b/tests/regression/tests/issue-971.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-971.test.ts rename to tests/regression/tests/issue-971.test.ts diff --git a/tests/integration/tests/regression/issue-992.test.ts b/tests/regression/tests/issue-992.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-992.test.ts rename to tests/regression/tests/issue-992.test.ts diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/regression/tests/issues.test.ts similarity index 93% rename from tests/integration/tests/regression/issues.test.ts rename to tests/regression/tests/issues.test.ts index 8353f8bad..318682aad 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/regression/tests/issues.test.ts @@ -13,7 +13,7 @@ describe('GitHub issues regression', () => { }); it('issue 389', async () => { - const { withPolicy } = await loadSchema(` + const { enhance } = await loadSchema(` model model { id String @id @default(uuid()) value Int @@ -21,7 +21,7 @@ describe('GitHub issues regression', () => { @@allow('create', value > 0) } `); - const db = withPolicy(); + const db = enhance(); await expect(db.model.create({ data: { value: 0 } })).toBeRejectedByPolicy(); await expect(db.model.create({ data: { value: 1 } })).toResolveTruthy(); }); @@ -88,7 +88,7 @@ describe('GitHub issues regression', () => { }); it('select with _count', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id String @id @unique @default(uuid()) @@ -117,7 +117,7 @@ describe('GitHub issues regression', () => { }, }); - const db = withPolicy(); + const db = enhance(); const r = await db.user.findFirst({ select: { _count: { select: { posts: true } } } }); expect(r).toMatchObject({ _count: { posts: 2 } }); }); @@ -150,7 +150,7 @@ describe('GitHub issues regression', () => { }); it('issue 552', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model Tenant { id Int @id @default(autoincrement()) @@ -240,7 +240,7 @@ describe('GitHub issues regression', () => { }, }); - const db = withPolicy({ id: 1, is_super_admin: true }); + const db = enhance({ id: 1, is_super_admin: true }); await db.userTenant.update({ where: { user_id_tenant_id: { @@ -259,7 +259,7 @@ describe('GitHub issues regression', () => { }); it('issue 609', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model User { id String @id @default(cuid()) @@ -300,7 +300,7 @@ describe('GitHub issues regression', () => { }); // connecting a child comment from a different user to a parent comment should succeed - const db = withPolicy({ id: '2' }); + const db = enhance({ id: '2' }); await expect( db.comment.create({ data: { @@ -313,7 +313,7 @@ describe('GitHub issues regression', () => { }); it('issue 624', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -327,9 +327,9 @@ model User { // can be created by anyone, even not logged in @@allow('create', true) // can be read by users in the same organization - @@allow('read', orgs?[members?[auth() == this]]) + @@allow('read', orgs?[members?[auth().id == id]]) // full access by oneself - @@allow('all', auth() == this) + @@allow('all', auth().id == id) } model Organization { @@ -343,7 +343,7 @@ model Organization { // everyone can create a organization @@allow('create', true) // any user in the organization can read the organization - @@allow('read', members?[auth() == this]) + @@allow('read', members?[auth().id == id]) } abstract model organizationBaseEntity { @@ -359,15 +359,15 @@ abstract model organizationBaseEntity { groups Group[] // when create, owner must be set to current user, and user must be in the organization - @@allow('create', owner == auth() && org.members?[this == auth()]) + @@allow('create', owner == auth() && org.members?[id == auth().id]) // only the owner can update it and is not allowed to change the owner - @@allow('update', owner == auth() && org.members?[this == auth()] && future().owner == owner) + @@allow('update', owner == auth() && org.members?[id == auth().id] && future().owner == owner) // allow owner to read @@allow('read', owner == auth()) // allow shared group members to read it - @@allow('read', groups?[users?[this == auth()]]) + @@allow('read', groups?[users?[id == auth().id]]) // allow organization to access if public - @@allow('read', isPublic && org.members?[this == auth()]) + @@allow('read', isPublic && org.members?[id == auth().id]) // can not be read if deleted @@deny('all', isDeleted == true) } @@ -394,7 +394,7 @@ model Group { orgId String // group is shared by organization - @@allow('all', org.members?[auth() == this]) + @@allow('all', org.members?[auth().id == id]) } ` ); @@ -476,7 +476,7 @@ model Group { console.log(`Created user with id: ${user.id}`); } - const db = withPolicy({ id: 'robin@prisma.io' }); + const db = enhance({ id: 'robin@prisma.io' }); await expect( db.post.findMany({ where: {}, @@ -507,7 +507,7 @@ model Group { }); it('issue 627', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -541,7 +541,7 @@ model Equipment extends BaseEntityWithTenant { }, }); - const db = withPolicy({ id: 'tenant-1' }); + const db = enhance({ id: 'tenant-1' }); await expect( db.equipment.create({ data: { @@ -586,7 +586,7 @@ model TwoEnumsOneModelTest { }); it('issue 634', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { id String @id @default(uuid()) @@ -600,7 +600,7 @@ model User { // can be created by anyone, even not logged in @@allow('create', true) // can be read by users in the same organization - @@allow('read', orgs?[members?[auth() == this]]) + @@allow('read', orgs?[members?[auth().id == id]]) // full access by oneself @@allow('all', auth() == this) } @@ -616,7 +616,7 @@ model Organization { // everyone can create a organization @@allow('create', true) // any user in the organization can read the organization - @@allow('read', members?[auth() == this]) + @@allow('read', members?[auth().id == id]) } abstract model organizationBaseEntity { @@ -632,15 +632,15 @@ abstract model organizationBaseEntity { groups Group[] // when create, owner must be set to current user, and user must be in the organization - @@allow('create', owner == auth() && org.members?[this == auth()]) + @@allow('create', owner == auth() && org.members?[id == auth().id]) // only the owner can update it and is not allowed to change the owner - @@allow('update', owner == auth() && org.members?[this == auth()] && future().owner == owner) + @@allow('update', owner == auth() && org.members?[id == auth().id] && future().owner == owner) // allow owner to read @@allow('read', owner == auth()) // allow shared group members to read it - @@allow('read', groups?[users?[this == auth()]]) + @@allow('read', groups?[users?[id == auth().id]]) // allow organization to access if public - @@allow('read', isPublic && org.members?[this == auth()]) + @@allow('read', isPublic && org.members?[id == auth().id]) // can not be read if deleted @@deny('all', isDeleted == true) } @@ -667,7 +667,7 @@ model Group { orgId String // group is shared by organization - @@allow('all', org.members?[auth() == this]) + @@allow('all', org.members?[auth().id == id]) } ` ); @@ -749,7 +749,7 @@ model Group { console.log(`Created user with id: ${user.id}`); } - const db = withPolicy({ id: 'robin@prisma.io' }); + const db = enhance({ id: 'robin@prisma.io' }); await expect( db.comment.findMany({ where: { diff --git a/tests/regression/tsconfig.json b/tests/regression/tsconfig.json new file mode 100644 index 000000000..c6cc8d4a7 --- /dev/null +++ b/tests/regression/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "experimentalDecorators": true + }, + "include": ["**/*.ts", "**/*.d.ts", "../regression/tests/issue-177.test.ts", "../regression/tests/issue-416.test.ts", "../regression/tests/issue-646.test.ts", "../regression/tests/issue-657.test.ts", "../regression/tests/issue-665.test.ts", "../regression/tests/issue-674.test.ts", "../regression/tests/issue-689.test.ts", "../regression/tests/issue-703.test.ts", "../regression/tests/issue-714.test.ts", "../regression/tests/issue-724.test.ts", "../regression/tests/issue-735.test.ts", "../regression/tests/issue-744.test.ts", "../regression/tests/issue-756.test.ts", "../regression/tests/issue-764.test.ts", "../regression/tests/issue-765.test.ts", "../regression/tests/issue-804.test.ts", "../regression/tests/issue-811.test.ts", "../regression/tests/issue-814.test.ts", "../regression/tests/issue-825.test.ts", "../regression/tests/issue-864.test.ts", "../regression/tests/issue-886.test.ts", "../regression/tests/issue-925.test.ts", "../regression/tests/issue-947.test.ts", "../regression/tests/issue-961.test.ts", "../regression/tests/issue-965.test.ts", "../regression/tests/issue-971.test.ts", "../regression/tests/issue-992.test.ts", "../regression/tests/issue-1014.test.ts", "../regression/tests/issue-1078.test.ts", "../regression/tests/issue-1080.test.ts", "../regression/tests/issue-1129.test.ts", "../regression/tests/issue-1167.test.ts", "../regression/tests/issue-1179.test.ts", "../regression/tests/issue-1186.test.ts", "../regression/tests/issue-1210.test.ts", "../regression/tests/issue-1235.test.ts", "../regression/tests/issue-1241.test.ts", "../regression/tests/issue-1257.test.ts", "../regression/tests/issue-1265.test.ts", "../regression/tests/issues.test.ts"] +} From a331fb407c2141fae73aafb94a68a6f43a3df0eb Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 24 Apr 2024 21:25:45 +0800 Subject: [PATCH 2/2] chore: fix CI score card settings (#1283) --- .github/workflows/security-scorecard.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml index 2e8dd159a..aafcfb513 100644 --- a/.github/workflows/security-scorecard.yml +++ b/.github/workflows/security-scorecard.yml @@ -14,7 +14,6 @@ on: push: branches: - main - - dev # Declare default permissions as read only. permissions: