From a3722b015cc6f84bac488639a38e649421644177 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:27:49 +0800 Subject: [PATCH 001/127] chore: bump version v2 --- package.json | 2 +- packages/language/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index dbedaf9dd..83d9f7072 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.5.0", + "version": "2.0.0-alpha.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index fff1840c2..f6683a216 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.5.0", + "version": "2.0.0-alpha.1", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 00267ae73..883c9a954 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.5.0", + "version": "2.0.0-alpha.1", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index d38a7dcd7..d8427700e 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.5.0", + "version": "2.0.0-alpha.1", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index d50a39ee5..39f22ace4 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.5.0", + "version": "2.0.0-alpha.1", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 846febecd..2f24da31f 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.5.0", + "version": "2.0.0-alpha.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a56f6b115..336ad6d34 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.5.0", + "version": "2.0.0-alpha.1", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index a27e44032..c27906d0b 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.5.0", + "version": "2.0.0-alpha.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5c0fcd83e..d61b2449d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.5.0", + "version": "2.0.0-alpha.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 2d02ee7e5..87d0260c0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.5.0", + "version": "2.0.0-alpha.1", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 3919fe14e..84eb36b60 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.5.0", + "version": "2.0.0-alpha.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From e574048d8b2b312a473884da7f20cfcdedff68bd Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 8 Jan 2024 23:53:10 +0800 Subject: [PATCH 002/127] refactor: generate `enhance` API and avoid dynamic `require` in runtime (#937) --- .github/workflows/build-test.yml | 9 +- packages/plugins/tanstack-query/tsconfig.json | 2 +- packages/runtime/package.json | 2 - packages/runtime/res/enhance.d.ts | 1 + packages/runtime/res/enhance.js | 10 ++ packages/runtime/src/constants.ts | 2 +- packages/runtime/src/enhance.d.ts | 2 + packages/runtime/src/enhancements/enhance.ts | 12 +-- packages/runtime/src/enhancements/index.ts | 1 - packages/runtime/src/enhancements/omit.ts | 10 +- packages/runtime/src/enhancements/password.ts | 10 +- .../src/enhancements/policy/handler.ts | 91 ++++++++++++++----- .../runtime/src/enhancements/policy/index.ts | 21 +++-- .../src/enhancements/policy/policy-utils.ts | 8 +- packages/runtime/src/enhancements/preset.ts | 20 ---- packages/runtime/src/enhancements/types.ts | 2 +- packages/runtime/src/enhancements/utils.ts | 72 ++------------- packages/runtime/src/index.ts | 2 +- packages/runtime/src/loader.ts | 88 ------------------ packages/runtime/src/package.json | 1 + packages/runtime/src/version.ts | 43 +-------- packages/schema/package.json | 4 +- packages/schema/src/cli/plugin-runner.ts | 25 ++++- packages/schema/src/plugins/enhancer/index.ts | 64 +++++++++++++ packages/schema/src/plugins/plugin-utils.ts | 2 +- packages/schema/src/plugins/prisma/index.ts | 1 + .../src/plugins/prisma/schema-generator.ts | 2 +- packages/schema/src/plugins/zod/index.ts | 1 + packages/schema/src/utils/exec-utils.ts | 9 +- .../tests/generator/prisma-generator.test.ts | 22 ++++- packages/sdk/src/prisma.ts | 28 +++++- packages/server/src/api/base.ts | 5 +- packages/server/src/shared.ts | 89 +++++++++++++++++- packages/server/tests/api/rest.test.ts | 6 +- packages/testtools/src/package.template.json | 4 +- packages/testtools/src/schema.ts | 55 ++++++----- pnpm-lock.yaml | 74 ++++++++++----- script/test-prisma-v5.sh | 3 - tests/integration/test-run/package.json | 4 +- tests/integration/tests/cli/generate.test.ts | 20 ---- tests/integration/tests/cli/init.test.ts | 36 +++++--- tests/integration/tests/cli/plugins.test.ts | 2 - .../enhancements/with-omit/with-omit.test.ts | 29 ------ .../with-password/with-password.test.ts | 23 ----- .../with-policy/client-extensions.test.ts | 57 ++++++------ .../enhancements/with-policy/options.test.ts | 24 ++--- .../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 -- 50 files changed, 513 insertions(+), 507 deletions(-) create mode 100644 packages/runtime/res/enhance.d.ts create mode 100644 packages/runtime/res/enhance.js create mode 100644 packages/runtime/src/enhance.d.ts delete mode 100644 packages/runtime/src/enhancements/preset.ts delete mode 100644 packages/runtime/src/loader.ts create mode 120000 packages/runtime/src/package.json create mode 100644 packages/schema/src/plugins/enhancer/index.ts delete mode 100755 script/test-prisma-v5.sh diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index aa060aa56..484779e32 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,7 +9,7 @@ env: on: pull_request: - branches: ['dev', 'main'] + branches: ['dev', 'main', 'v2'] jobs: build-test: @@ -32,18 +32,11 @@ jobs: strategy: matrix: node-version: [18.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/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/runtime/package.json b/packages/runtime/package.json index cab662fe2..5ae027701 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -53,11 +53,9 @@ "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", "lower-case-first": "^2.0.2", 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/src/constants.ts b/packages/runtime/src/constants.ts index 3c12b5a88..bd191924a 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -66,7 +66,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. 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/enhance.ts b/packages/runtime/src/enhancements/enhance.ts index 42a504bdf..977a8e950 100644 --- a/packages/runtime/src/enhancements/enhance.ts +++ b/packages/runtime/src/enhancements/enhance.ts @@ -1,4 +1,3 @@ -import { getDefaultModelMeta } from '../loader'; import { withOmit, WithOmitOptions } from './omit'; import { withPassword, WithPasswordOptions } from './password'; import { withPolicy, WithPolicyContext, WithPolicyOptions } from './policy'; @@ -21,16 +20,15 @@ let hasOmit: boolean | undefined = undefined; * @param context The context to for evaluating access policies. * @param options Options. */ -export function enhance( +export function createEnhancement( prisma: DbClient, - context?: WithPolicyContext, - options?: EnhancementOptions + options: EnhancementOptions, + context?: WithPolicyContext ) { 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)); + const allFields = Object.values(options.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')); } @@ -46,7 +44,7 @@ export function enhance( } // policy proxy - result = withPolicy(result, context, options); + result = withPolicy(result, options, context); return result; } diff --git a/packages/runtime/src/enhancements/index.ts b/packages/runtime/src/enhancements/index.ts index 25b150a71..51f304657 100644 --- a/packages/runtime/src/enhancements/index.ts +++ b/packages/runtime/src/enhancements/index.ts @@ -3,7 +3,6 @@ export * from './enhance'; export * from './omit'; export * from './password'; export * from './policy'; -export * from './preset'; export * from './types'; export * from './utils'; export * from './where-visitor'; diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index e8b3f8c98..22b002309 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -2,7 +2,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { enumerate, getModelFields, resolveField, type ModelMeta } from '../cross'; -import { getDefaultModelMeta } from '../loader'; import { DbClientContract } from '../types'; import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; import { CommonEnhancementOptions } from './types'; @@ -14,18 +13,17 @@ export interface WithOmitOptions extends CommonEnhancementOptions { /** * Model metadata */ - modelMeta?: ModelMeta; + modelMeta: ModelMeta; } /** * Gets an enhanced Prisma client that supports @omit attribute. */ -export function withOmit(prisma: DbClient, options?: WithOmitOptions): DbClient { - const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); +export function withOmit(prisma: DbClient, options: WithOmitOptions): 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.modelMeta), 'omit' ); } diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index 34844dfb4..5846bc8dc 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -4,7 +4,6 @@ import { hash } from 'bcryptjs'; import { DEFAULT_PASSWORD_SALT_LENGTH } from '../constants'; import { NestedWriteVisitor, type ModelMeta, type PrismaWriteActionType } from '../cross'; -import { getDefaultModelMeta } from '../loader'; import { DbClientContract } from '../types'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; import { CommonEnhancementOptions } from './types'; @@ -16,18 +15,17 @@ export interface WithPasswordOptions extends CommonEnhancementOptions { /** * Model metadata */ - modelMeta?: ModelMeta; + modelMeta: ModelMeta; } /** * Gets an enhanced Prisma client that supports @password attribute. */ -export function withPassword(prisma: DbClient, options?: WithPasswordOptions): DbClient { - const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); +export function withPassword(prisma: DbClient, options: WithPasswordOptions): 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.modelMeta), 'password' ); } diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index f002002d2..748877805 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -47,6 +47,7 @@ export class PolicyProxyHandler implements Pr private readonly policy: PolicyDef, private readonly modelMeta: ModelMeta, private readonly zodSchemas: ZodSchemas | undefined, + private readonly prismaModule: any, model: string, private readonly user?: AuthUser, private readonly logPrismaQuery?: boolean @@ -57,6 +58,7 @@ export class PolicyProxyHandler implements Pr this.modelMeta, this.policy, this.zodSchemas, + this.prismaModule, this.user, this.shouldLogQuery ); @@ -73,20 +75,28 @@ export class PolicyProxyHandler implements Pr findUnique(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, '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); } findUniqueOrThrow(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, '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); @@ -216,10 +226,14 @@ export class PolicyProxyHandler implements Pr async create(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, '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'); @@ -469,10 +483,14 @@ export class PolicyProxyHandler implements Pr async createMany(args: { data: any; skipDuplicates?: boolean }) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, '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'); @@ -576,13 +594,21 @@ export class PolicyProxyHandler implements Pr async update(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, '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, '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); @@ -967,10 +993,14 @@ export class PolicyProxyHandler implements Pr async updateMany(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, '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'); @@ -1024,16 +1054,28 @@ export class PolicyProxyHandler implements Pr async upsert(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, '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, '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, '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'); @@ -1077,10 +1119,14 @@ export class PolicyProxyHandler implements Pr async delete(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, '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'); @@ -1133,7 +1179,7 @@ export class PolicyProxyHandler implements Pr async aggregate(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } args = this.utils.clone(args); @@ -1149,7 +1195,7 @@ export class PolicyProxyHandler implements Pr async groupBy(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } args = this.utils.clone(args); @@ -1193,7 +1239,7 @@ export class PolicyProxyHandler implements Pr args = { create: {}, update: {}, delete: {} }; } else { if (typeof args !== 'object') { - throw prismaClientValidationError(this.prisma, 'argument must be an object'); + throw prismaClientValidationError(this.prisma, this.prismaModule, 'argument must be an object'); } if (Object.keys(args).length === 0) { // include all @@ -1254,6 +1300,7 @@ export class PolicyProxyHandler implements Pr this.policy, this.modelMeta, this.zodSchemas, + this.prismaModule, model, this.user, this.logPrismaQuery diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index 678f777ef..497379cc3 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -4,7 +4,6 @@ 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 { hasAllFields } from '../../validation'; import { ErrorTransformer, makeProxy } from '../proxy'; @@ -25,12 +24,12 @@ export interface WithPolicyOptions extends CommonEnhancementOptions { /** * Policy definition */ - policy?: PolicyDef; + policy: PolicyDef; /** * Model metadata */ - modelMeta?: ModelMeta; + modelMeta: ModelMeta; /** * Zod schemas for validation @@ -46,6 +45,11 @@ export interface WithPolicyOptions extends CommonEnhancementOptions { * Hook for transforming errors before they are thrown to the caller. */ errorTransformer?: ErrorTransformer; + + /** + * The Node module that contains PrismaClient + */ + prismaModule: any; } /** @@ -58,8 +62,8 @@ export interface WithPolicyOptions extends CommonEnhancementOptions { */ export function withPolicy( prisma: DbClient, - context?: WithPolicyContext, - options?: WithPolicyOptions + options: WithPolicyOptions, + context?: WithPolicyContext ): DbClient { if (!prisma) { throw new Error('Invalid prisma instance'); @@ -72,9 +76,9 @@ export function withPolicy( ); } - const _policy = options?.policy ?? getDefaultPolicy(options?.loadPath); - const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath); - const _zodSchemas = options?.zodSchemas ?? getDefaultZodSchemas(options?.loadPath); + const _policy = options.policy; + const _modelMeta = options.modelMeta; + const _zodSchemas = options?.zodSchemas; // validate user context const userContext = context?.user; @@ -111,6 +115,7 @@ export function withPolicy( _policy, _modelMeta, _zodSchemas, + options.prismaModule, model, context?.user, options?.logPrismaQuery diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index d6f9595b2..7b1597ccd 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -51,6 +51,7 @@ export class PolicyUtil { private readonly modelMeta: ModelMeta, private readonly policy: PolicyDef, private readonly zodSchemas: ZodSchemas | undefined, + private readonly prismaModule: any, private readonly user?: AuthUser, private readonly shouldLogQuery = false ) { @@ -1083,24 +1084,25 @@ export class PolicyUtil { return prismaClientKnownRequestError( this.db, + this.prismaModule, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, args ); } notFound(model: string) { - return prismaClientKnownRequestError(this.db, `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, message); + return prismaClientValidationError(this.db, this.prismaModule, message); } unknownError(message: string) { - return prismaClientUnknownRequestError(this.db, message, { + return prismaClientUnknownRequestError(this.db, this.prismaModule, message, { clientVersion: getVersion(), }); } diff --git a/packages/runtime/src/enhancements/preset.ts b/packages/runtime/src/enhancements/preset.ts deleted file mode 100644 index b4c29dd7b..000000000 --- a/packages/runtime/src/enhancements/preset.ts +++ /dev/null @@ -1,20 +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. - */ -export function withPresets( - prisma: DbClient, - context?: WithPolicyContext, - options?: EnhancementOptions -) { - return enhance(prisma, context, options); -} diff --git a/packages/runtime/src/enhancements/types.ts b/packages/runtime/src/enhancements/types.ts index 9c8080096..4dcfa1c1a 100644 --- a/packages/runtime/src/enhancements/types.ts +++ b/packages/runtime/src/enhancements/types.ts @@ -76,5 +76,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 73b4d42a0..ba2f9a2d8 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -import path from 'path'; import * as util from 'util'; import type { DbClientContract } from '../types'; @@ -11,68 +8,17 @@ export function formatObject(value: unknown) { return util.formatWithOptions({ depth: 20 }, value); } -let _PrismaClientValidationError: new (...args: unknown[]) => Error; -let _PrismaClientKnownRequestError: new (...args: unknown[]) => Error; -let _PrismaClientUnknownRequestError: new (...args: unknown[]) => Error; - -/* 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 - } - } - - 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; - } - } - } -} - -export function prismaClientValidationError(prisma: DbClientContract, message: string) { - if (!_PrismaClientValidationError) { - const _prisma = loadPrismaModule(prisma); - _PrismaClientValidationError = _prisma.PrismaClientValidationError; - } - throw new _PrismaClientValidationError(message, { clientVersion: prisma._clientVersion }); +// 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 }); } -export function prismaClientKnownRequestError(prisma: DbClientContract, ...args: unknown[]) { - if (!_PrismaClientKnownRequestError) { - const _prisma = loadPrismaModule(prisma); - _PrismaClientKnownRequestError = _prisma.PrismaClientKnownRequestError; - } - return new _PrismaClientKnownRequestError(...args); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function prismaClientKnownRequestError(prisma: DbClientContract, prismaModule: any, ...args: unknown[]): Error { + return new prismaModule.PrismaClientKnownRequestError(...args); } -export function prismaClientUnknownRequestError(prisma: DbClientContract, ...args: unknown[]) { - if (!_PrismaClientUnknownRequestError) { - const _prisma = loadPrismaModule(prisma); - _PrismaClientUnknownRequestError = _prisma.PrismaClientUnknownRequestError; - } - throw new _PrismaClientUnknownRequestError(...args); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function prismaClientUnknownRequestError(prismaModule: any, ...args: unknown[]): Error { + throw new prismaModule.PrismaClientUnknownRequestError(...args); } 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/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 2f0af442a..95c7de07f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -112,7 +112,7 @@ "zod-validation-error": "^1.5.0" }, "devDependencies": { - "@prisma/client": "^4.8.0", + "@prisma/client": "^5.7.1", "@types/async-exit-hook": "^2.0.0", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", @@ -124,7 +124,7 @@ "@zenstackhq/runtime": "workspace:*", "dotenv": "^16.0.3", "esbuild": "^0.15.12", - "prisma": "^4.8.0", + "prisma": "^5.7.1", "renamer": "^4.0.0", "tmp": "^0.2.1", "tsc-alias": "^1.7.0", diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 0609fd4fb..2914ae43b 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -24,6 +24,7 @@ import { getVersion } from '../utils/version-utils'; type PluginInfo = { name: string; + description?: string; provider: string; options: PluginOptions; run: PluginFunction; @@ -93,6 +94,7 @@ export class PluginRunner { plugins.push({ name: pluginName, + description: this.getPluginDescription(pluginModule), provider: pluginProvider, dependencies, options: pluginOptions, @@ -123,6 +125,7 @@ export class PluginRunner { const pluginName = this.getPluginName(pluginModule, corePlugin.provider); plugins.unshift({ name: pluginName, + description: this.getPluginDescription(pluginModule), provider: corePlugin.provider, dependencies: [], options: { schemaPath: options.schemaPath, name: pluginName, ...corePlugin.options }, @@ -153,9 +156,9 @@ export class PluginRunner { const warnings: string[] = []; let dmmf: DMMF.Document | undefined = undefined; - for (const { name, provider, run, options: pluginOptions } of plugins) { + for (const { name, description, provider, run, options: pluginOptions } of plugins) { // const start = Date.now(); - await this.runPlugin(name, run, options, pluginOptions, dmmf, warnings); + await this.runPlugin(name, description, 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 @@ -201,6 +204,13 @@ export class PluginRunner { } } + if (options.defaultPlugins) { + corePlugins.push({ + provider: '@core/enhancer', + options: { withZodSchemas: corePlugins.some((p) => p.provider === '@core/zod') }, + }); + } + // core plugins introduced by dependencies plugins.forEach((plugin) => { // TODO: generalize this @@ -251,10 +261,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,13 +281,15 @@ export class PluginRunner { private async runPlugin( name: string, + description: string | undefined, run: PluginFunction, runnerOptions: PluginRunnerOptions, options: PluginOptions, dmmf: DMMF.Document | undefined, warnings: string[] ) { - 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( 'cli:plugin:start', diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts new file mode 100644 index 000000000..f18418f9c --- /dev/null +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -0,0 +1,64 @@ +import { + PluginError, + createProject, + emitProject, + getPrismaClientImportSpec, + resolvePath, + saveProject, + type PluginFunction, +} from '@zenstackhq/sdk'; +import path from 'path'; +import { getDefaultOutputFolder } from '../plugin-utils'; + +export const name = 'Prisma Enhancer'; + +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, 'enhance.ts'); + 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; + } + + project.createSourceFile( + outFile, + `import { createEnhancement, type WithPolicyContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; +import modelMeta from './model-meta'; +import policy from './policy'; +${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} +import { Prisma } from '${getPrismaClientImportSpec(_model, output)}'; + +export function enhance(prisma: DbClient, context?: WithPolicyContext, options?: EnhancementOptions): DbClient { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: Prisma, + ...options + }, context); +} +`, + { overwrite: true } + ); + + if (!shouldCompile || options.preserveTsFiles === true) { + await saveProject(project); + } + + if (shouldCompile) { + await emitProject(project); + } +}; + +export default run; diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index e095de898..4b1be34ff 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -81,7 +81,7 @@ export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { let runtimeModulePath = require.resolve('@zenstackhq/runtime'); if (process.env.ZENSTACK_TEST === '1') { - // handling the case when running as tests, resolve relative to CWD + // handle the case when running as tests, resolve relative to CWD runtimeModulePath = path.resolve(path.join(process.cwd(), 'node_modules', '@zenstackhq', 'runtime')); } diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 3a96cf40f..c4b209aa6 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -2,6 +2,7 @@ import { PluginFunction } from '@zenstackhq/sdk'; 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); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 508e379f3..feee0f3d1 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -138,7 +138,7 @@ export default class PrismaSchemaGenerator { if (generateClient) { try { // run 'prisma generate' - await execSync(`npx prisma generate --schema "${outFile}"`, 'ignore'); + await execSync(`npx prisma generate --schema "${outFile}"`, { stdio: 'ignore' }); } catch { await this.trackPrismaSchemaError(outFile); try { diff --git a/packages/schema/src/plugins/zod/index.ts b/packages/schema/src/plugins/zod/index.ts index b2b43cb40..53a30b4e3 100644 --- a/packages/schema/src/plugins/zod/index.ts +++ b/packages/schema/src/plugins/zod/index.ts @@ -3,6 +3,7 @@ import invariant from 'tiny-invariant'; import { generate } from './generator'; export const name = 'Zod'; +export const description = 'Generating Zod schemas'; const run: PluginFunction = async (model, options, dmmf, globalOptions) => { invariant(dmmf); diff --git a/packages/schema/src/utils/exec-utils.ts b/packages/schema/src/utils/exec-utils.ts index f355ae2b4..d88e42b3d 100644 --- a/packages/schema/src/utils/exec-utils.ts +++ b/packages/schema/src/utils/exec-utils.ts @@ -1,9 +1,10 @@ -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 }); } diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 30a477026..8d295d143 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -6,9 +6,26 @@ import path from 'path'; import tmp from 'tmp'; import { loadDocument } from '../../src/cli/cli-util'; import PrismaSchemaGenerator from '../../src/plugins/prisma/schema-generator'; +import { execSync } from '../../src/utils/exec-utils'; import { loadModel } from '../utils'; 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 { @@ -32,15 +49,14 @@ describe('Prisma generator test', () => { } `); - const { name } = tmp.fileSync({ postfix: '.prisma' }); await new PrismaSchemaGenerator().generate(model, { name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', - output: name, + output: 'schema.prisma', }); - 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")'); diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index 970ce58ba..77db556b4 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -1,15 +1,11 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import type { DMMF } from '@prisma/generator-helper'; -import { getPrismaVersion } 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'; - /** * Given a ZModel and an import context directory, compute the import spec for the Prisma Client. */ @@ -91,3 +87,27 @@ export function getDMMF(options: GetDMMFOptions, defaultPrismaVersion?: string): return _getDMMF(options); } } + +/** + * Gets the installed Prisma's version + */ +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; + } + } +} diff --git a/packages/server/src/api/base.ts b/packages/server/src/api/base.ts index ba385f31c..96c547204 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 diff --git a/packages/server/src/shared.ts b/packages/server/src/shared.ts index 6001fbbaa..1a9c62119 100644 --- a/packages/server/src/shared.ts +++ b/packages/server/src/shared.ts @@ -1,4 +1,6 @@ -import { ZodSchemas, getDefaultModelMeta, getDefaultZodSchemas } from '@zenstackhq/runtime'; +/* eslint-disable @typescript-eslint/no-var-requires */ +import type { ModelMeta, PolicyDef, ZodSchemas } from '@zenstackhq/runtime'; +import path from 'path'; import { AdapterBaseOptions } from './types'; export function loadAssets(options: AdapterBaseOptions) { @@ -18,3 +20,88 @@ 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(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/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 7b084ef8a..c1b3bfd4e 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, withPolicy, type ModelMeta } from '@zenstackhq/runtime'; import { loadSchema, run } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; @@ -1882,7 +1882,7 @@ describe('REST server tests', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params); + prisma = withPolicy(params.prisma, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; @@ -1995,7 +1995,7 @@ describe('REST server tests', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params); + prisma = withPolicy(params.prisma, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; diff --git a/packages/testtools/src/package.template.json b/packages/testtools/src/package.template.json index 8ea542361..cd443d32d 100644 --- a/packages/testtools/src/package.template.json +++ b/packages/testtools/src/package.template.json @@ -7,12 +7,12 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^4.8.0", + "@prisma/client": "^5.7.1", "@zenstackhq/runtime": "file:/packages/runtime/dist", "@zenstackhq/swr": "file:/packages/plugins/swr/dist", "@zenstackhq/trpc": "file:/packages/plugins/trpc/dist", "@zenstackhq/openapi": "file:/packages/plugins/openapi/dist", - "prisma": "^4.8.0", + "prisma": "^5.7.1", "typescript": "^4.9.3", "zenstack": "file:/packages/schema/dist", "zod": "^3.22.4", diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index f69a845cc..52d0e2376 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -2,7 +2,7 @@ /* 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 { withOmit, withPassword, withPolicy, type AuthUser, type DbOperations } from '@zenstackhq/runtime'; import { getDMMF } from '@zenstackhq/sdk'; import { execSync } from 'child_process'; import * as fs from 'fs'; @@ -35,14 +35,14 @@ export type FullDbClientContract = Record & { }; export function run(cmd: string, env?: Record, cwd?: string) { - const start = Date.now(); + // const start = Date.now(); execSync(cmd, { stdio: 'pipe', encoding: 'utf-8', env: { ...process.env, DO_NOT_TRACK: '1', ...env }, cwd, }); - console.log('Execution took', Date.now() - start, 'ms', '-', cmd); + // console.log('Execution took', Date.now() - start, 'ms', '-', cmd); } function normalizePath(p: string) { @@ -224,7 +224,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { // 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(projectRoot, 'node_modules/@prisma/client')).Prisma; if (opt.pulseApiKey) { const withPulse = require(path.join(projectRoot, 'node_modules/@prisma/extension-pulse/dist/cjs')).withPulse; @@ -248,58 +248,55 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { if (options?.getPrismaOnly) { return { prisma, - Prisma, + prismaModule, projectDir: projectRoot, withPolicy: undefined as any, withOmit: undefined as any, withPassword: undefined as any, 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(projectRoot, opt.output) + : path.join(projectRoot, 'node_modules', '.zenstack'); - 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, prisma, - Prisma, withPolicy: (user?: AuthUser) => withPolicy( prisma, - { user }, - { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery } + { policy, modelMeta, zodSchemas, prismaModule, logPrismaQuery: opt.logPrismaQuery }, + { user } ), withOmit: () => withOmit(prisma, { modelMeta }), withPassword: () => withPassword(prisma, { modelMeta }), - enhance: (user?: AuthUser) => - enhance( - prisma, - { user }, - { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery } - ), + enhance: (user?: AuthUser): FullDbClientContract => + enhance(prisma, { user }, { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery }), + enhanceRaw: enhance, policy, modelMeta, zodSchemas, + prismaModule, }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1736e7bba..84ef3e88d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -391,9 +391,6 @@ importers: packages/runtime: dependencies: - '@types/bcryptjs': - specifier: ^2.4.2 - version: 2.4.2 bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -403,9 +400,6 @@ 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 @@ -443,6 +437,9 @@ 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 @@ -554,8 +551,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.7.1 + version: 5.7.1(prisma@5.7.1) '@types/async-exit-hook': specifier: ^2.0.0 version: 2.0.0 @@ -590,8 +587,8 @@ importers: specifier: ^0.15.12 version: 0.15.12 prisma: - specifier: ^4.8.0 - version: 4.16.2 + specifier: ^5.7.1 + version: 5.7.1 renamer: specifier: ^4.0.0 version: 4.0.0 @@ -3489,22 +3486,19 @@ 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.7.0: + resolution: {integrity: sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==} + engines: {node: '>=16.13'} requiresBuild: true peerDependencies: prisma: '*' peerDependenciesMeta: prisma: optional: true - dependencies: - '@prisma/engines-version': 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 - prisma: 4.16.2 dev: true - /@prisma/client@5.7.0: - resolution: {integrity: sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==} + /@prisma/client@5.7.1(prisma@5.7.1): + resolution: {integrity: sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==} engines: {node: '>=16.13'} requiresBuild: true peerDependencies: @@ -3512,6 +3506,8 @@ packages: peerDependenciesMeta: prisma: optional: true + dependencies: + prisma: 5.7.1 dev: true /@prisma/debug@4.16.2: @@ -3538,17 +3534,22 @@ packages: resolution: {integrity: sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==} dev: false - /@prisma/engines-version@4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81: - resolution: {integrity: sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==} + /@prisma/debug@5.7.1: + resolution: {integrity: sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==} dev: true /@prisma/engines-version@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9: resolution: {integrity: sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==} dev: false + /@prisma/engines-version@5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5: + resolution: {integrity: sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==} + dev: true + /@prisma/engines@4.16.2: resolution: {integrity: sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==} requiresBuild: true + dev: false /@prisma/engines@5.0.0: resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==} @@ -3565,6 +3566,16 @@ packages: '@prisma/get-platform': 5.7.0 dev: false + /@prisma/engines@5.7.1: + resolution: {integrity: sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==} + requiresBuild: true + dependencies: + '@prisma/debug': 5.7.1 + '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 + '@prisma/fetch-engine': 5.7.1 + '@prisma/get-platform': 5.7.1 + dev: true + /@prisma/fetch-engine@4.16.2: resolution: {integrity: sha512-lnCnHcOaNn0kw8qTJbVcNhyfIf5Lus2GFXbj3qpkdKEIB9xLgqkkuTP+35q1xFaqwQ0vy4HFpdRUpFP7njE15g==} dependencies: @@ -3623,6 +3634,14 @@ packages: '@prisma/get-platform': 5.7.0 dev: false + /@prisma/fetch-engine@5.7.1: + resolution: {integrity: sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==} + dependencies: + '@prisma/debug': 5.7.1 + '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 + '@prisma/get-platform': 5.7.1 + dev: true + /@prisma/generator-helper@4.16.2: resolution: {integrity: sha512-bMOH7y73Ui7gpQrioFeavMQA+Tf8ksaVf8Nhs9rQNzuSg8SSV6E9baczob0L5KGZTSgYoqnrRxuo03kVJYrnIg==} dependencies: @@ -3691,6 +3710,12 @@ packages: '@prisma/debug': 5.7.0 dev: false + /@prisma/get-platform@5.7.1: + resolution: {integrity: sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==} + dependencies: + '@prisma/debug': 5.7.1 + dev: true + /@prisma/internals@4.16.2: resolution: {integrity: sha512-/3OiSADA3RRgsaeEE+MDsBgL6oAMwddSheXn6wtYGUnjERAV/BmF5bMMLnTykesQqwZ1s8HrISrJ0Vf6cjOxMg==} dependencies: @@ -4594,6 +4619,7 @@ 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==} @@ -12474,13 +12500,13 @@ packages: hasBin: true dev: true - /prisma@4.16.2: - resolution: {integrity: sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==} - engines: {node: '>=14.17'} + /prisma@5.7.1: + resolution: {integrity: sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==} + engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 4.16.2 + '@prisma/engines': 5.7.1 dev: true /process-nextick-args@2.0.1: 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/tests/integration/test-run/package.json b/tests/integration/test-run/package.json index d4e05bd29..31497a99a 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.0.0", "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "^4.8.0", + "prisma": "^5.0.0", "react": "^18.2.0", "swr": "^1.3.0", "typescript": "^4.9.3", diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 40a7981c8..c8e1b0e6b 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -86,26 +86,6 @@ 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' }); diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts index 96492b286..987752bd2 100644 --- a/tests/integration/tests/cli/init.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -24,9 +24,12 @@ describe('CLI init command 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(); @@ -42,9 +45,12 @@ describe('CLI init command tests', () => { // Disabled because it blows up memory on MAC, not sure why ... // eslint-disable-next-line jest/no-disabled-tests it.skip('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(); @@ -58,9 +64,12 @@ describe('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(); @@ -74,9 +83,12 @@ describe('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 005a0f69b..4f93106e8 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -131,12 +131,10 @@ describe('CLI Plugins Tests', () => { }`, `plugin meta { provider = '@core/model-meta' - output = 'model-meta' } `, `plugin policy { provider = '@core/access-policy' - output = 'policy' }`, `plugin tanstack { provider = '@zenstackhq/tanstack-query' 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 61d44b440..82e3ec2c8 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'; @@ -77,32 +76,4 @@ describe('Omit test', () => { expect(e.profile.image).toBeUndefined(); }); }); - - it('customization', async () => { - const { prisma } = await loadSchema(model, { getPrismaOnly: true, output: './zen' }); - - const db = withOmit(prisma, { loadPath: './zen' }); - const r = await db.user.create({ - include: { profile: true }, - data: { - id: '1', - password: 'abc123', - profile: { create: { image: 'an image' } }, - }, - }); - expect(r.password).toBeUndefined(); - expect(r.profile.image).toBeUndefined(); - - const db1 = withOmit(prisma, { modelMeta: require(path.resolve('./zen/model-meta')).default }); - const r1 = await db1.user.create({ - include: { profile: true }, - data: { - id: '2', - password: 'abc123', - profile: { create: { image: 'an image' } }, - }, - }); - expect(r1.password).toBeUndefined(); - expect(r1.profile.image).toBeUndefined(); - }); }); 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..737338666 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'; @@ -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/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/options.test.ts b/tests/integration/tests/enhancements/with-policy/options.test.ts index 2c661ceb4..79adb8c49 100644 --- a/tests/integration/tests/enhancements/with-policy/options.test.ts +++ b/tests/integration/tests/enhancements/with-policy/options.test.ts @@ -3,18 +3,8 @@ 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()) @@ -25,7 +15,8 @@ describe('Password test', () => { { getPrismaOnly: true, output: './zen' } ); - const db = withPolicy(prisma, undefined, { loadPath: './zen' }); + const enhance = require(path.join(projectDir, 'zen/enhance')).enhance; + const db = enhance(prisma, { loadPath: './zen' }); await expect( db.foo.create({ data: { x: 0 }, @@ -34,7 +25,7 @@ describe('Password test', () => { }); it('overrides', async () => { - const { prisma } = await loadSchema( + const { prisma, projectDir } = await loadSchema( ` model Foo { id String @id @default(cuid()) @@ -45,9 +36,10 @@ describe('Password test', () => { { getPrismaOnly: true, output: './zen' } ); - const db = withPolicy(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/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json index 1461849f1..7b93ec340 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.0.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.0.0" } } diff --git a/tests/integration/tests/frameworks/trpc/generation.test.ts b/tests/integration/tests/frameworks/trpc/generation.test.ts index 5e15d9943..3c867bc0f 100644 --- a/tests/integration/tests/frameworks/trpc/generation.test.ts +++ b/tests/integration/tests/frameworks/trpc/generation.test.ts @@ -35,7 +35,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..8445cc451 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.0.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.0.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' From d2d07aee1bc8d549cdcef0a5d1e54731a10b0ae5 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 9 Jan 2024 17:55:34 +0800 Subject: [PATCH 003/127] refactor: merge core plugins (#939) --- CONTRIBUTING.md | 2 +- .../plugins/openapi/src/generator-base.ts | 8 +- packages/plugins/openapi/src/rpc-generator.ts | 2 +- packages/plugins/swr/src/generator.ts | 2 - packages/plugins/swr/tests/swr.test.ts | 7 +- .../plugins/tanstack-query/src/generator.ts | 9 +- .../tanstack-query/tests/plugin.test.ts | 12 +- packages/plugins/trpc/tests/trpc.test.ts | 25 ++- packages/schema/src/cli/plugin-runner.ts | 180 +++++++++--------- .../schema/src/plugins/access-policy/index.ts | 10 - .../schema/src/plugins/enhancer/enhancer.ts | 29 +++ packages/schema/src/plugins/enhancer/index.ts | 42 ++-- .../schema/src/plugins/enhancer/model-meta.ts | 13 ++ .../policy}/expression-writer.ts | 6 +- .../src/plugins/enhancer/policy/index.ts | 8 + .../policy}/policy-guard-generator.ts | 42 +--- .../schema/src/plugins/model-meta/index.ts | 42 ---- packages/schema/src/plugins/plugin-utils.ts | 9 + packages/schema/src/plugins/zod/generator.ts | 2 +- .../tests/generator/expression-writer.test.ts | 2 +- packages/schema/tests/schema/cal-com.zmodel | 9 +- packages/sdk/src/model-meta-generator.ts | 13 +- packages/sdk/src/types.ts | 19 +- packages/sdk/src/utils.ts | 10 +- packages/testtools/src/schema.ts | 16 +- tests/integration/tests/cli/generate.test.ts | 8 +- tests/integration/tests/cli/plugins.test.ts | 8 +- 27 files changed, 242 insertions(+), 293 deletions(-) delete mode 100644 packages/schema/src/plugins/access-policy/index.ts create mode 100644 packages/schema/src/plugins/enhancer/enhancer.ts create mode 100644 packages/schema/src/plugins/enhancer/model-meta.ts rename packages/schema/src/plugins/{access-policy => enhancer/policy}/expression-writer.ts (99%) create mode 100644 packages/schema/src/plugins/enhancer/policy/index.ts rename packages/schema/src/plugins/{access-policy => enhancer/policy}/policy-guard-generator.ts (96%) delete mode 100644 packages/schema/src/plugins/model-meta/index.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1f733983..2b16adebf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ 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` diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index d00c081fc..1a46fa528 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -2,9 +2,10 @@ import type { DMMF } from '@prisma/generator-helper'; import { PluginError, PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; 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'; @@ -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/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 13bb91272..c551a8aef 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -721,7 +721,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}`); } } diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index e074b603c..3a47a1c87 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -38,8 +38,6 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. await generateModelMeta(project, models, { output: path.join(outDir, '__model_meta.ts'), - compile: false, - preserveTsFiles: true, generateAttributes: false, }); diff --git a/packages/plugins/swr/tests/swr.test.ts b/packages/plugins/swr/tests/swr.test.ts index 76db29b49..9759aee2d 100644 --- a/packages/plugins/swr/tests/swr.test.ts +++ b/packages/plugins/swr/tests/swr.test.ts @@ -59,7 +59,12 @@ ${sharedModel} { provider: 'postgresql', pushDb: false, - extraDependencies: [`${origDir}/dist`, 'react@18.2.0', '@types/react@18.2.0', 'swr@^2'], + extraDependencies: [ + path.resolve(__dirname, '../dist'), + 'react@18.2.0', + '@types/react@18.2.0', + 'swr@^2', + ], compile: true, } ); diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 1e39253fc..9ad261c21 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -34,21 +34,16 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. 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'; 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"`); } await generateModelMeta(project, models, { output: path.join(outDir, '__model_meta.ts'), - compile: false, - preserveTsFiles: true, generateAttributes: false, }); diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index 49a99df94..c87e2a38f 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -61,7 +61,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@4.29.7'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -83,7 +83,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@^5.0.0'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -104,7 +104,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@4.37.0'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -126,7 +126,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@latest'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -147,7 +147,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); @@ -169,7 +169,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@^5.0.0'], - copyDependencies: [`${origDir}/dist`], + copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, } ); diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index cf43c9a49..757e7e182 100644 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ b/packages/plugins/trpc/tests/trpc.test.ts @@ -56,7 +56,7 @@ model Foo { { provider: 'postgresql', pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -98,7 +98,7 @@ model Foo { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -128,7 +128,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -153,7 +153,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -183,7 +183,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, customSchemaFilePath: 'zenstack/schema.zmodel', @@ -229,7 +229,12 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/react-query'], + extraDependencies: [ + path.resolve(__dirname, '../dist'), + '@trpc/client', + '@trpc/server', + '@trpc/react-query', + ], compile: true, fullZod: true, } @@ -249,7 +254,7 @@ model Post { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/next'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server', '@trpc/next'], compile: true, fullZod: true, } @@ -279,7 +284,7 @@ model post_item { `, { pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, fullZod: true, } @@ -326,7 +331,7 @@ model Foo { { addPrelude: false, pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, } ); @@ -397,7 +402,7 @@ model Foo { { addPrelude: false, pushDb: false, - extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'], + extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client', '@trpc/server'], compile: true, } ); diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 2914ae43b..3e73932a1 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -8,16 +8,17 @@ import { getLiteral, getLiteralArray, hasValidationAttributes, + OptionValue, + PluginDeclaredOptions, PluginError, PluginFunction, - PluginOptions, resolvePath, } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'fs'; import ora from 'ora'; import path from 'path'; -import { ensureDefaultOutputFolder } from '../plugins/plugin-utils'; +import { CorePlugins, ensureDefaultOutputFolder } from '../plugins/plugin-utils'; import { getDefaultPrismaOutputFile } from '../plugins/prisma/schema-generator'; import telemetry from '../telemetry'; import { getVersion } from '../utils/version-utils'; @@ -26,7 +27,7 @@ type PluginInfo = { name: string; description?: string; provider: string; - options: PluginOptions; + options: PluginDeclaredOptions; run: PluginFunction; dependencies: string[]; module: any; @@ -47,16 +48,16 @@ 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)); + const pluginDecls = runnerOptions.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - let prismaOutput = getDefaultPrismaOutputFile(options.schemaPath); + let prismaOutput = getDefaultPrismaOutputFile(runnerOptions.schemaPath); for (const pluginDecl of pluginDecls) { const pluginProvider = this.getPluginProvider(pluginDecl); @@ -69,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}`); @@ -81,19 +82,20 @@ 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, @@ -104,41 +106,17 @@ export class PluginRunner { if (pluginProvider === '@core/prisma' && typeof pluginOptions.output === 'string') { // record custom prisma output path - prismaOutput = resolvePath(pluginOptions.output, pluginOptions); + prismaOutput = resolvePath(pluginOptions.output, { schemaPath: runnerOptions.schemaPath }); } } - // 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, - description: this.getPluginDescription(pluginModule), - 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 allPlugins = this.calculateAllPlugins(runnerOptions, plugins); // 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, @@ -148,7 +126,7 @@ export class PluginRunner { } } - if (plugins.length === 0) { + if (allPlugins.length === 0) { console.log(colors.yellow('No plugins configured.')); return; } @@ -156,9 +134,9 @@ export class PluginRunner { const warnings: string[] = []; let dmmf: DMMF.Document | undefined = undefined; - for (const { name, description, provider, run, options: pluginOptions } of plugins) { + for (const { name, description, provider, run, options: pluginOptions } of allPlugins) { // const start = Date.now(); - await this.runPlugin(name, description, run, options, pluginOptions, dmmf, warnings); + await this.runPlugin(name, description, run, runnerOptions, pluginOptions, dmmf, warnings); // console.log(`✅ Plugin ${colors.bold(name)} (${provider}) completed in ${Date.now() - start}ms`); if (provider === '@core/prisma') { // load prisma DMMF @@ -174,44 +152,56 @@ export class PluginRunner { 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/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 (options.defaultPlugins) { - corePlugins.push({ - provider: '@core/enhancer', - options: { withZodSchemas: corePlugins.some((p) => p.provider === '@core/zod') }, - }); + if ( + !corePlugins.some((p) => p.provider === CorePlugins.Zod) && + (options.defaultPlugins || plugins.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 })); + } + + // 3. @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 + // collect core plugins introduced by dependencies plugins.forEach((plugin) => { // TODO: generalize this const isTrpcPlugin = @@ -227,7 +217,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 && @@ -239,21 +231,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, ...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) { @@ -284,7 +294,7 @@ export class PluginRunner { description: string | undefined, run: PluginFunction, runnerOptions: PluginRunnerOptions, - options: PluginOptions, + options: PluginDeclaredOptions, dmmf: DMMF.Document | undefined, warnings: string[] ) { @@ -300,7 +310,7 @@ export class PluginRunner { options, }, async () => { - let result = run(runnerOptions.schema, options, dmmf, { + let result = run(runnerOptions.schema, { ...options, schemaPath: runnerOptions.schemaPath }, dmmf, { output: runnerOptions.output, compile: runnerOptions.compile, }); @@ -319,7 +329,7 @@ export class PluginRunner { } } - 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')); @@ -329,14 +339,14 @@ 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); } } 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/enhancer.ts b/packages/schema/src/plugins/enhancer/enhancer.ts new file mode 100644 index 000000000..09360254b --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhancer.ts @@ -0,0 +1,29 @@ +import { getPrismaClientImportSpec, 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, 'enhance.ts'); + + project.createSourceFile( + outFile, + `import { createEnhancement, type WithPolicyContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; +import modelMeta from './model-meta'; +import policy from './policy'; +${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} +import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; + +export function enhance(prisma: DbClient, context?: WithPolicyContext, options?: EnhancementOptions): DbClient { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: Prisma, + ...options + }, context); +} +`, + { overwrite: true } + ); +} diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index f18418f9c..45f3ceb35 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -2,26 +2,31 @@ import { PluginError, createProject, emitProject, - getPrismaClientImportSpec, resolvePath, saveProject, type PluginFunction, } from '@zenstackhq/sdk'; -import path from 'path'; import { getDefaultOutputFolder } from '../plugin-utils'; +import { generate as generateEnhancer } from './enhancer'; +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 output = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); - if (!output) { - throw new PluginError(options.name, `Unable to determine output path, not running plugin`); +const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { + let ourDir = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); + if (!ourDir) { + throw new PluginError(name, `Unable to determine output path, not running plugin`); } + ourDir = resolvePath(ourDir, options); - output = resolvePath(output, options); - const outFile = path.join(output, 'enhance.ts'); const project = createProject(); + await generateModelMeta(model, options, project, ourDir); + await generatePolicy(model, options, project, ourDir); + await generateEnhancer(model, options, project, ourDir); + let shouldCompile = true; if (typeof options.compile === 'boolean') { // explicit override @@ -31,27 +36,6 @@ const run: PluginFunction = async (_model, options, _dmmf, globalOptions) => { shouldCompile = globalOptions.compile; } - project.createSourceFile( - outFile, - `import { createEnhancement, type WithPolicyContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; -import modelMeta from './model-meta'; -import policy from './policy'; -${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} -import { Prisma } from '${getPrismaClientImportSpec(_model, output)}'; - -export function enhance(prisma: DbClient, context?: WithPolicyContext, options?: EnhancementOptions): DbClient { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: Prisma, - ...options - }, context); -} -`, - { overwrite: true } - ); - if (!shouldCompile || options.preserveTsFiles === true) { await saveProject(project); } diff --git a/packages/schema/src/plugins/enhancer/model-meta.ts b/packages/schema/src/plugins/enhancer/model-meta.ts new file mode 100644 index 000000000..541106e24 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/model-meta.ts @@ -0,0 +1,13 @@ +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); + await generateModelMeta(project, dataModels, { + output: outFile, + generateAttributes: true, + }); +} diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts similarity index 99% rename from packages/schema/src/plugins/access-policy/expression-writer.ts rename to packages/schema/src/plugins/enhancer/policy/expression-writer.ts index 2ab3e2bdd..0cc80c7ea 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -26,12 +26,12 @@ import { } from '@zenstackhq/sdk'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter } from 'ts-morph'; -import { name } from '.'; -import { getIdFields, isAuthInvocation } from '../../utils/ast-utils'; +import { name } from '..'; +import { getIdFields, isAuthInvocation } from '../../../utils/ast-utils'; import { TypeScriptExpressionTransformer, TypeScriptExpressionTransformerError, -} from '../../utils/typescript-expression-transformer'; +} from '../../../utils/typescript-expression-transformer'; type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<='; 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 96% 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 2025c3d5c..e5017383d 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -31,12 +31,9 @@ import { import { ExpressionContext, PluginError, - PluginGlobalOptions, PluginOptions, RUNTIME_PACKAGE, analyzePolicies, - createProject, - emitProject, getAttributeArg, getAuthModel, getDataModels, @@ -48,35 +45,26 @@ import { isForeignKeyField, isFromStdlib, isFutureExpr, - resolvePath, resolved, - saveProject, } from '@zenstackhq/sdk'; 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 { FunctionDeclaration, Project, 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'; +} from '../../../utils/typescript-expression-transformer'; +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 */'); @@ -156,22 +144,6 @@ 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); - } } // Generates a { select: ... } object to select `auth()` fields used in policy rules 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 4b1be34ff..f4f521fdc 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -95,3 +95,12 @@ export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { 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/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 2727a781f..19ce18a3b 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -26,7 +26,7 @@ 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'; +import { getFieldSchemaDefault, makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; export async function generate( model: Model, diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index f9baa0de9..a4cc6ae5f 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'; describe('Expression Writer Tests', () => { 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/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 41a0ea0c9..da0ba96dd 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -14,7 +14,6 @@ import type { RuntimeAttribute } from '@zenstackhq/runtime'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter, Project, VariableDeclarationKind } from 'ts-morph'; import { - emitProject, getAttribute, getAttributeArg, getAttributeArgs, @@ -26,13 +25,10 @@ import { isForeignKeyField, isIdField, resolved, - saveProject, } from '.'; export type ModelMetaGeneratorOptions = { output: string; - compile: boolean; - preserveTsFiles: boolean; generateAttributes: boolean; }; @@ -44,14 +40,7 @@ export async function generate(project: Project, models: DataModel[], options: M declarations: [{ name: 'metadata', initializer: (writer) => generateModelMetadata(models, writer, options) }], }); sf.addStatements('export default metadata;'); - - if (!options.compile || options.preserveTsFiles) { - // save ts files - await saveProject(project); - } - if (options.compile) { - await emitProject(project); - } + return sf; } function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter, options: ModelMetaGeneratorOptions) { diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c19fdfc42..9fbbd5553 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -9,23 +9,18 @@ export type OptionValue = string | number | boolean; /** * Plugin configuration options */ -export type PluginOptions = { +export type PluginDeclaredOptions = { /*** * The provider package */ - provider?: string; - - /** - * The path of the ZModel schema - */ - schemaPath: string; - - /** - * The name of the plugin - */ - name: string; + provider: string; } & Record; +/** + * Plugin configuration options for execution + */ +export type PluginOptions = { schemaPath: string } & PluginDeclaredOptions; + /** * Global options that apply to all plugins */ diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index abec6092b..a4b5039b5 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -29,7 +29,7 @@ import { } from '@zenstackhq/language/ast'; import path from 'path'; import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; -import { PluginError, PluginOptions } from './types'; +import { PluginDeclaredOptions, PluginError, PluginOptions } from './types'; /** * Gets data models that are not ignored @@ -280,7 +280,7 @@ export function resolvePath(_path: string, options: Pick(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}`); @@ -288,8 +288,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') { @@ -304,7 +304,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` ); } } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 52d0e2376..fd7df30b7 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -84,13 +84,8 @@ generator js { previewFeatures = ['clientExtensions'] } -plugin meta { - provider = '@core/model-meta' - preserveTsFiles = true -} - -plugin policy { - provider = '@core/access-policy' +plugin enhancer { + provider = '@core/enhancer' preserveTsFiles = true } @@ -321,7 +316,12 @@ 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, + }); const prismaContent = fs.readFileSync(prismaFile, { encoding: 'utf-8' }); diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index c8e1b0e6b..85fe76a53 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -116,8 +116,8 @@ model Post { fs.appendFileSync( 'schema.zmodel', ` - plugin policy { - provider = '@core/access-policy' + plugin enhancer { + provider = '@core/enhancer' } ` ); @@ -133,8 +133,8 @@ model Post { fs.appendFileSync( 'schema.zmodel', ` - plugin policy { - provider = '@core/access-policy' + plugin enhancer { + provider = '@core/enhancer' } ` ); diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 4f93106e8..19dfb4dce 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -129,12 +129,8 @@ describe('CLI Plugins Tests', () => { output = 'prisma/my.prisma' generateClient = true }`, - `plugin meta { - provider = '@core/model-meta' - } - `, - `plugin policy { - provider = '@core/access-policy' + `plugin enhancer { + provider = '@core/enhancer' }`, `plugin tanstack { provider = '@zenstackhq/tanstack-query' From e6e21715d750d7eaa0d869fae459aec6ada4fc59 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 10 Jan 2024 10:59:57 +0800 Subject: [PATCH 004/127] refactor: unify enhancement API to `enhance` (#940) --- CONTRIBUTING.md | 2 +- packages/README.md | 4 +- packages/runtime/src/constants.ts | 5 - packages/runtime/src/enhancements/enhance.ts | 123 ++++++++++++++++-- packages/runtime/src/enhancements/index.ts | 4 - packages/runtime/src/enhancements/omit.ts | 18 +-- packages/runtime/src/enhancements/password.ts | 18 +-- .../src/enhancements/policy/handler.ts | 51 +++----- .../runtime/src/enhancements/policy/index.ts | 94 ++----------- .../src/enhancements/policy/policy-utils.ts | 23 +++- packages/runtime/src/enhancements/proxy.ts | 9 +- .../schema/src/plugins/enhancer/enhancer.ts | 4 +- packages/server/tests/api/rest.test.ts | 6 +- packages/testtools/src/schema.ts | 24 ++-- .../enhancements/with-omit/with-omit.test.ts | 4 +- .../with-password/with-password.test.ts | 4 +- .../enhancements/with-policy/auth.test.ts | 60 ++++----- .../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 | 96 +++++++------- .../with-policy/field-validation.test.ts | 4 +- .../with-policy/fluent-api.test.ts | 4 +- .../with-policy/multi-field-unique.test.ts | 12 +- .../with-policy/multi-id-fields.test.ts | 20 +-- .../with-policy/nested-to-many.test.ts | 36 ++--- .../with-policy/nested-to-one.test.ts | 36 ++--- .../enhancements/with-policy/options.test.ts | 1 - .../with-policy/petstore-sample.test.ts | 4 +- .../with-policy/post-update.test.ts | 44 +++---- .../enhancements/with-policy/postgres.test.ts | 4 +- .../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 | 12 +- .../with-policy/unique-as-id.test.ts | 12 +- .../enhancements/with-policy/view.test.ts | 4 +- .../integration/tests/misc/stacktrace.test.ts | 4 +- .../tests/regression/issue-665.test.ts | 10 +- .../tests/regression/issues.test.ts | 28 ++-- 46 files changed, 459 insertions(+), 453 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b16adebf..1eed5723d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ The `zenstack` CLI and ZModel VSCode extension implementation. The package also ### `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/packages/README.md b/packages/README.md index 2104ff0eb..d4f076584 100644 --- a/packages/README.md +++ b/packages/README.md @@ -51,12 +51,12 @@ At runtime, transparent proxies are created around Prisma clients for intercepti // Next.js example: pages/api/model/[...path].ts import { requestHandler } from '@zenstackhq/next'; -import { withPolicy } from '@zenstackhq/runtime'; +import { enhance } from '@zenstackhq/runtime'; import { getSessionUser } from '@lib/auth'; import { prisma } from '@lib/db'; export default requestHandler({ - getPrisma: (req, res) => withPolicy(prisma, { user: getSessionUser(req, res) }), + getPrisma: (req, res) => enhance(prisma, { user: getSessionUser(req, res) }), }); ``` diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts index bd191924a..c381a5a88 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -53,11 +53,6 @@ export enum PrismaErrorCode { DEPEND_ON_RECORD_NOT_FOUND = 'P2025', } -/** - * Field name for storing in-transaction flag - */ -export const PRISMA_TX_FLAG = '$__zenstack_tx'; - /** * Field name for getting current enhancer */ diff --git a/packages/runtime/src/enhancements/enhance.ts b/packages/runtime/src/enhancements/enhance.ts index 977a8e950..a82640905 100644 --- a/packages/runtime/src/enhancements/enhance.ts +++ b/packages/runtime/src/enhancements/enhance.ts @@ -1,30 +1,123 @@ -import { withOmit, WithOmitOptions } from './omit'; -import { withPassword, WithPasswordOptions } from './password'; -import { withPolicy, WithPolicyContext, WithPolicyOptions } from './policy'; +import semver from 'semver'; +import { PRISMA_MINIMUM_VERSION } from '../constants'; +import { ModelMeta } from '../cross'; +import type { AuthUser } from '../types'; +import { withOmit } from './omit'; +import { withPassword } from './password'; +import { withPolicy } from './policy'; +import type { ErrorTransformer } from './proxy'; +import type { PolicyDef, ZodSchemas } from './types'; /** - * Options @see enhance + * Kinds of enhancements to `PrismaClient` */ -export type EnhancementOptions = WithPolicyOptions & WithPasswordOptions & WithOmitOptions; +export enum EnhancementKind { + Password = 'password', + Omit = 'omit', + Policy = 'policy', +} + +/** + * 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 {@link createEnhancement} + */ +export type EnhancementOptions = { + /** + * Policy definition + */ + policy: PolicyDef; + + /** + * Model metadata + */ + modelMeta: ModelMeta; + + /** + * Zod schemas for validation + */ + zodSchemas?: ZodSchemas; + + /** + * Whether to log Prisma query + */ + logPrismaQuery?: boolean; + + /** + * The Node module that contains PrismaClient + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prismaModule: any; + + /** + * The kinds of enhancements to apply. By default all enhancements are applied. + */ + kinds?: EnhancementKind[]; + + /** + * 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; +}; + +/** + * Context for creating enhanced `PrismaClient` + */ +export type EnhancementContext = { + user?: AuthUser; +}; let hasPassword: boolean | undefined = undefined; let hasOmit: boolean | undefined = undefined; /** - * Gets a Prisma client enhanced with all essential behaviors, including access + * Gets a Prisma client enhanced with all enhancement 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 context Context. * @param options Options. */ export function createEnhancement( prisma: DbClient, options: EnhancementOptions, - context?: WithPolicyContext + 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}".` + ); + } + let result = prisma; if (hasPassword === undefined || hasOmit === undefined) { @@ -33,18 +126,22 @@ export function createEnhancement( hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit')); } - if (hasPassword) { + const kinds = options.kinds ?? [EnhancementKind.Password, EnhancementKind.Omit, EnhancementKind.Policy]; + + if (hasPassword && kinds.includes(EnhancementKind.Password)) { // @password proxy result = withPassword(result, options); } - if (hasOmit) { + if (hasOmit && kinds.includes(EnhancementKind.Omit)) { // @omit proxy result = withOmit(result, options); } // policy proxy - result = withPolicy(result, options, context); + if (kinds.includes(EnhancementKind.Policy)) { + result = withPolicy(result, options, context); + } return result; } diff --git a/packages/runtime/src/enhancements/index.ts b/packages/runtime/src/enhancements/index.ts index 51f304657..ebdd16a08 100644 --- a/packages/runtime/src/enhancements/index.ts +++ b/packages/runtime/src/enhancements/index.ts @@ -1,8 +1,4 @@ export * from '../cross'; export * from './enhance'; -export * from './omit'; -export * from './password'; -export * from './policy'; export * from './types'; export * from './utils'; -export * from './where-visitor'; diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index 22b002309..d3e566ebb 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -3,23 +3,15 @@ import { enumerate, getModelFields, resolveField, type ModelMeta } from '../cross'; import { DbClientContract } from '../types'; +import { EnhancementOptions } from './enhance'; import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; -import { CommonEnhancementOptions } from './types'; /** - * Options for @see withOmit + * Gets an enhanced Prisma client that supports `@omit` attribute. + * + * @private */ -export interface WithOmitOptions extends CommonEnhancementOptions { - /** - * Model metadata - */ - modelMeta: ModelMeta; -} - -/** - * Gets an enhanced Prisma client that supports @omit attribute. - */ -export function withOmit(prisma: DbClient, options: WithOmitOptions): DbClient { +export function withOmit(prisma: DbClient, options: EnhancementOptions): DbClient { return makeProxy( prisma, options.modelMeta, diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index 5846bc8dc..d10e1d9a7 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -5,23 +5,15 @@ import { hash } from 'bcryptjs'; import { DEFAULT_PASSWORD_SALT_LENGTH } from '../constants'; import { NestedWriteVisitor, type ModelMeta, type PrismaWriteActionType } from '../cross'; import { DbClientContract } from '../types'; +import { EnhancementOptions } from './enhance'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; -import { CommonEnhancementOptions } from './types'; /** - * Options for @see withPassword + * Gets an enhanced Prisma client that supports `@password` attribute. + * + * @private */ -export interface WithPasswordOptions extends CommonEnhancementOptions { - /** - * Model metadata - */ - modelMeta: ModelMeta; -} - -/** - * Gets an enhanced Prisma client that supports @password attribute. - */ -export function withPassword(prisma: DbClient, options: WithPasswordOptions): DbClient { +export function withPassword(prisma: DbClient, options: EnhancementOptions): DbClient { return makeProxy( prisma, options.modelMeta, diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 748877805..bc8214698 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -4,7 +4,7 @@ import { lowerCaseFirst } from 'lower-case-first'; import invariant from 'tiny-invariant'; import { upperCaseFirst } from 'upper-case-first'; import { fromZodError } from 'zod-validation-error'; -import { CrudFailureReason, PRISMA_TX_FLAG } from '../../constants'; +import { CrudFailureReason } from '../../constants'; import { ModelDataVisitor, NestedWriteVisitor, @@ -16,9 +16,9 @@ import { type FieldInfo, type ModelMeta, } from '../../cross'; -import { AuthUser, DbClientContract, DbOperations, PolicyOperationKind } from '../../types'; +import { DbClientContract, DbOperations, PolicyOperationKind } from '../../types'; +import type { EnhancementContext, EnhancementOptions } from '../enhance'; import { PrismaProxyHandler } from '../proxy'; -import type { PolicyDef, ZodSchemas } from '../types'; import { formatObject, prismaClientValidationError } from '../utils'; import { Logger } from './logger'; import { PolicyUtil } from './policy-utils'; @@ -41,28 +41,22 @@ export class PolicyProxyHandler implements Pr private readonly logger: Logger; private readonly utils: PolicyUtil; private readonly model: string; + private readonly modelMeta: ModelMeta; + private readonly prismaModule: any; + private readonly logPrismaQuery?: boolean; constructor( private readonly prisma: DbClient, - private readonly policy: PolicyDef, - private readonly modelMeta: ModelMeta, - private readonly zodSchemas: ZodSchemas | undefined, - private readonly prismaModule: any, model: string, - private readonly user?: AuthUser, - private readonly logPrismaQuery?: boolean + private readonly options: EnhancementOptions, + private readonly context?: EnhancementContext ) { this.logger = new Logger(prisma); - this.utils = new PolicyUtil( - this.prisma, - this.modelMeta, - this.policy, - this.zodSchemas, - this.prismaModule, - this.user, - this.shouldLogQuery - ); this.model = lowerCaseFirst(model); + + ({ modelMeta: this.modelMeta, logPrismaQuery: this.logPrismaQuery, prismaModule: this.prismaModule } = options); + + this.utils = new PolicyUtil(prisma, options, context, this.shouldLogQuery); } private get modelClient() { @@ -1278,11 +1272,15 @@ export class PolicyProxyHandler implements Pr } private transaction(action: (tx: Record) => Promise) { - if (this.prisma[PRISMA_TX_FLAG]) { + if (this.prisma['$transaction']) { + return this.prisma.$transaction((tx) => action(tx), { + maxWait: this.options.transactionMaxWait, + timeout: this.options.transactionTimeout, + isolationLevel: this.options.transactionIsolationLevel, + }); + } else { // already in transaction, don't nest return action(this.prisma); - } else { - return this.prisma.$transaction((tx) => action(tx), { maxWait: 100000, timeout: 100000 }); } } @@ -1295,16 +1293,7 @@ export class PolicyProxyHandler implements Pr } private makeHandler(model: string) { - return new PolicyProxyHandler( - this.prisma, - this.policy, - this.modelMeta, - this.zodSchemas, - this.prismaModule, - model, - this.user, - this.logPrismaQuery - ); + return new PolicyProxyHandler(this.prisma, model, this.options, this.context); } private requireBackLink(fieldInfo: FieldInfo) { diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index 497379cc3..439f48933 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -1,57 +1,12 @@ -/* 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 { 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, EnhancementOptions } from '../enhance'; +import { makeProxy } from '../proxy'; import { PolicyProxyHandler } from './handler'; -/** - * Context for evaluating access policies - */ -export type WithPolicyContext = { - user?: AuthUser; -}; - -/** - * 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 Node module that contains PrismaClient - */ - prismaModule: any; -} - /** * Gets an enhanced Prisma client with access policy check. * @@ -59,31 +14,20 @@ export interface WithPolicyOptions extends CommonEnhancementOptions { * @param context The policy evaluation context * @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 + * + * @private */ export function withPolicy( prisma: DbClient, - options: WithPolicyOptions, - context?: WithPolicyContext + options: EnhancementOptions, + 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; - const _modelMeta = options.modelMeta; - const _zodSchemas = options?.zodSchemas; + 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, @@ -96,7 +40,7 @@ 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)) { @@ -108,18 +52,8 @@ export function withPolicy( return makeProxy( prisma, - _modelMeta, - (_prisma, model) => - new PolicyProxyHandler( - _prisma as DbClientContract, - _policy, - _modelMeta, - _zodSchemas, - options.prismaModule, - model, - context?.user, - options?.logPrismaQuery - ), + 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 7b1597ccd..4ebd8de3a 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -29,6 +29,7 @@ import { } from '../../cross'; import { AuthUser, DbClientContract, DbOperations, PolicyOperationKind } from '../../types'; import { getVersion } from '../../version'; +import type { EnhancementContext, EnhancementOptions } from '../enhance'; import type { InputCheckFunc, PolicyDef, ReadFieldCheckFunc, ZodSchemas } from '../types'; import { formatObject, @@ -42,20 +43,28 @@ import { Logger } from './logger'; * Access policy enforcement utilities */ export class PolicyUtil { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore 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 modelMeta: ModelMeta, - private readonly policy: PolicyDef, - private readonly zodSchemas: ZodSchemas | undefined, - private readonly prismaModule: any, - private readonly user?: AuthUser, + options: EnhancementOptions, + context?: EnhancementContext, private readonly shouldLogQuery = false ) { 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 diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index 358bff153..c735d595a 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PRISMA_PROXY_ENHANCER, PRISMA_TX_FLAG } from '../constants'; +import { PRISMA_PROXY_ENHANCER } from '../constants'; import type { ModelMeta } from '../cross'; import type { DbClientContract } from '../types'; import { createDeferredPromise } from './policy/promise'; @@ -183,6 +183,7 @@ export function makeProxy( errorTransformer?: ErrorTransformer ) { const models = Object.keys(modelMeta.fields).map((k) => k.toLowerCase()); + const proxy = new Proxy(prisma, { get: (target: any, prop: string | symbol, receiver: any) => { // enhancer metadata @@ -191,7 +192,7 @@ export function makeProxy( } if (prop === 'toString') { - return () => `$zenstack_${name}[${target.toString()}]`; + return () => `$zenstack_prisma_${prisma._clientVersion}`; } if (prop === '$transaction') { @@ -213,8 +214,10 @@ export function makeProxy( const txFunc = input; return $transaction.bind(target)((tx: any) => { + // create a proxy for the transaction function const txProxy = makeProxy(tx, modelMeta, makeHandler, name + '$tx'); - txProxy[PRISMA_TX_FLAG] = true; + + // call the transaction function with the proxy return txFunc(txProxy); }, ...rest); }; diff --git a/packages/schema/src/plugins/enhancer/enhancer.ts b/packages/schema/src/plugins/enhancer/enhancer.ts index 09360254b..5eccd356d 100644 --- a/packages/schema/src/plugins/enhancer/enhancer.ts +++ b/packages/schema/src/plugins/enhancer/enhancer.ts @@ -8,13 +8,13 @@ export async function generate(model: Model, options: PluginOptions, project: Pr project.createSourceFile( outFile, - `import { createEnhancement, type WithPolicyContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; + `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; import modelMeta from './model-meta'; import policy from './policy'; ${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; -export function enhance(prisma: DbClient, context?: WithPolicyContext, options?: EnhancementOptions): DbClient { +export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions): DbClient { return createEnhancement(prisma, { modelMeta, policy, diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index c1b3bfd4e..770d05017 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, withPolicy, type ModelMeta } 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'; @@ -1882,7 +1882,7 @@ describe('REST server tests', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, params); + prisma = params.enhanceRaw(params.prisma, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; @@ -1995,7 +1995,7 @@ describe('REST server tests', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, params); + prisma = params.enhanceRaw(params.prisma, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index fd7df30b7..2b501abc0 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { DMMF } from '@prisma/generator-helper'; import type { Model } from '@zenstackhq/language/ast'; -import { withOmit, withPassword, withPolicy, type AuthUser, type DbOperations } from '@zenstackhq/runtime'; +import type { AuthUser, DbOperations, EnhancementOptions } from '@zenstackhq/runtime'; import { getDMMF } from '@zenstackhq/sdk'; import { execSync } from 'child_process'; import * as fs from 'fs'; @@ -245,9 +245,6 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { prisma, prismaModule, projectDir: projectRoot, - withPolicy: undefined as any, - withOmit: undefined as any, - withPassword: undefined as any, enhance: undefined as any, enhanceRaw: undefined as any, policy: undefined as any, @@ -277,16 +274,19 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { return { projectDir: projectRoot, prisma, - withPolicy: (user?: AuthUser) => - withPolicy( + enhance: (user?: AuthUser, options?: EnhancementOptions): FullDbClientContract => + enhance( prisma, - { policy, modelMeta, zodSchemas, prismaModule, logPrismaQuery: opt.logPrismaQuery }, - { user } + { user }, + { + policy, + modelMeta, + zodSchemas, + logPrismaQuery: opt.logPrismaQuery, + transactionTimeout: 10000, + ...options, + } ), - withOmit: () => withOmit(prisma, { modelMeta }), - withPassword: () => withPassword(prisma, { modelMeta }), - enhance: (user?: AuthUser): FullDbClientContract => - enhance(prisma, { user }, { policy, modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery }), enhanceRaw: enhance, policy, modelMeta, 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 82e3ec2c8..67b97776a 100644 --- a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts +++ b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts @@ -32,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: { 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 737338666..37b23ecde 100644 --- a/tests/integration/tests/enhancements/with-password/with-password.test.ts +++ b/tests/integration/tests/enhancements/with-password/with-password.test.ts @@ -22,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', diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index 8f095f677..942d2d579 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -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(); }); 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 9608f9c62..c4da8349f 100644 --- a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts +++ b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts @@ -69,7 +69,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..ebaf2d858 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 @@ -103,7 +103,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 +128,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 +181,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 +205,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 +246,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 +281,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 +306,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 +381,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 +393,7 @@ describe('With Policy: field-level policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); let r; // y is unreadable @@ -430,7 +430,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 +472,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 +498,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 +523,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 +569,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 +583,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 +648,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 +676,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 +706,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 +734,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 +758,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 +786,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 +828,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 +854,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 +922,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 +948,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 +1010,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 +1042,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 +1054,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 +1067,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 +1084,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 +1116,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 +1144,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 +1157,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 +1206,7 @@ describe('With Policy: field-level policy', () => { ` ); - const db = withPolicy(); + const db = enhance(); await prisma.user.create({ data: { @@ -1269,7 +1269,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 +1285,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 +1300,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 +1317,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 +1335,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 8727f1561..ca71841db 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -5,7 +5,7 @@ describe('With Policy: 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()) @@ -49,7 +49,7 @@ describe('With Policy: field validation', () => { } ` ); - db = withPolicy(); + db = enhance(); }); beforeEach(() => { 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..6c27aab1c 100644 --- a/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts +++ b/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts @@ -13,7 +13,7 @@ describe('With Policy: fluent API', () => { }); it('fluent api', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model User { id Int @id @@ -58,7 +58,7 @@ model Post { }, }); - const db = withPolicy({ id: 1 }); + const db = enhance({ id: 1 }); // check policies await expect(db.user.findUnique({ where: { id: 1 } }).posts()).resolves.toHaveLength(2); 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/multi-id-fields.test.ts b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts index f48cdba45..227dc5a27 100644 --- a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts +++ b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts @@ -13,7 +13,7 @@ describe('With Policy: multiple id fields', () => { }); it('multi-id fields', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model A { x String @@ -43,7 +43,7 @@ describe('With Policy: multiple id fields', () => { ` ); - 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(); @@ -70,7 +70,7 @@ describe('With Policy: multiple id fields', () => { }); it('multi-id auth', async () => { - const { prisma, withPolicy } = await loadSchema( + const { prisma, enhance } = await loadSchema( ` model User { x String @@ -124,7 +124,7 @@ describe('With Policy: multiple id fields', () => { await prisma.user.create({ data: { x: '1', y: '1' } }); await prisma.user.create({ data: { x: '1', y: '2' } }); - const anonDb = withPolicy(); + const anonDb = enhance(); await expect( anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) @@ -139,7 +139,7 @@ describe('With Policy: multiple id fields', () => { anonDb.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) ).toBeRejectedByPolicy(); - const db = withPolicy({ x: '1', y: '1' }); + const db = enhance({ x: '1', y: '1' }); await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); @@ -149,13 +149,13 @@ describe('With Policy: multiple id fields', () => { await expect(db.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); await expect( - withPolicy(undefined).q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) + enhance(undefined).q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) ).toBeRejectedByPolicy(); await expect(db.q.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); }); it('multi-id to-one nested write', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model A { x Int @@ -177,7 +177,7 @@ describe('With Policy: multiple id fields', () => { } ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.b.create({ data: { @@ -205,7 +205,7 @@ describe('With Policy: multiple id fields', () => { }); it('multi-id to-many nested write', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model A { x Int @@ -237,7 +237,7 @@ describe('With Policy: multiple id fields', () => { } ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.b.create({ data: { 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 b112aeeb1..777af1118 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 with create from one to many', 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: { @@ -342,7 +342,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()) @@ -364,7 +364,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m2.create({ data: { id: '1' } }); @@ -392,7 +392,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()) @@ -415,7 +415,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -496,7 +496,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()) @@ -530,7 +530,7 @@ describe('With Policy:nested to-many', () => { ` ); - const db = withPolicy(); + const db = enhance(); await expect( db.m1.create({ @@ -589,7 +589,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()) @@ -621,7 +621,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 2e14b6d02..4b30c095f 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 create', async () => { - const { withPolicy } = await loadSchema( + const { enhance } = await loadSchema( ` model M1 { id String @id @default(uuid()) @@ -236,7 +236,7 @@ describe('With Policy:nested to-one', () => { { logPrismaQuery: true } ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -269,7 +269,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()) @@ -292,7 +292,7 @@ describe('With Policy:nested to-one', () => { ` ); - const db = withPolicy(); + const db = enhance(); await db.m1.create({ data: { @@ -335,7 +335,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()) @@ -356,7 +356,7 @@ describe('With Policy:nested to-one', () => { ` ); - await withPolicy({ id: 'user1' }).m1.create({ + await enhance({ id: 'user1' }).m1.create({ data: { id: 'm1', value: 1, @@ -364,7 +364,7 @@ describe('With Policy:nested to-one', () => { }); await expect( - withPolicy({ id: 'user2' }).user.create({ + enhance({ id: 'user2' }).user.create({ data: { id: 'user2', m1: { @@ -375,7 +375,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 }, @@ -384,7 +384,7 @@ describe('With Policy:nested to-one', () => { ).toBeRejectedByPolicy(); await expect( - withPolicy({ id: 'user1' }).user.create({ + enhance({ id: 'user1' }).user.create({ data: { id: 'user1', m1: { @@ -395,7 +395,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 79adb8c49..55c5458f4 100644 --- a/tests/integration/tests/enhancements/with-policy/options.test.ts +++ b/tests/integration/tests/enhancements/with-policy/options.test.ts @@ -1,4 +1,3 @@ -import { withPolicy } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; 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/postgres.test.ts b/tests/integration/tests/enhancements/with-policy/postgres.test.ts index caed6a5ce..7653b89f7 100644 --- a/tests/integration/tests/enhancements/with-policy/postgres.test.ts +++ b/tests/integration/tests/enhancements/with-policy/postgres.test.ts @@ -16,14 +16,14 @@ describe('With Policy: with postgres', () => { beforeEach(async () => { dbUrl = await createPostgresDb(DB_NAME); - const { prisma: _prisma, withPolicy } = await loadSchemaFromFile( + const { prisma: _prisma, enhance } = await loadSchemaFromFile( path.join(__dirname, '../../schema/todo-pg.zmodel'), { provider: 'postgresql', dbUrl, } ); - getDb = withPolicy; + getDb = enhance; prisma = _prisma; }); 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 126c038fa..0cd490f6c 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 99179e015..61f25dc25 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('delete tests', async () => { - const { withPolicy, prisma } = await loadSchema( + const { enhance, prisma } = 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 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/misc/stacktrace.test.ts b/tests/integration/tests/misc/stacktrace.test.ts index 6573ed088..08454d529 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 { diff --git a/tests/integration/tests/regression/issue-665.test.ts b/tests/integration/tests/regression/issue-665.test.ts index 8bd9f717b..b6552fd2b 100644 --- a/tests/integration/tests/regression/issue-665.test.ts +++ b/tests/integration/tests/regression/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/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index 8353f8bad..4ade85c8c 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/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()) @@ -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()) @@ -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: { From 2b3cf70c1bcdfeffa98baf09d53d59133b897c90 Mon Sep 17 00:00:00 2001 From: Augustin <43639468+Azzerty23@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:58:35 +0100 Subject: [PATCH 005/127] Support for auth() in @default attribute (#958) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> Co-authored-by: Yiming --- packages/ide/jetbrains/package.json | 2 +- packages/runtime/src/cross/model-meta.ts | 10 ++ .../runtime/src/cross/nested-write-visitor.ts | 2 +- .../src/enhancements/create-enhancement.ts | 23 ++- .../runtime/src/enhancements/default-auth.ts | 102 +++++++++++++ packages/runtime/src/enhancements/utils.ts | 15 ++ .../validator/expression-validator.ts | 37 +++-- .../function-invocation-validator.ts | 9 +- .../src/language-server/zmodel-linker.ts | 13 +- .../enhancer/policy/expression-writer.ts | 9 +- .../enhancer/policy/policy-guard-generator.ts | 10 +- .../src/plugins/prisma/prisma-builder.ts | 1 - .../src/plugins/prisma/schema-generator.ts | 14 +- .../src/plugins/zod/utils/schema-gen.ts | 6 +- packages/schema/src/res/stdlib.zmodel | 4 +- packages/schema/src/utils/ast-utils.ts | 48 +----- .../tests/generator/prisma-generator.test.ts | 2 + .../validation/attribute-validation.test.ts | 45 +++++- packages/sdk/package.json | 2 + packages/sdk/src/index.ts | 1 + packages/sdk/src/model-meta-generator.ts | 56 ++++++- .../src}/typescript-expression-transformer.ts | 4 +- packages/sdk/src/utils.ts | 47 +++++- pnpm-lock.yaml | 6 + .../enhancements/with-policy/auth.test.ts | 142 ++++++++++++++++++ 25 files changed, 498 insertions(+), 112 deletions(-) create mode 100644 packages/runtime/src/enhancements/default-auth.ts rename packages/{schema/src/utils => sdk/src}/typescript-expression-transformer.ts (98%) diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 274e88c2a..4e7fc26df 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -6,7 +6,7 @@ "homepage": "https://zenstack.dev", "private": true, "scripts": { - "build": "./gradlew buildPlugin" + "build": "./gradlew buildPlugin" }, "author": "ZenStack Team", "license": "MIT", diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts index a38f7986d..401caeaf2 100644 --- a/packages/runtime/src/cross/model-meta.ts +++ b/packages/runtime/src/cross/model-meta.ts @@ -8,6 +8,11 @@ export type RuntimeAttribute = { 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 */ @@ -67,6 +72,11 @@ export type FieldInfo = { */ foreignKeyMapping?: Record; + /** + * A function that provides a default value for the field + */ + defaultValueProvider?: FieldDefaultValueProvider; + /** * If the field is an auto-increment field */ diff --git a/packages/runtime/src/cross/nested-write-visitor.ts b/packages/runtime/src/cross/nested-write-visitor.ts index 7d67f6d9b..477117dbd 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, diff --git a/packages/runtime/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index a82640905..e3204cd52 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -7,6 +7,7 @@ import { withPassword } from './password'; import { withPolicy } from './policy'; import type { ErrorTransformer } from './proxy'; import type { PolicyDef, ZodSchemas } from './types'; +import { withDefaultAuth } from './default-auth'; /** * Kinds of enhancements to `PrismaClient` @@ -15,6 +16,7 @@ export enum EnhancementKind { Password = 'password', Omit = 'omit', Policy = 'policy', + DefaultAuth = 'defaultAuth', } /** @@ -92,6 +94,7 @@ export type EnhancementContext = { let hasPassword: boolean | undefined = undefined; let hasOmit: boolean | undefined = undefined; +let hasDefaultAuth: boolean | undefined = undefined; /** * Gets a Prisma client enhanced with all enhancement behaviors, including access @@ -120,13 +123,24 @@ export function createEnhancement( let result = prisma; - if (hasPassword === undefined || hasOmit === undefined) { + if ( + process.env.ZENSTACK_TEST === '1' || // avoid caching in tests + hasPassword === undefined || + hasOmit === undefined || + hasDefaultAuth === undefined + ) { const allFields = Object.values(options.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')); + hasDefaultAuth = allFields.some((field) => field.defaultValueProvider); } - const kinds = options.kinds ?? [EnhancementKind.Password, EnhancementKind.Omit, EnhancementKind.Policy]; + const kinds = options.kinds ?? [ + EnhancementKind.Password, + EnhancementKind.Omit, + EnhancementKind.Policy, + EnhancementKind.DefaultAuth, + ]; if (hasPassword && kinds.includes(EnhancementKind.Password)) { // @password proxy @@ -138,6 +152,11 @@ export function createEnhancement( result = withOmit(result, options); } + if (hasDefaultAuth && kinds.includes(EnhancementKind.DefaultAuth)) { + // @default(auth()) proxy + result = withDefaultAuth(result, options, context); + } + // policy proxy if (kinds.includes(EnhancementKind.Policy)) { result = withPolicy(result, options, context); diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts new file mode 100644 index 000000000..48af0ed73 --- /dev/null +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import deepcopy from 'deepcopy'; +import { FieldInfo, NestedWriteVisitor, PrismaWriteActionType, enumerate, getFields } from '../cross'; +import { DbClientContract } from '../types'; +import { EnhancementContext, EnhancementOptions } from './create-enhancement'; +import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; + +/** + * Gets an enhanced Prisma client that supports `@default(auth())` attribute. + * + * @private + */ +export function withDefaultAuth( + prisma: DbClient, + options: EnhancementOptions, + 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 db: DbClientContract; + private readonly userContext: any; + + constructor( + prisma: DbClientContract, + model: string, + private readonly options: EnhancementOptions, + private readonly context?: EnhancementContext + ) { + super(prisma, model); + this.db = prisma; + + if (!this.context?.user) { + throw new Error(`Using \`auth()\` in \`@default\` requires a user context`); + } + + 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()` + data[fieldInfo.name] = 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 getDefaultValueFromAuth(fieldInfo: FieldInfo) { + return fieldInfo.defaultValueProvider?.(this.userContext); + } +} diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index ba2f9a2d8..2879a3119 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -22,3 +22,18 @@ export function prismaClientKnownRequestError(prisma: DbClientContract, prismaMo export function prismaClientUnknownRequestError(prismaModule: any, ...args: unknown[]): Error { throw new prismaModule.PrismaClientUnknownRequestError(...args); } + +export function deepGet(object: object, path: string | string[] | undefined, defaultValue: unknown): unknown { + if (path === undefined || path === '') { + return defaultValue; + } + const keys = Array.isArray(path) ? path : path.split('.'); + for (const key of keys) { + if (object && typeof object === 'object' && key in object) { + object = object[key as keyof typeof object]; + } else { + return defaultValue; + } + } + return object !== undefined ? object : defaultValue; +} diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 7644521b8..cfc8a39af 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -3,16 +3,16 @@ import { Expression, ExpressionType, isDataModel, + isDataModelField, isEnum, + isLiteralExpr, isMemberAccessExpr, isNullExpr, isThisExpr, - isDataModelField, - isLiteralExpr, } from '@zenstackhq/language/ast'; -import { isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; +import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; import { ValidationAcceptor } from 'langium'; -import { getContainingDataModel, isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils'; +import { getContainingDataModel, isCollectionPredicate } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { typeAssignable } from './utils'; @@ -132,18 +132,24 @@ export default class ExpressionValidator implements AstValidator { // - foo.user.id == userId // except: // - future().userId == userId - if(isMemberAccessExpr(expr.left) && isDataModelField(expr.left.member.ref) && expr.left.member.ref.$container != getContainingDataModel(expr) - || isMemberAccessExpr(expr.right) && isDataModelField(expr.right.member.ref) && expr.right.member.ref.$container != getContainingDataModel(expr)) - { + if ( + (isMemberAccessExpr(expr.left) && + isDataModelField(expr.left.member.ref) && + expr.left.member.ref.$container != getContainingDataModel(expr)) || + (isMemberAccessExpr(expr.right) && + isDataModelField(expr.right.member.ref) && + expr.right.member.ref.$container != getContainingDataModel(expr)) + ) { // foo.user.id == auth().id // foo.user.id == "123" // foo.user.id == null // foo.user.id == EnumValue - if(!(this.isNotModelFieldExpr(expr.left) || this.isNotModelFieldExpr(expr.right))) - { - accept('error', 'comparison between fields of different models are not supported', { node: expr }); - break; - } + if (!(this.isNotModelFieldExpr(expr.left) || this.isNotModelFieldExpr(expr.right))) { + accept('error', 'comparison between fields of different models are not supported', { + node: expr, + }); + break; + } } if ( @@ -205,14 +211,13 @@ export default class ExpressionValidator implements AstValidator { } } - private isNotModelFieldExpr(expr: Expression) { - return isLiteralExpr(expr) || isEnumFieldReference(expr) || isNullExpr(expr) || this.isAuthOrAuthMemberAccess(expr) + return ( + isLiteralExpr(expr) || isEnumFieldReference(expr) || isNullExpr(expr) || this.isAuthOrAuthMemberAccess(expr) + ); } private isAuthOrAuthMemberAccess(expr: Expression) { return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthInvocation(expr.operand)); } - } - 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 3bc364bd2..50b974a53 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/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index ef97cf4b6..8c8fb2c98 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -35,7 +35,7 @@ import { isReferenceExpr, isStringLiteral, } from '@zenstackhq/language/ast'; -import { getContainingModel, hasAttribute, isFromStdlib } from '@zenstackhq/sdk'; +import { getContainingModel, hasAttribute, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -52,12 +52,7 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { - getAllDeclarationsFromImports, - getContainingDataModel, - isAuthInvocation, - isCollectionPredicate, -} from '../utils/ast-utils'; +import { getAllDeclarationsFromImports, getContainingDataModel, isCollectionPredicate } from '../utils/ast-utils'; import { mapBuiltinTypeToExpressionType } from './validator/utils'; interface DefaultReference extends Reference { @@ -329,7 +324,7 @@ 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); @@ -346,7 +341,7 @@ export class ZModelLinker extends DefaultLinker { 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 { diff --git a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts index 0cc80c7ea..e38a34c29 100644 --- a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -19,19 +19,18 @@ import { import { ExpressionContext, getFunctionExpressionContext, + getIdFields, getLiteral, + isAuthInvocation, isDataModelFieldReference, isFutureExpr, PluginError, + TypeScriptExpressionTransformer, + TypeScriptExpressionTransformerError, } from '@zenstackhq/sdk'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter } from 'ts-morph'; import { name } from '..'; -import { getIdFields, isAuthInvocation } from '../../../utils/ast-utils'; -import { - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '../../../utils/typescript-expression-transformer'; type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<='; diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index e5017383d..149858cd6 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -33,14 +33,18 @@ import { PluginError, PluginOptions, RUNTIME_PACKAGE, + TypeScriptExpressionTransformer, + TypeScriptExpressionTransformerError, analyzePolicies, getAttributeArg, getAuthModel, getDataModels, + getIdFields, getLiteral, getPrismaClientImportSpec, hasAttribute, hasValidationAttributes, + isAuthInvocation, isEnumFieldReference, isForeignKeyField, isFromStdlib, @@ -52,11 +56,7 @@ import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { FunctionDeclaration, Project, 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 { isCollectionPredicate } from '../../../utils/ast-utils'; import { ALL_OPERATION_KINDS } from '../../plugin-utils'; import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 64777b62e..68336baeb 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -310,7 +310,6 @@ export class FunctionCallArg { return this.name ? `${this.name}: ${this.value}` : this.value; } } - export class Enum extends ContainerDeclaration { public fields: EnumField[] = []; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index feee0f3d1..0f25ab1b8 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -33,6 +33,7 @@ import { getDMMF, getLiteral, getPrismaVersion, + isDefaultAuthField, PluginError, PluginOptions, resolved, @@ -311,9 +312,7 @@ export default class PrismaSchemaGenerator { const type = new ModelFieldType(fieldType, field.type.array, field.type.optional); - const attributes = field.attributes - .filter((attr) => this.isPrismaAttribute(attr)) - .map((attr) => this.makeFieldAttribute(attr)); + const attributes = this.getAttributesToGenerate(field); const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); @@ -325,6 +324,15 @@ export default class PrismaSchemaGenerator { field.comments.forEach((c) => result.addComment(c)); } + private getAttributesToGenerate(field: DataModelField) { + if (isDefaultAuthField(field)) { + return []; + } + return field.attributes + .filter((attr) => this.isPrismaAttribute(attr)) + .map((attr) => this.makeFieldAttribute(attr)); + } + private makeFieldAttribute(attr: DataModelFieldAttribute) { const attrName = resolved(attr.decl).name; if (attrName === FIELD_PASSTHROUGH_ATTR) { diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 802127c58..02607d4c7 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, @@ -18,10 +20,6 @@ import { } from '@zenstackhq/sdk/ast'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '..'; -import { - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '../../../utils/typescript-expression-transformer'; export function makeFieldSchema(field: DataModelField, respectDefault = false) { if (isDataModel(field.type.reference?.ref)) { diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 1a9446d7b..f755bb3df 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -73,7 +73,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). @@ -204,7 +204,7 @@ attribute @id(map: String?, length: Int?, sort: String?, 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 diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 661f14b26..80543d6a2 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -1,21 +1,13 @@ import { BinaryExpr, DataModel, - DataModelField, Expression, - isArrayExpr, isBinaryExpr, isDataModel, - isDataModelField, - isInvocationExpr, - isMemberAccessExpr, isModel, - isReferenceExpr, Model, ModelImport, - ReferenceExpr, } from '@zenstackhq/language/ast'; -import { isFromStdlib } from '@zenstackhq/sdk'; import { AstNode, getDocument, LangiumDocuments, Mutable } from 'langium'; import { URI, Utils } from 'vscode-uri'; @@ -56,43 +48,6 @@ function updateContainer(nodes: T[], container: AstNode): Mut }); } -export function getIdFields(dataModel: DataModel) { - const fieldLevelId = dataModel.$resolvedFields.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 isAuthInvocation(node: AstNode) { - return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); -} - -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 resolveImportUri(imp: ModelImport): URI | undefined { if (imp.path === undefined || imp.path.length === 0) { return undefined; @@ -157,7 +112,6 @@ export function isCollectionPredicate(node: AstNode): node is BinaryExpr { return isBinaryExpr(node) && ['?', '!', '^'].includes(node.operator); } - export function getContainingDataModel(node: Expression): DataModel | undefined { let curr: AstNode | undefined = node.$container; while (curr) { @@ -167,4 +121,4 @@ export function getContainingDataModel(node: Expression): DataModel | undefined curr = curr.$container; } return undefined; -} \ No newline at end of file +} diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 8d295d143..d2f425e53 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -123,6 +123,7 @@ 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) } `); @@ -142,6 +143,7 @@ describe('Prisma generator test', () => { 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 () => { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 8b7886334..cb2f788d4 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1009,6 +1009,35 @@ 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} + + // model User { + // id String @id + // name String + // } + // model B { + // id String @id + // userData String @default(auth()) + // } + // `) + // ).toContain("Value is not assignable to parameter"); + expect( await loadModelWithError(` ${prelude} @@ -1124,14 +1153,14 @@ 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} + // model M { + // id String @id @default(auth()) + // } + // `) + // ).toContain('function "auth" is not allowed in the current context: DefaultValue'); expect( await loadModelWithError(` diff --git a/packages/sdk/package.json b/packages/sdk/package.json index beddaad70..ac8bcaf1d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -23,10 +23,12 @@ "@prisma/internals-v5": "npm:@prisma/internals@^5.0.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", + "langium": "1.2.0", "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": { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 64060390e..5013267e8 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -4,6 +4,7 @@ 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 2692706c1..9beda653a 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -12,9 +12,11 @@ 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 { + ExpressionContext, getAttribute, getAttributeArg, getAttributeArgs, @@ -22,10 +24,12 @@ import { getDataModels, getLiteral, hasAttribute, + isAuthInvocation, isEnumFieldReference, isForeignKeyField, isIdField, resolved, + TypeScriptExpressionTransformer, } from '.'; export type ModelMetaGeneratorOptions = { @@ -38,13 +42,20 @@ 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;'); return sf; } -function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter, options: ModelMetaGeneratorOptions) { +function generateModelMetadata( + dataModels: DataModel[], + sourceFile: SourceFile, + writer: CodeBlockWriter, + options: ModelMetaGeneratorOptions +) { writer.block(() => { writer.write('fields:'); writer.block(() => { @@ -120,6 +131,12 @@ function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter, foreignKeyMapping: ${JSON.stringify(fkMapping)},`); } + const defaultValueProvider = generateDefaultValueProvider(f, sourceFile); + if (defaultValueProvider) { + writer.write(` + defaultValueProvider: ${defaultValueProvider},`); + } + if (isAutoIncrement(f)) { writer.write(` isAutoIncrement: true,`); @@ -334,6 +351,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/schema/src/utils/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts similarity index 98% rename from packages/schema/src/utils/typescript-expression-transformer.ts rename to packages/sdk/src/typescript-expression-transformer.ts index cd868d76c..20585118c 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -17,9 +17,9 @@ import { ThisExpr, UnaryExpr, } from '@zenstackhq/language/ast'; -import { ExpressionContext, getLiteral, isFromStdlib, isFutureExpr } from '@zenstackhq/sdk'; import { match, P } from 'ts-pattern'; -import { getIdFields } from './ast-utils'; +import { ExpressionContext } from './constants'; +import { getIdFields, getLiteral, 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 afd043565..2f046b692 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, @@ -280,6 +281,13 @@ export function isForeignKeyField(field: DataModelField) { }); } +export function isDefaultAuthField(field: DataModelField) { + return ( + hasAttribute(field, '@default') && + !!field.attributes.find((attr) => attr.args?.[0]?.value.$cstNode?.text.startsWith('auth()')) + ); +} + export function resolvePath(_path: string, options: Pick) { if (path.isAbsolute(_path)) { return _path; @@ -334,7 +342,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) { @@ -373,3 +385,36 @@ export function getAuthModel(dataModels: DataModel[]) { } return authModel; } + +export function getIdFields(dataModel: DataModel) { + const fieldLevelId = dataModel.$resolvedFields.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; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8af4092a1..1bbcc402d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -620,6 +620,9 @@ importers: '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime/dist + langium: + specifier: 1.2.0 + version: 1.2.0 lower-case-first: specifier: ^2.0.2 version: 2.0.2 @@ -632,6 +635,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 diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index 942d2d579..f5b4e2f4f 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -363,4 +363,146 @@ 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, modelMeta } = 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({ id: 'userId-1' }); + await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); + await expect(db.post.create({ data: { title: 'abc' } })).resolves.toMatchObject({ authorId: 'userId-1' }); + }); + + 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 }), + ]) + ); + }); }); From 9f2dfb1844e519f2d4d4cba21c6d088798ca6676 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 27 Jan 2024 18:25:49 +0800 Subject: [PATCH 006/127] refactor: simplify zmodel linking by improving scope computation; make AST cloning from base models more robust (#957) --- packages/language/src/ast.ts | 20 +- .../src/enhancements/create-enhancement.ts | 2 +- packages/runtime/src/enhancements/utils.ts | 15 -- packages/schema/src/cli/cli-util.ts | 2 +- .../validator/datamodel-validator.ts | 47 ++-- .../src/language-server/validator/utils.ts | 4 +- .../src/language-server/zmodel-code-action.ts | 11 +- .../src/language-server/zmodel-linker.ts | 73 ++---- .../src/language-server/zmodel-scope.ts | 215 +++++++++++------- .../src/plugins/prisma/prisma-builder.ts | 1 + .../src/plugins/prisma/schema-generator.ts | 21 +- packages/schema/src/telemetry.ts | 2 +- packages/schema/src/utils/ast-utils.ts | 87 +++++-- .../validation/attribute-validation.test.ts | 5 +- packages/schema/tests/utils.ts | 4 +- packages/sdk/src/utils.ts | 27 ++- packages/testtools/src/model.ts | 4 +- .../tests/regression/issue-925.test.ts | 20 +- .../tests/regression/issues.test.ts | 28 +-- 19 files changed, 341 insertions(+), 247 deletions(-) diff --git a/packages/language/src/ast.ts b/packages/language/src/ast.ts index c8637115a..86dd55bed 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,18 +45,19 @@ declare module './generated/ast' { $resolvedParam?: AttributeParam; } - interface DataModel { - /** - * Resolved fields, include inherited fields - */ - $resolvedFields: Array; + interface DataModelField { + $inheritedFrom?: DataModel; } - interface DataModelField { - $isInherited?: boolean; + interface DataModelAttribute { + $inheritedFrom?: DataModel; } } +export interface InheritableNode extends AstNode { + $inheritedFrom?: DataModel; +} + declare module 'langium' { export interface AstNode { /** diff --git a/packages/runtime/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index e3204cd52..b137e03f9 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -2,12 +2,12 @@ import semver from 'semver'; import { PRISMA_MINIMUM_VERSION } from '../constants'; import { ModelMeta } from '../cross'; import type { AuthUser } from '../types'; +import { withDefaultAuth } from './default-auth'; import { withOmit } from './omit'; import { withPassword } from './password'; import { withPolicy } from './policy'; import type { ErrorTransformer } from './proxy'; import type { PolicyDef, ZodSchemas } from './types'; -import { withDefaultAuth } from './default-auth'; /** * Kinds of enhancements to `PrismaClient` diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 2879a3119..ba2f9a2d8 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -22,18 +22,3 @@ export function prismaClientKnownRequestError(prisma: DbClientContract, prismaMo export function prismaClientUnknownRequestError(prismaModule: any, ...args: unknown[]): Error { throw new prismaModule.PrismaClientUnknownRequestError(...args); } - -export function deepGet(object: object, path: string | string[] | undefined, defaultValue: unknown): unknown { - if (path === undefined || path === '') { - return defaultValue; - } - const keys = Array.isArray(path) ? path : path.split('.'); - for (const key of keys) { - if (object && typeof object === 'object' && key in object) { - object = object[key as keyof typeof object]; - } else { - return defaultValue; - } - } - return object !== undefined ? object : defaultValue; -} diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 000e92ca7..2cfa18fcb 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -89,7 +89,7 @@ export async function loadDocument(fileName: string): Promise { validationAfterMerge(model); - mergeBaseModel(model); + mergeBaseModel(model, services.references.Linker); return model; } diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index ce1886f5e..33ec0ff37 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -6,7 +6,13 @@ import { isStringLiteral, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { analyzePolicies, getLiteral, getModelIdFields, getModelUniqueFields } from '@zenstackhq/sdk'; +import { + analyzePolicies, + getLiteral, + getModelFieldsWithBases, + getModelIdFields, + getModelUniqueFields, +} from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; @@ -20,16 +26,15 @@ import { validateDuplicatedDeclarations } from './utils'; export default class DataModelValidator implements AstValidator { validate(dm: DataModel, accept: ValidationAcceptor): void { this.validateBaseAbstractModel(dm, accept); - validateDuplicatedDeclarations(dm.$resolvedFields, accept); + validateDuplicatedDeclarations(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); @@ -42,7 +47,7 @@ export default class DataModelValidator implements AstValidator { const { allows, denies, hasFieldValidation } = analyzePolicies(dm); if (allows.length > 0 || denies.length > 0 || hasFieldValidation) { // TODO: relax this requirement to require only @unique fields - // when access policies or field valdaition is used, require an @id field + // when access policies or field validation is used, require an @id field accept( 'error', 'Model must include a field with @id or @unique attribute, or a model-level @@id or @@unique attribute to use access policies', @@ -74,10 +79,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); }); } } @@ -194,7 +199,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'); @@ -213,7 +218,7 @@ 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; @@ -223,8 +228,8 @@ export default class DataModelValidator implements AstValidator { 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).filter( + (f) => f.type.reference?.ref?.name === contextModel.name ); oppositeFields = oppositeFields.filter((f) => { const fieldRel = this.parseRelation(f); @@ -232,13 +237,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; @@ -247,20 +252,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((x) => !x.$inheritedFrom) .forEach((f) => { if (this.isSelfRelation(f)) { // self relations are partial diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 50e2263d7..340f471b8 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -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) => !(x as DataModelField).$inheritedFrom); 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..8f60cbe69 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 { getModelFieldsWithBases } from '@zenstackhq/sdk'; import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver'; 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-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 8c8fb2c98..30929791f 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, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; +import { + getContainingModel, + getModelFieldsWithBases, + hasAttribute, + isAuthInvocation, + isFutureExpr, +} from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -52,7 +58,7 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { getAllDeclarationsFromImports, getContainingDataModel, isCollectionPredicate } from '../utils/ast-utils'; +import { getAllDeclarationsFromImports, getContainingDataModel } from '../utils/ast-utils'; import { mapBuiltinTypeToExpressionType } from './validator/utils'; interface DefaultReference extends Reference { @@ -256,26 +262,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); @@ -285,26 +274,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)); @@ -367,14 +336,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); @@ -388,20 +354,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 @@ -455,10 +411,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)) { diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 8eda869e8..21304fa4a 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,16 @@ import { isMemberAccessExpr, isModel, isReferenceExpr, + isThisExpr, } from '@zenstackhq/language/ast'; -import { getAuthModel, getDataModels } from '@zenstackhq/sdk'; +import { + getAuthModel, + getDataModels, + getModelFieldsWithBases, + getRecursiveBases, + isAuthInvocation, + isFutureExpr, +} from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -19,7 +26,6 @@ import { EMPTY_SCOPE, LangiumDocument, LangiumServices, - Mutable, PrecomputedScopes, ReferenceInfo, Scope, @@ -30,8 +36,9 @@ import { stream, streamAllContents, } from 'langium'; +import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { resolveImportUri } from '../utils/ast-utils'; +import { isCollectionPredicate, resolveImportUri } from '../utils/ast-utils'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; /** @@ -66,49 +73,18 @@ 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)) { + // 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; + } } } @@ -140,50 +116,129 @@ export class ZModelScopeProvider extends DefaultScopeProvider { 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 + const ref = operand.member.ref; + if (isDataModelField(ref)) { + 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 (isFutureExpr(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 authModel = getAuthModel(getDataModels(model, true)); + if (authModel) { + return this.createScopeForNodes(authModel.fields, 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/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 68336baeb..64777b62e 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -310,6 +310,7 @@ export class FunctionCallArg { return this.name ? `${this.name}: ${this.value}` : this.value; } } + export class Enum extends ContainerDeclaration { public fields: EnumField[] = []; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 0f25ab1b8..d7a32ad30 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -30,10 +30,11 @@ import { match } from 'ts-pattern'; import { PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { + getAttribute, getDMMF, getLiteral, getPrismaVersion, - isDefaultAuthField, + isAuthInvocation, PluginError, PluginOptions, resolved, @@ -42,6 +43,7 @@ import { } from '@zenstackhq/sdk'; import fs from 'fs'; import { writeFile } from 'fs/promises'; +import { streamAst } from 'langium'; import path from 'path'; import semver from 'semver'; import stripColor from 'strip-color'; @@ -325,7 +327,7 @@ export default class PrismaSchemaGenerator { } private getAttributesToGenerate(field: DataModelField) { - if (isDefaultAuthField(field)) { + if (this.hasDefaultWithAuth(field)) { return []; } return field.attributes @@ -333,6 +335,21 @@ export default class PrismaSchemaGenerator { .map((attr) => this.makeFieldAttribute(attr)); } + private hasDefaultWithAuth(field: DataModelField) { + const defaultAttr = getAttribute(field, '@default'); + if (!defaultAttr) { + return false; + } + + const expr = defaultAttr.args[0]?.value; + if (!expr) { + return false; + } + + // find `auth()` in default value expression + return streamAst(expr).some(isAuthInvocation); + } + private makeFieldAttribute(attr: DataModelFieldAttribute) { const attrName = resolved(attr.decl).name; if (attrName === FIELD_PASSTHROUGH_ATTR) { diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index 3166a5f9b..9cd8ba386 100644 --- a/packages/schema/src/telemetry.ts +++ b/packages/schema/src/telemetry.ts @@ -8,8 +8,8 @@ import sleep from 'sleep-promise'; import { CliError } from './cli/cli-error'; import { TELEMETRY_TRACKING_TOKEN } from './constants'; import isDocker from './utils/is-docker'; -import { getVersion } from './utils/version-utils'; import { getMachineId } from './utils/machine-id-utils'; +import { getVersion } from './utils/version-utils'; /** * Telemetry events diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 80543d6a2..1e2850577 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -2,13 +2,27 @@ import { BinaryExpr, DataModel, Expression, + InheritableNode, isBinaryExpr, isDataModel, isModel, Model, ModelImport, } from '@zenstackhq/language/ast'; -import { AstNode, getDocument, LangiumDocuments, Mutable } from 'langium'; +import { + AstNode, + CstNode, + GenericAstNode, + getContainerOfType, + getDocument, + isAstNode, + isReference, + LangiumDocuments, + linkContentToContainer, + Linker, + Mutable, + Reference, +} from 'langium'; import { URI, Utils } from 'vscode-uri'; export function extractDataModelsWithAllowRules(model: Model): DataModel[] { @@ -17,7 +31,16 @@ export function extractDataModelsWithAllowRules(model: Model): DataModel[] { ) as DataModel[]; } -export function mergeBaseModel(model: Model) { +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); + model.declarations .filter((x) => x.$type === 'DataModel') .forEach((decl) => { @@ -25,27 +48,65 @@ export function mergeBaseModel(model: Model) { dataModel.fields = dataModel.superTypes // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .flatMap((superType) => updateContainer(superType.ref!.fields, dataModel)) + .flatMap((superType) => superType.ref!.fields) + .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)) + .flatMap((superType) => superType.ref!.attributes) + .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); }); // remove abstract models - model.declarations = model.declarations.filter((x) => !(x.$type == 'DataModel' && x.isAbstract)); + model.declarations = model.declarations.filter((x) => !(isDataModel(x) && x.isAbstract)); +} + +// 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.$containerProperty = node.$containerProperty; + clone.$containerIndex = node.$containerIndex; + clone.$inheritedFrom = getContainerOfType(node, isDataModel); + return clone; } -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; - }); +// this function is copied from Langium's ast-utils, but copying $resolvedType as well +function copyAstNode(node: T, buildReference: BuildReference): T { + const copy: GenericAstNode = { $type: node.$type, $resolvedType: node.$resolvedType }; + + for (const [name, value] of Object.entries(node)) { + if (!name.startsWith('$')) { + if (isAstNode(value)) { + copy[name] = copyAstNode(value, buildReference); + } else if (isReference(value)) { + copy[name] = buildReference(copy, name, value.$refNode, value.$refText); + } else if (Array.isArray(value)) { + const copiedArray: unknown[] = []; + for (const element of value) { + if (isAstNode(element)) { + copiedArray.push(copyAstNode(element, buildReference)); + } else if (isReference(element)) { + copiedArray.push(buildReference(copy, name, element.$refNode, element.$refText)); + } else { + copiedArray.push(element); + } + } + copy[name] = copiedArray; + } else { + copy[name] = value; + } + } + } + + linkContentToContainer(copy); + return copy as unknown as T; } export function resolveImportUri(imp: ModelImport): URI | undefined { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index cb2f788d4..dfc1d650c 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1088,11 +1088,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]) } `) diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index f88aae6e2..4dcd45170 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -16,7 +16,7 @@ export class SchemaLoadingError extends Error { 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')) ); @@ -52,7 +52,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; diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 2f046b692..ed841dbc7 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -32,7 +32,7 @@ import { } from '@zenstackhq/language/ast'; import path from 'path'; import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; -import { PluginDeclaredOptions, PluginError, PluginOptions } from './types'; +import { PluginError, type PluginDeclaredOptions, type PluginOptions } from './types'; /** * Gets data models that are not ignored @@ -281,13 +281,6 @@ export function isForeignKeyField(field: DataModelField) { }); } -export function isDefaultAuthField(field: DataModelField) { - return ( - hasAttribute(field, '@default') && - !!field.attributes.find((attr) => attr.args?.[0]?.value.$cstNode?.text.startsWith('auth()')) - ); -} - export function resolvePath(_path: string, options: Pick) { if (path.isAbsolute(_path)) { return _path; @@ -387,7 +380,7 @@ export function getAuthModel(dataModels: DataModel[]) { } 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) { @@ -418,3 +411,19 @@ export function getDataModelFieldReference(expr: Expression): DataModelField | u 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; +} diff --git a/packages/testtools/src/model.ts b/packages/testtools/src/model.ts index 4be8a1613..29b15467d 100644 --- a/packages/testtools/src/model.ts +++ b/packages/testtools/src/model.ts @@ -16,7 +16,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')) ); @@ -51,7 +51,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/tests/integration/tests/regression/issue-925.test.ts b/tests/integration/tests/regression/issue-925.test.ts index 34b1ac434..b19d9d615 100644 --- a/tests/integration/tests/regression/issue-925.test.ts +++ b/tests/integration/tests/regression/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,18 @@ 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( - ` + it('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 +64,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/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index 4ade85c8c..7c2ca94cd 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/issues.test.ts @@ -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]) } ` ); @@ -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]) } ` ); From bac368382b6c92585bc983861a56d141093b7896 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Feb 2024 15:51:32 +0800 Subject: [PATCH 007/127] feat: polymorphism (#990) --- .eslintrc.json | 4 +- packages/language/src/ast.ts | 11 + packages/plugins/swr/tests/test-model-meta.ts | 59 +- .../tanstack-query/tests/test-model-meta.ts | 59 +- packages/runtime/package.json | 2 + packages/runtime/src/constants.ts | 5 + packages/runtime/src/cross/model-meta.ts | 71 +- .../runtime/src/cross/nested-write-visitor.ts | 6 +- packages/runtime/src/cross/query-analyzer.ts | 2 +- packages/runtime/src/cross/utils.ts | 20 +- .../src/enhancements/create-enhancement.ts | 68 +- .../runtime/src/enhancements/default-auth.ts | 6 +- packages/runtime/src/enhancements/delegate.ts | 1133 +++++++++++++++++ .../src/enhancements/{policy => }/logger.ts | 0 packages/runtime/src/enhancements/omit.ts | 4 +- packages/runtime/src/enhancements/password.ts | 4 +- .../src/enhancements/policy/handler.ts | 274 ++-- .../src/enhancements/policy/policy-utils.ts | 219 +--- packages/runtime/src/enhancements/proxy.ts | 15 +- .../runtime/src/enhancements/query-utils.ts | 172 +++ packages/runtime/src/enhancements/types.ts | 4 +- packages/runtime/src/types.ts | 12 +- packages/schema/src/cli/cli-util.ts | 28 +- packages/schema/src/extension.ts | 2 +- .../validator/datamodel-validator.ts | 37 +- .../validator/datasource-validator.ts | 2 +- .../validator/enum-validator.ts | 2 +- .../validator/schema-validator.ts | 2 +- .../src/language-server/validator/utils.ts | 4 +- .../src/language-server/zmodel-code-action.ts | 2 +- .../zmodel-completion-provider.ts | 4 +- .../src/language-server/zmodel-linker.ts | 12 +- .../src/language-server/zmodel-scope.ts | 7 +- .../src/plugins/enhancer/delegate/index.ts | 16 + .../src/plugins/enhancer/enhance/index.ts | 215 ++++ .../schema/src/plugins/enhancer/enhancer.ts | 29 - packages/schema/src/plugins/enhancer/index.ts | 2 +- .../{model-meta.ts => model-meta/index.ts} | 0 .../enhancer/policy/expression-writer.ts | 102 +- .../enhancer/policy/policy-guard-generator.ts | 34 +- packages/schema/src/plugins/plugin-utils.ts | 10 +- packages/schema/src/plugins/prisma/index.ts | 2 +- .../src/plugins/prisma/prisma-builder.ts | 9 +- .../src/plugins/prisma/schema-generator.ts | 222 +++- packages/schema/src/res/stdlib.zmodel | 10 + packages/schema/src/utils/ast-utils.ts | 123 +- .../tests/generator/prisma-generator.test.ts | 7 +- .../validation/datamodel-validation.test.ts | 4 +- packages/sdk/src/model-meta-generator.ts | 311 +++-- packages/sdk/src/utils.ts | 8 +- packages/server/src/api/rest/index.ts | 10 +- packages/testtools/src/schema.ts | 12 +- pnpm-lock.yaml | 6 + tests/integration/package.json | 2 +- tests/integration/tests/cli/init.test.ts | 8 +- .../enhancements/with-delegate/policy.test.ts | 217 ++++ .../with-delegate/polymorphism.test.ts | 1015 +++++++++++++++ .../integration/tests/schema/petstore.zmodel | 1 - tests/integration/tests/schema/todo.zmodel | 1 - 59 files changed, 3850 insertions(+), 778 deletions(-) create mode 100644 packages/runtime/src/enhancements/delegate.ts rename packages/runtime/src/enhancements/{policy => }/logger.ts (100%) create mode 100644 packages/runtime/src/enhancements/query-utils.ts create mode 100644 packages/schema/src/plugins/enhancer/delegate/index.ts create mode 100644 packages/schema/src/plugins/enhancer/enhance/index.ts delete mode 100644 packages/schema/src/plugins/enhancer/enhancer.ts rename packages/schema/src/plugins/enhancer/{model-meta.ts => model-meta/index.ts} (100%) create mode 100644 tests/integration/tests/enhancements/with-delegate/policy.test.ts create mode 100644 tests/integration/tests/enhancements/with-delegate/polymorphism.test.ts 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/packages/language/src/ast.ts b/packages/language/src/ast.ts index 86dd55bed..3da706a75 100644 --- a/packages/language/src/ast.ts +++ b/packages/language/src/ast.ts @@ -52,6 +52,17 @@ declare module './generated/ast' { interface DataModelAttribute { $inheritedFrom?: DataModel; } + + export interface DataModel { + /** + * Indicates whether the model is already merged with the base types + */ + $baseMerged?: boolean; + } +} + +export interface InheritableNode extends AstNode { + $inheritedFrom?: DataModel; } export interface InheritableNode extends AstNode { diff --git a/packages/plugins/swr/tests/test-model-meta.ts b/packages/plugins/swr/tests/test-model-meta.ts index 41731ad18..71a657bad 100644 --- a/packages/plugins/swr/tests/test-model-meta.ts +++ b/packages/plugins/swr/tests/test-model-meta.ts @@ -11,39 +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: {}, }, 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: {}, }, }, - uniqueConstraints: {}, deleteCascade: { user: ['Post'], }, diff --git a/packages/plugins/tanstack-query/tests/test-model-meta.ts b/packages/plugins/tanstack-query/tests/test-model-meta.ts index 41731ad18..71a657bad 100644 --- a/packages/plugins/tanstack-query/tests/test-model-meta.ts +++ b/packages/plugins/tanstack-query/tests/test-model-meta.ts @@ -11,39 +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: {}, }, 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: {}, }, }, - uniqueConstraints: {}, deleteCascade: { user: ['Post'], }, diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5ae027701..1d5c8fd37 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -56,8 +56,10 @@ "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", "semver": "^7.5.2", diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts index c381a5a88..a85392887 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -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 401caeaf2..9f767af0e 100644 --- a/packages/runtime/src/cross/model-meta.ts +++ b/packages/runtime/src/cross/model-meta.ts @@ -4,7 +4,14 @@ 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 }>; }; @@ -72,6 +79,11 @@ export type FieldInfo = { */ foreignKeyMapping?: Record; + /** + * Model from which the field is inherited + */ + inheritedFrom?: string; + /** * A function that provides a default value for the field */ @@ -90,23 +102,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; + + /** + * Base types + */ + baseTypes?: string[]; + + /** + * Fields + */ + fields: Record; + + /** + * Unique constraints + */ + uniqueConstraints?: Record; + + /** + * Attributes on the model + */ + attributes?: RuntimeAttribute[]; + /** - * Model fields + * Discriminator field name */ - fields: Record>; + discriminator?: string; +}; +/** + * ZModel data model metadata + */ +export type ModelMeta = { /** - * Model unique constraints + * Data models */ - uniqueConstraints: Record>; + models: Record; /** - * Information for cascading delete + * Mapping from model name to models that will be deleted because of it due to cascade delete */ - deleteCascade: Record; + deleteCascade?: Record; /** * Name of model that backs the `auth()` function @@ -117,8 +159,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]; } /** @@ -136,5 +178,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 477117dbd..db2455d7e 100644 --- a/packages/runtime/src/cross/nested-write-visitor.ts +++ b/packages/runtime/src/cross/nested-write-visitor.ts @@ -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 enumerate(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..bf501f020 100644 --- a/packages/runtime/src/cross/query-analyzer.ts +++ b/packages/runtime/src/cross/query-analyzer.ts @@ -81,7 +81,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; diff --git a/packages/runtime/src/cross/utils.ts b/packages/runtime/src/cross/utils.ts index e4237dbc7..1982513b3 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 } from '.'; +import { ModelInfo, 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) { - let fields = modelMeta.fields[lowerCaseFirst(model)]; + let fields = modelMeta.models[lowerCaseFirst(model)]?.fields; if (!fields) { if (throwIfNotFound) { throw new Error(`Unable to load fields for ${model}`); @@ -61,3 +61,19 @@ export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound } return result; } + +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/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index b137e03f9..dbca40874 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -1,8 +1,11 @@ +import colors from 'colors'; import semver from 'semver'; import { PRISMA_MINIMUM_VERSION } from '../constants'; -import { ModelMeta } from '../cross'; +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'; @@ -12,12 +15,12 @@ import type { PolicyDef, ZodSchemas } from './types'; /** * Kinds of enhancements to `PrismaClient` */ -export enum EnhancementKind { - Password = 'password', - Omit = 'omit', - Policy = 'policy', - DefaultAuth = 'defaultAuth', -} +export type EnhancementKind = 'password' | 'omit' | 'policy' | 'delegate'; + +/** + * All enhancement kinds + */ +const ALL_ENHANCEMENTS = ['password', 'omit', 'policy', 'delegate']; /** * Transaction isolation levels: https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-isolation-level @@ -121,6 +124,9 @@ export function createEnhancement( ); } + const logger = new Logger(prisma); + logger.info(`Enabled ZenStack enhancements: ${options.kinds?.join(', ')}`); + let result = prisma; if ( @@ -129,38 +135,48 @@ export function createEnhancement( hasOmit === undefined || hasDefaultAuth === undefined ) { - const allFields = Object.values(options.modelMeta.fields).flatMap((modelInfo) => Object.values(modelInfo)); + const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => + Object.values(modelInfo.fields) + ); hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password')); hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit')); hasDefaultAuth = allFields.some((field) => field.defaultValueProvider); } - const kinds = options.kinds ?? [ - EnhancementKind.Password, - EnhancementKind.Omit, - EnhancementKind.Policy, - EnhancementKind.DefaultAuth, - ]; + const kinds = options.kinds ?? ALL_ENHANCEMENTS; + + // 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')) { + console.warn( + colors.yellow( + 'Your ZModel contains delegate models but "delegate" enhancement kind is not enabled. This may result in unexpected behavior.' + ) + ); + } else { + result = withDelegate(result, options); + } + } - if (hasPassword && kinds.includes(EnhancementKind.Password)) { + // policy proxy + if (kinds.includes('policy')) { + result = withPolicy(result, options, context); + if (hasDefaultAuth) { + // @default(auth()) proxy + result = withDefaultAuth(result, options, context); + } + } + + if (hasPassword && kinds.includes('password')) { // @password proxy result = withPassword(result, options); } - if (hasOmit && kinds.includes(EnhancementKind.Omit)) { + if (hasOmit && kinds.includes('omit')) { // @omit proxy result = withOmit(result, options); } - if (hasDefaultAuth && kinds.includes(EnhancementKind.DefaultAuth)) { - // @default(auth()) proxy - result = withDefaultAuth(result, options, context); - } - - // policy proxy - if (kinds.includes(EnhancementKind.Policy)) { - result = withPolicy(result, options, context); - } - return result; } diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts index 48af0ed73..9e0a64a4f 100644 --- a/packages/runtime/src/enhancements/default-auth.ts +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -26,17 +26,15 @@ export function withDefaultAuth( } class DefaultAuthHandler extends DefaultPrismaProxyHandler { - private readonly db: DbClientContract; private readonly userContext: any; constructor( prisma: DbClientContract, model: string, - private readonly options: EnhancementOptions, + options: EnhancementOptions, private readonly context?: EnhancementContext ) { - super(prisma, model); - this.db = prisma; + super(prisma, model, options); if (!this.context?.user) { throw new Error(`Using \`auth()\` in \`@default\` requires a user context`); diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts new file mode 100644 index 000000000..0a1e39d8c --- /dev/null +++ b/packages/runtime/src/enhancements/delegate.ts @@ -0,0 +1,1133 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import deepcopy from 'deepcopy'; +import deepmerge from 'deepmerge'; +import { lowerCaseFirst } from 'lower-case-first'; +import { DELEGATE_AUX_RELATION_PREFIX } from '../constants'; +import { + FieldInfo, + ModelInfo, + NestedWriteVisitor, + enumerate, + getIdFields, + getModelInfo, + isDelegateModel, + requireField, + resolveField, +} from '../cross'; +import type { CrudContract, DbClientContract } from '../types'; +import type { EnhancementOptions } 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: EnhancementOptions): 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: EnhancementOptions) { + 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 (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(where: any) { + if (!where) { + return undefined; + } + + where = deepcopy(where); + Object.entries(where).forEach(([field, value]) => { + const fieldInfo = resolveField(this.options.modelMeta, this.model, field); + if (!fieldInfo?.inheritedFrom) { + return; + } + + let base = this.getBaseModel(this.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])) { + if (value !== undefined) { + if (this.injectBaseFieldSelect(model, field, value, args, kind)) { + delete args[kind][field]; + } else { + const fieldInfo = resolveField(this.options.modelMeta, model, field); + 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) { + this.injectBaseIncludeRecursively(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); + } + 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]); + } + + // #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, 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, + }; + 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(args.cursor); + } + + if (args.orderBy) { + args.orderBy = this.buildWhereHierarchy(args.orderBy); + } + + if (args.where) { + args.where = this.buildWhereHierarchy(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(args.cursor); + } + + if (args?.where) { + args.where = this.buildWhereHierarchy(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(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 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.assembleHierarchy(base.name, baseData); + Object.assign(result, baseAssembled); + } + } + + const modelInfo = getModelInfo(this.options.modelMeta, model, true); + + for (const field of Object.values(modelInfo.fields)) { + if (field.inheritedFrom) { + // already merged from base + continue; + } + + if (field.name in entity) { + const fieldValue = entity[field.name]; + if (field.isDataModel) { + if (Array.isArray(fieldValue)) { + result[field.name] = fieldValue.map((item) => this.assembleHierarchy(field.type, item)); + } else { + result[field.name] = this.assembleHierarchy(field.type, fieldValue); + } + } else { + result[field.name] = fieldValue; + } + } + } + + return result; + } + + // #endregion + + // #region backup + + private transformWhereHierarchy(where: any, contextModel: ModelInfo, forModel: ModelInfo) { + if (!where || typeof where !== 'object') { + return where; + } + + let curr: ModelInfo | undefined = contextModel; + const inheritStack: ModelInfo[] = []; + while (curr) { + inheritStack.unshift(curr); + curr = this.getBaseModel(curr.name); + } + + let result: any = {}; + for (const [key, value] of Object.entries(where)) { + const fieldInfo = requireField(this.options.modelMeta, contextModel.name, key); + const fieldHierarchy = this.transformFieldHierarchy(fieldInfo, value, contextModel, forModel, inheritStack); + result = deepmerge(result, fieldHierarchy); + } + + return result; + } + + private transformFieldHierarchy( + fieldInfo: FieldInfo, + value: unknown, + contextModel: ModelInfo, + forModel: ModelInfo, + inheritStack: ModelInfo[] + ): any { + const fieldModel = fieldInfo.inheritedFrom ? this.getModelInfo(fieldInfo.inheritedFrom) : contextModel; + if (fieldModel === forModel) { + return { [fieldInfo.name]: value }; + } + + const fieldModelPos = inheritStack.findIndex((m) => m === fieldModel); + const forModelPos = inheritStack.findIndex((m) => m === forModel); + const result: any = {}; + let curr = result; + + if (fieldModelPos > forModelPos) { + // walk down hierarchy + for (let i = forModelPos + 1; i <= fieldModelPos; i++) { + const rel = this.makeAuxRelationName(inheritStack[i]); + curr[rel] = {}; + curr = curr[rel]; + } + } else { + // walk up hierarchy + for (let i = forModelPos - 1; i >= fieldModelPos; i--) { + const rel = this.makeAuxRelationName(inheritStack[i]); + curr[rel] = {}; + curr = curr[rel]; + } + } + + curr[fieldInfo.name] = value; + return result; + } + + // #endregion +} diff --git a/packages/runtime/src/enhancements/policy/logger.ts b/packages/runtime/src/enhancements/logger.ts similarity index 100% rename from packages/runtime/src/enhancements/policy/logger.ts rename to packages/runtime/src/enhancements/logger.ts diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index bedbf5458..e05a8a769 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -21,8 +21,8 @@ export function withOmit(prisma: DbClient, options: Enh } class OmitHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, private readonly options: EnhancementOptions) { - super(prisma, model); + constructor(prisma: DbClientContract, model: string, options: EnhancementOptions) { + super(prisma, model, options); } // base override diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index db8e3181b..7fef04dd8 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -23,8 +23,8 @@ export function withPassword(prisma: DbClient, op } class PasswordHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, private readonly options: EnhancementOptions) { - super(prisma, model); + constructor(prisma: DbClientContract, model: string, options: EnhancementOptions) { + super(prisma, model, options); } // base override diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 65207abea..1bc60a647 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -16,11 +16,12 @@ import { type FieldInfo, type ModelMeta, } from '../../cross'; -import { DbClientContract, DbOperations, PolicyOperationKind } from '../../types'; +import { type CrudContract, type DbClientContract, PolicyOperationKind } from '../../types'; import type { EnhancementContext, EnhancementOptions } from '../create-enhancement'; +import { Logger } from '../logger'; import { PrismaProxyHandler } from '../proxy'; +import { QueryUtils } from '../query-utils'; import { formatObject, prismaClientValidationError } from '../utils'; -import { Logger } from './logger'; import { PolicyUtil } from './policy-utils'; import { createDeferredPromise } from './promise'; @@ -39,14 +40,11 @@ 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 modelMeta: ModelMeta; private readonly prismaModule: any; - private readonly logPrismaQuery?: boolean; - - private readonly DEFAULT_TX_MAXWAIT = 100000; - private readonly DEFAULT_TX_TIMEOUT = 100000; + private readonly queryUtils: QueryUtils; constructor( private readonly prisma: DbClient, @@ -57,9 +55,10 @@ export class PolicyProxyHandler implements Pr this.logger = new Logger(prisma); this.model = lowerCaseFirst(model); - ({ modelMeta: this.modelMeta, logPrismaQuery: this.logPrismaQuery, prismaModule: this.prismaModule } = options); + ({ modelMeta: this.modelMeta, prismaModule: this.prismaModule } = options); - this.utils = new PolicyUtil(prisma, options, context, this.shouldLogQuery); + this.policyUtils = new PolicyUtil(prisma, options, context, this.shouldLogQuery); + this.queryUtils = new QueryUtils(prisma, options); } private get modelClient() { @@ -96,7 +95,7 @@ export class PolicyProxyHandler implements Pr ); } return this.findWithFluentCallStubs(args, 'findUniqueOrThrow', true, () => { - throw this.utils.notFound(this.model); + throw this.policyUtils.notFound(this.model); }); } @@ -106,7 +105,7 @@ export class PolicyProxyHandler implements Pr findFirstOrThrow(args: any) { return this.findWithFluentCallStubs(args, 'findFirstOrThrow', true, () => { - throw this.utils.notFound(this.model); + throw this.policyUtils.notFound(this.model); }); } @@ -129,12 +128,15 @@ export class PolicyProxyHandler implements Pr private 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 = this.policyUtils.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)}`); @@ -143,7 +145,7 @@ export class PolicyProxyHandler implements Pr return new Promise((resolve, reject) => { this.modelClient[actionName](_args).then( (value: any) => { - this.utils.postProcessForRead(value, this.model, origArgs); + this.policyUtils.postProcessForRead(value, this.model, origArgs); resolve(value); }, (err: any) => reject(err) @@ -154,14 +156,14 @@ export class PolicyProxyHandler implements Pr // returns a fluent API call function private fluentCall(filter: any, fieldInfo: FieldInfo, rootPromise?: Promise) { return (args: any) => { - args = this.utils.clone(args); + args = this.policyUtils.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); + args.where = this.policyUtils.and(args.where, condition); const promise = createDeferredPromise(() => { // Promise for fetching @@ -207,7 +209,7 @@ export class PolicyProxyHandler implements Pr // add fluent API functions to the given promise private addFluentFunctions(promise: any, model: string, filter: any, rootPromise?: Promise) { - const fields = this.utils.getModelFields(model); + const fields = this.policyUtils.getModelFields(model); if (fields) { for (const [field, fieldInfo] of Object.entries(fields)) { if (fieldInfo.isDataModel) { @@ -233,20 +235,25 @@ export class PolicyProxyHandler implements Pr ); } - this.utils.tryReject(this.prisma, this.model, 'create'); + this.policyUtils.tryReject(this.prisma, this.model, 'create'); const origArgs = args; - args = this.utils.clone(args); + args = this.policyUtils.clone(args); // static input policy check for top-level create data - const inputCheck = this.utils.checkInputGuard(this.model, args.data, 'create'); + const inputCheck = this.policyUtils.checkInputGuard(this.model, args.data, 'create'); if (inputCheck === false) { - throw this.utils.deniedByPolicy(this.model, 'create', undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); + throw this.policyUtils.deniedByPolicy( + this.model, + 'create', + undefined, + CrudFailureReason.ACCESS_POLICY_VIOLATION + ); } const hasNestedCreateOrConnect = await this.hasNestedCreateOrConnect(args); - const { result, error } = await this.transaction(async (tx) => { + 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 && @@ -259,7 +266,7 @@ export class PolicyProxyHandler implements Pr 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) }; + const createArgs: any = { data: args.data, select: this.policyUtils.makeIdSelection(this.model) }; if (this.shouldLogQuery) { this.logger.info(`[policy] \`create\` ${this.model}: ${formatObject(createArgs)}`); @@ -267,7 +274,7 @@ export class PolicyProxyHandler implements Pr const result = await tx[this.model].create(createArgs); // filter the read-back data - return this.utils.readBack(tx, this.model, 'create', args, result); + 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); @@ -276,7 +283,7 @@ export class PolicyProxyHandler implements Pr await this.runPostWriteChecks(postWriteChecks, tx); // filter the read-back data - return this.utils.readBack(tx, this.model, 'create', origArgs, result); + return this.policyUtils.readBack(tx, this.model, 'create', origArgs, result); } }); @@ -288,7 +295,7 @@ export class PolicyProxyHandler implements Pr } // 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) => { @@ -323,12 +330,12 @@ 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`); } 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) { @@ -336,7 +343,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); } } @@ -370,18 +377,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); } } }, @@ -426,7 +433,7 @@ export class PolicyProxyHandler implements Pr }); // return only the ids of the top-level entity - const ids = this.utils.getEntityIds(this.model, result); + const ids = this.policyUtils.getEntityIds(this.model, result); return { result: ids, postWriteChecks: [...postCreateChecks.values()] }; } @@ -463,11 +470,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) { const parseResult = schema.safeParse(data); if (!parseResult.success) { - throw this.utils.deniedByPolicy( + throw this.policyUtils.deniedByPolicy( model, 'create', `input failed validation: ${fromZodError(parseResult.error)}`, @@ -490,16 +497,16 @@ export class PolicyProxyHandler implements Pr ); } - this.utils.tryReject(this.prisma, this.model, 'create'); + this.policyUtils.tryReject(this.prisma, this.model, 'create'); - args = this.utils.clone(args); + args = this.policyUtils.clone(args); // do static input validation and check if post-create checks are needed let needPostCreateCheck = false; for (const item of enumerate(args.data)) { - const inputCheck = this.utils.checkInputGuard(this.model, item, 'create'); + const inputCheck = this.policyUtils.checkInputGuard(this.model, item, 'create'); if (inputCheck === false) { - throw this.utils.deniedByPolicy( + throw this.policyUtils.deniedByPolicy( this.model, 'create', undefined, @@ -518,7 +525,7 @@ export class PolicyProxyHandler implements Pr return this.modelClient.createMany(args); } else { // create entities in a transaction with post-create checks - return this.transaction(async (tx) => { + 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); @@ -527,11 +534,7 @@ export class PolicyProxyHandler implements Pr } } - 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. @@ -549,7 +552,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 }); }) ); @@ -566,18 +569,18 @@ export class PolicyProxyHandler implements Pr }; } - private async hasDuplicatedUniqueConstraint(model: string, createData: any, db: Record) { + private async hasDuplicatedUniqueConstraint(model: string, createData: 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 // 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)) { if (constraint.fields.every((f) => createData[f] !== undefined)) { const uniqueFilter = constraint.fields.reduce((acc, f) => ({ ...acc, [f]: createData[f] }), {}); - const existing = await this.utils.checkExistence(db, model, uniqueFilter); + const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter); if (existing) { return true; } @@ -615,9 +618,9 @@ export class PolicyProxyHandler implements Pr ); } - args = this.utils.clone(args); + args = this.policyUtils.clone(args); - const { result, error } = await this.transaction(async (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); @@ -625,7 +628,7 @@ export class PolicyProxyHandler implements Pr await this.runPostWriteChecks(postWriteChecks, tx); // filter the read-back data - return this.utils.readBack(tx, this.model, 'update', args, result); + return this.policyUtils.readBack(tx, this.model, 'update', args, result); }); if (error) { @@ -635,17 +638,17 @@ export class PolicyProxyHandler implements Pr } } - private async doUpdate(args: any, db: Record) { + private async doUpdate(args: any, db: CrudContract) { // collected post-update checks const postWriteChecks: PostWriteCheckRecord[] = []; // registers a post-update check task const _registerPostUpdateCheck = async (model: string, uniqueFilter: 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: uniqueFilter, select: preValueSelect }); } @@ -672,7 +675,7 @@ export class PolicyProxyHandler implements Pr const unsafe = this.isUnsafeMutate(model, args); // 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 @@ -707,7 +710,7 @@ 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 @@ -757,8 +760,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 = {}; @@ -770,7 +773,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); @@ -782,10 +785,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; @@ -808,13 +811,13 @@ 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); // handles the case where id fields are updated - const ids = this.utils.clone(existing); + const ids = this.policyUtils.clone(existing); for (const key of Object.keys(existing)) { const updateValue = (args as any).data ? (args as any).data[key] : (args as any)[key]; if ( @@ -833,15 +836,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( @@ -860,15 +863,15 @@ export class PolicyProxyHandler implements Pr ); } - 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({ @@ -906,15 +909,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, uniqueFilter, 'update', db, args); + await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); // register post-update check await _registerPostUpdateCheck(model, uniqueFilter); @@ -943,7 +946,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); @@ -957,9 +960,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) { @@ -976,25 +979,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 })}`); } @@ -1013,7 +1016,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 }; @@ -1025,7 +1028,7 @@ export class PolicyProxyHandler implements Pr } for (const k of Object.keys(args)) { const field = resolveField(this.modelMeta, model, k); - if (this.isAutoIncrementIdField(field) || field?.isForeignKey) { + if (field && (this.isAutoIncrementIdField(field) || field.isForeignKey)) { return true; } } @@ -1048,23 +1051,23 @@ export class PolicyProxyHandler implements Pr ); } - this.utils.tryReject(this.prisma, this.model, 'update'); + this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.utils.clone(args); - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); + args = this.policyUtils.clone(args); + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); - if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) { + 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.transaction(async (tx) => { + return this.queryUtils.transaction(this.prisma, async (tx) => { // collect pre-update values - let select = this.utils.makeIdSelection(this.model); - const preValueSelect = this.utils.getPreValueSelect(this.model); + 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.utils.injectAuthGuardAsWhere(tx, currentSetQuery, this.model, 'read'); + this.policyUtils.injectAuthGuardAsWhere(tx, currentSetQuery, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`findMany\` ${this.model}: ${formatObject(currentSetQuery)}`); @@ -1075,7 +1078,7 @@ export class PolicyProxyHandler implements Pr ...currentSet.map((preValue) => ({ model: this.model, operation: 'postUpdate' as PolicyOperationKind, - uniqueFilter: this.utils.getEntityIds(this.model, preValue), + uniqueFilter: this.policyUtils.getEntityIds(this.model, preValue), preValue: preValueSelect ? preValue : undefined, })) ); @@ -1123,28 +1126,28 @@ export class PolicyProxyHandler implements Pr ); } - this.utils.tryReject(this.prisma, this.model, 'create'); - this.utils.tryReject(this.prisma, this.model, 'update'); + this.policyUtils.tryReject(this.prisma, this.model, 'create'); + this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.utils.clone(args); + args = this.policyUtils.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. - const { result, error } = await this.transaction(async (tx) => { + const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { const { where, create, update, ...rest } = args; - const existing = await this.utils.checkExistence(tx, this.model, args.where); + const existing = await this.policyUtils.checkExistence(tx, this.model, args.where); if (existing) { // update case const { result, postWriteChecks } = await this.doUpdate({ where, data: update, ...rest }, tx); await this.runPostWriteChecks(postWriteChecks, tx); - return this.utils.readBack(tx, this.model, 'update', args, result); + 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.utils.readBack(tx, this.model, 'create', args, result); + return this.policyUtils.readBack(tx, this.model, 'create', args, result); } }); @@ -1174,19 +1177,19 @@ export class PolicyProxyHandler implements Pr ); } - this.utils.tryReject(this.prisma, this.model, 'delete'); + this.policyUtils.tryReject(this.prisma, this.model, 'delete'); - const { result, error } = await this.transaction(async (tx) => { + const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { // do a read-back before delete - const r = await this.utils.readBack(tx, this.model, 'delete', args, args.where); + 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); + await this.policyUtils.checkExistence(tx, this.model, args.where, true); // inject delete guard - await this.utils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args); + await this.policyUtils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args); // proceed with the deletion if (this.shouldLogQuery) { @@ -1205,11 +1208,11 @@ export class PolicyProxyHandler implements Pr } async deleteMany(args: any) { - this.utils.tryReject(this.prisma, this.model, 'delete'); + this.policyUtils.tryReject(this.prisma, this.model, 'delete'); // inject policy conditions args = args ?? {}; - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete'); + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete'); // conduct the deletion if (this.shouldLogQuery) { @@ -1227,10 +1230,10 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } - args = this.utils.clone(args); + args = this.policyUtils.clone(args); // inject policy conditions - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${formatObject(args)}`); @@ -1243,10 +1246,10 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } - args = this.utils.clone(args); + args = this.policyUtils.clone(args); // inject policy conditions - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${formatObject(args)}`); @@ -1256,8 +1259,8 @@ export class PolicyProxyHandler implements Pr async count(args: any) { // inject policy conditions - args = args ? this.utils.clone(args) : {}; - this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + args = args ? this.policyUtils.clone(args) : {}; + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { this.logger.info(`[policy] \`count\` ${this.model}:\n${formatObject(args)}`); @@ -1270,8 +1273,8 @@ export class PolicyProxyHandler implements Pr //#region Subscribe (Prisma Pulse) async subscribe(args: any) { - const readGuard = this.utils.getAuthGuard(this.prisma, this.model, 'read'); - if (this.utils.isTrue(readGuard)) { + 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)}`); @@ -1290,22 +1293,22 @@ export class PolicyProxyHandler implements Pr // include all args = { create: {}, update: {}, delete: {} }; } else { - args = this.utils.clone(args); + args = this.policyUtils.clone(args); } } // inject into subscribe conditions if (args.create) { - args.create.after = this.utils.and(args.create.after, readGuard); + args.create.after = this.policyUtils.and(args.create.after, readGuard); } if (args.update) { - args.update.after = this.utils.and(args.update.after, readGuard); + args.update.after = this.policyUtils.and(args.update.after, readGuard); } if (args.delete) { - args.delete.before = this.utils.and(args.delete.before, readGuard); + args.delete.before = this.policyUtils.and(args.delete.before, readGuard); } if (this.shouldLogQuery) { @@ -1322,23 +1325,10 @@ export class PolicyProxyHandler implements Pr return !!this.options?.logPrismaQuery && this.logger.enabled('info'); } - private transaction(action: (tx: Record) => Promise) { - if (this.prisma['$transaction']) { - return this.prisma.$transaction((tx) => action(tx), { - maxWait: this.options.transactionMaxWait, - timeout: this.options.transactionTimeout, - isolationLevel: this.options.transactionIsolationLevel, - }); - } 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) ) ); } diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index b7e3448c8..50ef3a3bc 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -16,33 +16,19 @@ 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, EnhancementOptions } 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 { +export class PolicyUtil extends QueryUtils { private readonly logger: Logger; private readonly modelMeta: ModelMeta; private readonly policy: PolicyDef; @@ -56,6 +42,8 @@ export class PolicyUtil { context?: EnhancementContext, private readonly shouldLogQuery = false ) { + super(db, options); + this.logger = new Logger(db); this.user = context?.user; @@ -248,7 +236,7 @@ export class PolicyUtil { * @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) { + getAuthGuard(db: CrudContract, model: string, operation: PolicyOperationKind, preValue?: any) { const guard = this.policy.guard[lowerCaseFirst(model)]; if (!guard) { throw this.unknownError(`unable to load policy guard for ${model}`); @@ -269,7 +257,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}`]; @@ -289,7 +277,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}`]; @@ -309,7 +297,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}`]; @@ -365,7 +353,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) { @@ -413,7 +401,7 @@ export class PolicyUtil { } private injectGuardForRelationFields( - db: Record, + db: CrudContract, model: string, payload: any, operation: PolicyOperationKind @@ -437,7 +425,7 @@ export class PolicyUtil { } private injectGuardForToManyField( - db: Record, + db: CrudContract, fieldInfo: FieldInfo, payload: { some?: any; every?: any; none?: any }, operation: PolicyOperationKind @@ -471,7 +459,7 @@ export class PolicyUtil { } private injectGuardForToOneField( - db: Record, + db: CrudContract, fieldInfo: FieldInfo, payload: { is?: any; isNot?: any } & Record, operation: PolicyOperationKind @@ -501,7 +489,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')) { @@ -539,111 +527,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; - } - } - } - } - } - } - /** * 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 }; - } - 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 []; @@ -736,7 +627,7 @@ export class PolicyUtil { model: string, uniqueFilter: any, operation: PolicyOperationKind, - db: Record, + db: CrudContract, args: any, preValue?: any ) { @@ -830,7 +721,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 @@ -853,7 +744,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 = []; @@ -912,7 +803,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)) { throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); @@ -922,12 +813,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); @@ -948,7 +834,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 }, @@ -1059,7 +945,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]) => { @@ -1106,16 +992,6 @@ export class PolicyUtil { }); } - validationError(message: string) { - return prismaClientValidationError(this.db, this.prismaModule, message); - } - - unknownError(message: string) { - return prismaClientUnknownRequestError(this.db, this.prismaModule, message, { - clientVersion: getVersion(), - }); - } - //#endregion //#region Misc @@ -1264,22 +1140,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. */ @@ -1300,33 +1160,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'); diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index c735d595a..e0302f7e9 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -3,6 +3,7 @@ import { PRISMA_PROXY_ENHANCER } from '../constants'; import type { ModelMeta } from '../cross'; import type { DbClientContract } from '../types'; +import { EnhancementOptions } from './create-enhancement'; import { createDeferredPromise } from './policy/promise'; /** @@ -31,7 +32,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,7 +64,11 @@ 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: EnhancementOptions + ) {} async findUnique(args: any): Promise { args = await this.preprocessArgs('findUnique', args); @@ -101,9 +106,9 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { return this.processResultEntity(r); } - async createMany(args: any, skipDuplicates?: boolean | undefined): Promise<{ count: number }> { + async createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> { args = await this.preprocessArgs('createMany', args); - return this.prisma[this.model].createMany(args, skipDuplicates); + return this.prisma[this.model].createMany(args); } async update(args: any): Promise { @@ -182,7 +187,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) => { diff --git a/packages/runtime/src/enhancements/query-utils.ts b/packages/runtime/src/enhancements/query-utils.ts new file mode 100644 index 000000000..f92353081 --- /dev/null +++ b/packages/runtime/src/enhancements/query-utils.ts @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + FieldInfo, + NestedWriteVisitorContext, + getIdFields, + getModelInfo, + getUniqueConstraints, + resolveField, +} from '../cross'; +import { CrudContract, DbClientContract } from '../types'; +import { getVersion } from '../version'; +import { EnhancementOptions } from './create-enhancement'; +import { prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; + +export class QueryUtils { + constructor(private readonly prisma: DbClientContract, private readonly options: EnhancementOptions) {} + + 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); + } + } + + buildReversedQuery(context: NestedWriteVisitorContext, mutating = 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 && !mutating) { + // 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 + mutating && + // 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 }; + } + currQuery = currQuery[currField.backLink]; + } + currField = field; + } + } + return result; + } + + flattenGeneratedUniqueField(model: string, args: any) { + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + 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 4dcfa1c1a..53410a196 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 @@ -24,7 +24,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 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/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 49923284a..3a92d393c 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -85,12 +85,19 @@ export async function loadDocument(fileName: string): Promise { const model = document.parseResult.value as Model; - mergeImportsDeclarations(langiumDocuments, model); + const imported = mergeImportsDeclarations(langiumDocuments, model); + // remove imported documents + await services.shared.workspace.DocumentBuilder.update( + [], + imported.map((m) => m.$document!.uri) + ); validationAfterMerge(model); mergeBaseModel(model, services.references.Linker); + await relinkAll(model, services); + return model; } @@ -151,6 +158,8 @@ export function mergeImportsDeclarations(documents: LangiumDocuments, model: Mod }); model.declarations.push(...importedDeclarations); + + return importedModels; } export async function getPluginDocuments(services: ZModelServices, fileName: string): Promise { @@ -295,3 +304,20 @@ 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 the document + const newDoc = services.shared.workspace.LangiumDocumentFactory.fromModel(model, doc.uri); + (model as Mutable).$document = newDoc; + + // rebuild the document + await services.shared.workspace.DocumentBuilder.build([newDoc], { validationChecks: 'all' }); +} 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 33ec0ff37..3e4517444 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -6,14 +6,9 @@ import { isStringLiteral, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { - analyzePolicies, - getLiteral, - getModelFieldsWithBases, - getModelIdFields, - getModelUniqueFields, -} from '@zenstackhq/sdk'; +import { analyzePolicies, 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'; @@ -26,7 +21,7 @@ import { validateDuplicatedDeclarations } from './utils'; export default class DataModelValidator implements AstValidator { validate(dm: DataModel, accept: ValidationAcceptor): void { this.validateBaseAbstractModel(dm, accept); - validateDuplicatedDeclarations(getModelFieldsWithBases(dm), accept); + validateDuplicatedDeclarations(dm, getModelFieldsWithBases(dm), accept); this.validateAttributes(dm, accept); this.validateFields(dm, accept); } @@ -224,6 +219,11 @@ export default class DataModelValidator implements AstValidator { 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; @@ -265,7 +265,7 @@ export default class DataModelValidator implements AstValidator { return; } else if (oppositeFields.length > 1) { oppositeFields - .filter((x) => !x.$inheritedFrom) + .filter((f) => f.$container !== contextModel) .forEach((f) => { if (this.isSelfRelation(f)) { // self relations are partial @@ -368,12 +368,19 @@ 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, + } + ); }); } } 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/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts index b80bf890d..d3722638e 100644 --- a/packages/schema/src/language-server/validator/schema-validator.ts +++ b/packages/schema/src/language-server/validator/schema-validator.ts @@ -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); diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 340f471b8..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 { @@ -34,7 +34,7 @@ export function validateDuplicatedDeclarations( if (decls.length > 1) { let errorField = decls[1]; if (isDataModelField(decls[0])) { - const nonInheritedFields = decls.filter((x) => !(x as DataModelField).$inheritedFrom); + 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 8f60cbe69..5b6a6c95a 100644 --- a/packages/schema/src/language-server/zmodel-code-action.ts +++ b/packages/schema/src/language-server/zmodel-code-action.ts @@ -10,8 +10,8 @@ import { getDocument, } from 'langium'; -import { getModelFieldsWithBases } from '@zenstackhq/sdk'; import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver'; +import { getModelFieldsWithBases } from '../utils/ast-utils'; import { IssueCodes } from './constants'; import { MissingOppositeRelationData } from './validator/datamodel-validator'; import { ZModelFormatter } from './zmodel-formatter'; diff --git a/packages/schema/src/language-server/zmodel-completion-provider.ts b/packages/schema/src/language-server/zmodel-completion-provider.ts index 742f7087f..e100c870a 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(item); }; - super.completionForCrossReference(context, crossRef, customAcceptor); + return super.completionForCrossReference(context, crossRef, customAcceptor); } override completionForKeyword( @@ -174,7 +174,7 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { } acceptor(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-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 30929791f..5ab841c96 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -470,12 +470,12 @@ export class ZModelLinker extends DefaultLinker { } private resolveDataModel(node: DataModel, document: LangiumDocument, extraScopes: ScopeProvider[]) { - if (node.superTypes.length > 0) { - const providers = node.superTypes.map( - (superType) => (name: string) => superType.ref?.fields.find((f) => f.name === name) - ); - extraScopes = [...providers, ...extraScopes]; - } + // if (node.superTypes.length > 0) { + // const providers = node.superTypes.map( + // (superType) => (name: string) => superType.ref?.fields.find((f) => f.name === name) + // ); + // extraScopes = [...providers, ...extraScopes]; + // } return this.resolveDefault(node, document, extraScopes); } diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 21304fa4a..9d685db27 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -16,7 +16,6 @@ import { getModelFieldsWithBases, getRecursiveBases, isAuthInvocation, - isFutureExpr, } from '@zenstackhq/sdk'; import { AstNode, @@ -38,7 +37,7 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { isCollectionPredicate, resolveImportUri } from '../utils/ast-utils'; +import { isCollectionPredicate, isFutureInvocation, resolveImportUri } from '../utils/ast-utils'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; /** @@ -76,7 +75,7 @@ export class ZModelScopeComputation extends DefaultScopeComputation { override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes) { super.processNode(node, document, scopes); - if (isDataModel(node)) { + if (isDataModel(node) && !node.$baseMerged) { // add base fields to the scope recursively const bases = getRecursiveBases(node); for (const base of bases) { @@ -164,7 +163,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // resolve to `User` or `@@auth` model return this.createScopeForAuthModel(node, globalScope); } - if (isFutureExpr(operand)) { + if (isFutureInvocation(operand)) { // resolve `future()` to the containing model return this.createScopeForContainingModel(node, globalScope); } diff --git a/packages/schema/src/plugins/enhancer/delegate/index.ts b/packages/schema/src/plugins/enhancer/delegate/index.ts new file mode 100644 index 000000000..5e4cffdfa --- /dev/null +++ b/packages/schema/src/plugins/enhancer/delegate/index.ts @@ -0,0 +1,16 @@ +import { type PluginOptions } from '@zenstackhq/sdk'; +import type { Model } from '@zenstackhq/sdk/ast'; +import type { Project } from 'ts-morph'; +import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; +import path from 'path'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + const prismaGenerator = new PrismaSchemaGenerator(); + await prismaGenerator.generate(model, { + provider: '@internal', + schemaPath: options.schemaPath, + output: path.join(outDir, 'delegate.prisma'), + overrideClientGenerationPath: path.join(outDir, '.delegate'), + mode: 'logical', + }); +} 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..c33de08b0 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -0,0 +1,215 @@ +import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; +import { + getAttribute, + getDataModels, + getPrismaClientImportSpec, + isDelegateModel, + type PluginOptions, +} from '@zenstackhq/sdk'; +import { DataModelField, isDataModel, isReferenceExpr, type DataModel, type Model } from '@zenstackhq/sdk/ast'; +import path from 'path'; +import { + ForEachDescendantTraversalControl, + MethodSignature, + Node, + Project, + PropertySignature, + SyntaxKind, + TypeAliasDeclaration, +} from 'ts-morph'; +import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; + +export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { + const outFile = path.join(outDir, 'enhance.ts'); + let logicalPrismaClientDir: string | undefined; + + if (hasDelegateModel(model)) { + logicalPrismaClientDir = await generateLogicalPrisma(model, options, outDir); + } + + project.createSourceFile( + outFile, + `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; +import modelMeta from './model-meta'; +import policy from './policy'; +${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} +import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; +${logicalPrismaClientDir ? `import { PrismaClient as EnhancedPrismaClient } from '${logicalPrismaClientDir}';` : ''} + +export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions) { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: Prisma, + ...options + }, context)${logicalPrismaClientDir ? ' as EnhancedPrismaClient' : ''}; +} +`, + { overwrite: true } + ); +} + +function hasDelegateModel(model: Model) { + const dataModels = getDataModels(model); + return dataModels.some( + (dm) => isDelegateModel(dm) && dataModels.some((sub) => sub.superTypes.some((base) => base.ref === dm)) + ); +} + +async function generateLogicalPrisma(model: Model, options: PluginOptions, outDir: string) { + const prismaGenerator = new PrismaSchemaGenerator(); + const prismaClientOutDir = './.delegate'; + await prismaGenerator.generate(model, { + provider: '@internal', + schemaPath: options.schemaPath, + output: path.join(outDir, 'delegate.prisma'), + overrideClientGenerationPath: prismaClientOutDir, + mode: 'logical', + }); + + await processClientTypes(model, path.join(outDir, prismaClientOutDir)); + return prismaClientOutDir; +} + +async function processClientTypes(model: Model, prismaClientDir: string) { + const project = new Project(); + const sf = project.addSourceFileAtPath(path.join(prismaClientDir, 'index.d.ts')); + + const delegateModels: [DataModel, DataModel[]][] = []; + model.declarations + .filter((d): d is DataModel => isDelegateModel(d)) + .forEach((dm) => { + delegateModels.push([ + dm, + model.declarations.filter( + (d): d is DataModel => isDataModel(d) && d.superTypes.some((s) => s.ref === dm) + ), + ]); + }); + + const toRemove: (PropertySignature | MethodSignature)[] = []; + const toReplaceText: [TypeAliasDeclaration, string][] = []; + + sf.forEachDescendant((desc, traversal) => { + removeAuxRelationFields(desc, toRemove, traversal); + fixDelegateUnionType(desc, delegateModels, toReplaceText, traversal); + removeCreateFromDelegateInputTypes(desc, delegateModels, toRemove, traversal); + removeToplevelCreates(desc, delegateModels, toRemove, traversal); + }); + + toRemove.forEach((n) => n.remove()); + toReplaceText.forEach(([node, text]) => node.replaceWithText(text)); + + await project.save(); +} + +function removeAuxRelationFields( + desc: Node, + toRemove: (PropertySignature | MethodSignature)[], + traversal: ForEachDescendantTraversalControl +) { + if (desc.isKind(SyntaxKind.PropertySignature) || desc.isKind(SyntaxKind.MethodSignature)) { + // remove aux fields + const name = desc.getName(); + + if (name.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { + toRemove.push(desc); + traversal.skip(); + } + } +} + +function fixDelegateUnionType( + desc: Node, + delegateModels: [DataModel, DataModel[]][], + toReplaceText: [TypeAliasDeclaration, string][], + traversal: ForEachDescendantTraversalControl +) { + if (!desc.isKind(SyntaxKind.TypeAliasDeclaration)) { + return; + } + + const name = desc.getName(); + delegateModels.forEach(([delegate, concreteModels]) => { + if (name === `$${delegate.name}Payload`) { + const discriminator = getDiscriminatorField(delegate); + // const discriminator = 'delegateType'; // delegate.fields.find((f) => hasAttribute(f, '@discriminator')); + if (discriminator) { + toReplaceText.push([ + desc, + `export type ${name} = + ${concreteModels + .map((m) => `($${m.name}Payload & { scalars: { ${discriminator.name}: '${m.name}' } })`) + .join(' | ')};`, + ]); + traversal.skip(); + } + } + }); +} + +function removeCreateFromDelegateInputTypes( + desc: Node, + delegateModels: [DataModel, DataModel[]][], + toRemove: (PropertySignature | MethodSignature)[], + traversal: ForEachDescendantTraversalControl +) { + if (!desc.isKind(SyntaxKind.TypeAliasDeclaration)) { + return; + } + + const name = desc.getName(); + delegateModels.forEach(([delegate]) => { + // remove create related sub-payload from delegate's input types since they cannot be created directly + const regex = new RegExp(`\\${delegate.name}(Unchecked)?(Create|Update).*Input`); + if (regex.test(name)) { + desc.forEachDescendant((d, innerTraversal) => { + if ( + d.isKind(SyntaxKind.PropertySignature) && + ['create', 'upsert', 'connectOrCreate'].includes(d.getName()) + ) { + toRemove.push(d); + innerTraversal.skip(); + } + }); + traversal.skip(); + } + }); +} + +function removeToplevelCreates( + desc: Node, + delegateModels: [DataModel, DataModel[]][], + toRemove: (PropertySignature | MethodSignature)[], + traversal: ForEachDescendantTraversalControl +) { + if (desc.isKind(SyntaxKind.InterfaceDeclaration)) { + // remove create and upsert methods from delegate interfaces since they cannot be created directly + const name = desc.getName(); + if (delegateModels.map(([dm]) => `${dm.name}Delegate`).includes(name)) { + const createMethod = desc.getMethod('create'); + if (createMethod) { + toRemove.push(createMethod); + } + const createManyMethod = desc.getMethod('createMany'); + if (createManyMethod) { + toRemove.push(createManyMethod); + } + const upsertMethod = desc.getMethod('upsert'); + if (upsertMethod) { + toRemove.push(upsertMethod); + } + traversal.skip(); + } + } +} + +function 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; +} diff --git a/packages/schema/src/plugins/enhancer/enhancer.ts b/packages/schema/src/plugins/enhancer/enhancer.ts deleted file mode 100644 index 5eccd356d..000000000 --- a/packages/schema/src/plugins/enhancer/enhancer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getPrismaClientImportSpec, 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, 'enhance.ts'); - - project.createSourceFile( - outFile, - `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; -import modelMeta from './model-meta'; -import policy from './policy'; -${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} -import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; - -export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions): DbClient { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: Prisma, - ...options - }, context); -} -`, - { overwrite: true } - ); -} diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index 45f3ceb35..86e3ecf39 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -7,7 +7,7 @@ import { type PluginFunction, } from '@zenstackhq/sdk'; import { getDefaultOutputFolder } from '../plugin-utils'; -import { generate as generateEnhancer } from './enhancer'; +import { generate as generateEnhancer } from './enhance'; import { generate as generateModelMeta } from './model-meta'; import { generate as generatePolicy } from './policy'; diff --git a/packages/schema/src/plugins/enhancer/model-meta.ts b/packages/schema/src/plugins/enhancer/model-meta/index.ts similarity index 100% rename from packages/schema/src/plugins/enhancer/model-meta.ts rename to packages/schema/src/plugins/enhancer/model-meta/index.ts diff --git a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts index e38a34c29..9333634fa 100644 --- a/packages/schema/src/plugins/enhancer/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,9 +15,11 @@ import { MemberAccessExpr, NumberLiteral, ReferenceExpr, + ReferenceTarget, StringLiteral, UnaryExpr, } from '@zenstackhq/language/ast'; +import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { ExpressionContext, getFunctionExpressionContext, @@ -23,12 +27,14 @@ import { 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 '..'; @@ -113,11 +119,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 @@ -496,48 +535,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/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index 149858cd6..2032f2b99 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -1,12 +1,9 @@ import { DataModel, - DataModelAttribute, DataModelField, - DataModelFieldAttribute, Enum, Expression, Model, - isBinaryExpr, isDataModel, isDataModelField, isEnum, @@ -15,7 +12,6 @@ import { isMemberAccessExpr, isReferenceExpr, isThisExpr, - isUnaryExpr, } from '@zenstackhq/language/ast'; import { FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX, @@ -71,7 +67,7 @@ export class PolicyGenerator { sf.addImportDeclaration({ namedImports: [ { name: 'type QueryContext' }, - { name: 'type DbOperations' }, + { name: 'type CrudContract' }, { name: 'allFieldsEqual' }, { name: 'type PolicyDef' }, ], @@ -203,7 +199,7 @@ export 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) { @@ -253,30 +249,6 @@ export class PolicyGenerator { } } - private visitPolicyExpression(expr: Expression, postUpdate: boolean): Expression | undefined { - if (isBinaryExpr(expr) && (expr.operator === '&&' || expr.operator === '||')) { - const left = this.visitPolicyExpression(expr.left, postUpdate); - const right = this.visitPolicyExpression(expr.right, postUpdate); - if (!left) return right; - if (!right) return left; - return { ...expr, left, right }; - } - - if (isUnaryExpr(expr) && expr.operator === '!') { - const operand = this.visitPolicyExpression(expr.operand, postUpdate); - if (!operand) return undefined; - return { ...expr, operand }; - } - - if (postUpdate && !this.hasFutureReference(expr)) { - return undefined; - } else if (!postUpdate && this.hasFutureReference(expr)) { - return undefined; - } - - return expr; - } - private hasFutureReference(expr: Expression) { for (const node of streamAst(expr)) { if (isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref)) { @@ -766,7 +738,7 @@ export 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/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index f4f521fdc..00b806e7e 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -35,13 +35,9 @@ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { 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', diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index c4b209aa6..b27624cd7 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,5 +1,5 @@ import { PluginFunction } from '@zenstackhq/sdk'; -import PrismaSchemaGenerator from './schema-generator'; +import { PrismaSchemaGenerator } from './schema-generator'; export const name = 'Prisma'; export const description = 'Generating Prisma schema'; diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 64777b62e..b65313940 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 0be727d31..01a8efc60 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -16,6 +16,7 @@ import { GeneratorDecl, InvocationExpr, isArrayExpr, + isDataModel, isInvocationExpr, isLiteralExpr, isNullExpr, @@ -27,14 +28,17 @@ import { StringLiteral, } from '@zenstackhq/language/ast'; import { match } 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 { getAttribute, getDMMF, getLiteral, getPrismaVersion, isAuthInvocation, + isDelegateModel, + isIdField, PluginError, PluginOptions, resolved, @@ -44,15 +48,18 @@ import { import fs from 'fs'; import { writeFile } from 'fs/promises'; import { streamAst } from 'langium'; +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 { execSync } from '../../utils/exec-utils'; import { findPackageJson } from '../../utils/pkg-utils'; import { + AttributeArgValue, ModelFieldType, AttributeArg as PrismaAttributeArg, AttributeArgValue as PrismaAttributeArgValue, @@ -76,7 +83,7 @@ const FIELD_PASSTHROUGH_ATTR = '@prisma.passthrough'; /** * Generates Prisma schema file */ -export default class PrismaSchemaGenerator { +export class PrismaSchemaGenerator { private zModelGenerator: ZModelCodeGenerator = new ZModelCodeGenerator(); private readonly PRELUDE = `////////////////////////////////////////////////////////////////////////////////////////////// @@ -86,8 +93,13 @@ export default class PrismaSchemaGenerator { `; + private mode: 'logical' | 'physical' = 'physical'; + async generate(model: Model, options: PluginOptions) { const warnings: string[] = []; + if (options.mode) { + this.mode = options.mode as 'logical' | 'physical'; + } const prismaVersion = getPrismaVersion(); if (prismaVersion && semver.lt(prismaVersion, PRISMA_MINIMUM_VERSION)) { @@ -113,7 +125,7 @@ export default class PrismaSchemaGenerator { break; case GeneratorDecl: - this.generateGenerator(prisma, decl as GeneratorDecl); + this.generateGenerator(prisma, decl as GeneratorDecl, options); break; } } @@ -220,7 +232,7 @@ export default class PrismaSchemaGenerator { return JSON.stringify(expr.value); } - 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) })) @@ -262,13 +274,31 @@ 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 (field.$inheritedFrom.isAbstract || this.mode === 'logical' || isIdField(field)) { + this.generateModelField(model, field); + } + } else { + this.generateModelField(model, field); + } } for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { @@ -281,6 +311,148 @@ 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); + } + + 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 all concrete models that have relation to it, + // and generate an auxiliary opposite relation field + 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) + ); + + concreteModels.forEach((concrete) => { + const relationField = model.addField( + `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(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); + idFields.forEach((idField) => { + model.addField( + `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(concrete.name)}${upperCaseFirst( + idField.name + )}`, + idField.type.type! + ); + }); + + const args = new AttributeArgValue( + 'Array', + idFields.map( + (idField) => + new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) + ) + ); + relationField.attributes.push( + new PrismaFieldAttribute('@relation', [ + new PrismaAttributeArg('fields', args), + new PrismaAttributeArg('references', args), + ]) + ); + } else { + relationField.attributes.push(this.makeFieldAttribute(relAttr as DataModelFieldAttribute)); + } + } + }); + }); } private isPrismaAttribute(attr: DataModelAttribute | DataModelFieldAttribute) { @@ -309,7 +481,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) { @@ -318,34 +490,42 @@ export default class PrismaSchemaGenerator { const type = new ModelFieldType(fieldType, field.type.array, field.type.optional); - const attributes = this.getAttributesToGenerate(field); + const attributes = field.attributes + .filter((attr) => this.isPrismaAttribute(attr)) + // `@default` with `auth()` is handled outside Prisma + .filter((attr) => !this.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); // user defined comments pass-through field.comments.forEach((c) => result.addComment(c)); } - private getAttributesToGenerate(field: DataModelField) { - if (this.hasDefaultWithAuth(field)) { - return []; - } - return field.attributes - .filter((attr) => this.isPrismaAttribute(attr)) - .map((attr) => this.makeFieldAttribute(attr)); + private isInheritedFromDelegate(field: DataModelField) { + return field.$inheritedFrom && isDelegateModel(field.$inheritedFrom); } - private hasDefaultWithAuth(field: DataModelField) { - const defaultAttr = getAttribute(field, '@default'); - if (!defaultAttr) { + private isDefaultWithAuth(attr: DataModelFieldAttribute) { + if (attr.decl.ref?.name !== '@default') { return false; } - const expr = defaultAttr.args[0]?.value; + const expr = attr.args[0]?.value; if (!expr) { return false; } @@ -469,6 +649,10 @@ export default class PrismaSchemaGenerator { } } +function isDescendantOf(model: DataModel, superModel: DataModel): boolean { + return model.superTypes.some((s) => s.ref === superModel || isDescendantOf(s.ref!, superModel)); +} + export function getDefaultPrismaOutputFile(schemaPath: string) { // handle override from package.json const pkgJsonPath = findPackageJson(path.dirname(schemaPath)); diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index e95099498..721dee538 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -598,3 +598,13 @@ attribute @prisma.passthrough(_ text: String) * A utility attribute to allow passthrough of arbitrary attribute text to the generated Prisma schema. */ attribute @@prisma.passthrough(_ text: String) + +/** + * Marks a model to be a delegate. Used for implementing polymorphism. + */ +attribute @@delegate(_ discriminator: FieldReference) + +// /** +// * Marks a field to be the discriminator that identifies model's type in a polymorphic hierarchy. +// */ +// attribute @discriminator() diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 1e2850577..2688987a2 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -1,24 +1,29 @@ import { BinaryExpr, DataModel, + DataModelField, Expression, InheritableNode, + isArrayExpr, isBinaryExpr, isDataModel, + isDataModelField, + isInvocationExpr, + isMemberAccessExpr, isModel, + isReferenceExpr, Model, ModelImport, + ReferenceExpr, } from '@zenstackhq/language/ast'; +import { isFromStdlib } from '@zenstackhq/sdk'; import { AstNode, + copyAstNode, CstNode, - GenericAstNode, getContainerOfType, getDocument, - isAstNode, - isReference, LangiumDocuments, - linkContentToContainer, Linker, Mutable, Reference, @@ -41,23 +46,32 @@ type BuildReference = ( export function mergeBaseModel(model: Model, linker: Linker) { const buildReference = linker.buildReference.bind(linker); - model.declarations - .filter((x) => x.$type === 'DataModel') - .forEach((decl) => { - const dataModel = decl as DataModel; + model.declarations.filter(isDataModel).forEach((decl) => { + const dataModel = decl as DataModel; - dataModel.fields = dataModel.superTypes + const bases = getRecursiveBases(dataModel).reverse(); + if (bases.length > 0) { + dataModel.fields = bases // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .flatMap((superType) => superType.ref!.fields) + .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 + dataModel.attributes = bases // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .flatMap((superType) => superType.ref!.attributes) + .flatMap((base) => base.attributes) + // don't inherit skip-level attributes + .filter((attr) => !attr.$inheritedFrom) + // don't inherit `@@delegate` attribute + .filter((attr) => attr.decl.$refText !== '@@delegate') .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); - }); + } + + dataModel.$baseMerged = true; + }); // remove abstract models model.declarations = model.declarations.filter((x) => !(isDataModel(x) && x.isAbstract)); @@ -73,40 +87,49 @@ function cloneAst( clone.$container = newContainer; clone.$containerProperty = node.$containerProperty; clone.$containerIndex = node.$containerIndex; - clone.$inheritedFrom = getContainerOfType(node, isDataModel); + clone.$inheritedFrom = node.$inheritedFrom ?? getContainerOfType(node, isDataModel); return clone; } -// this function is copied from Langium's ast-utils, but copying $resolvedType as well -function copyAstNode(node: T, buildReference: BuildReference): T { - const copy: GenericAstNode = { $type: node.$type, $resolvedType: node.$resolvedType }; - - for (const [name, value] of Object.entries(node)) { - if (!name.startsWith('$')) { - if (isAstNode(value)) { - copy[name] = copyAstNode(value, buildReference); - } else if (isReference(value)) { - copy[name] = buildReference(copy, name, value.$refNode, value.$refText); - } else if (Array.isArray(value)) { - const copiedArray: unknown[] = []; - for (const element of value) { - if (isAstNode(element)) { - copiedArray.push(copyAstNode(element, buildReference)); - } else if (isReference(element)) { - copiedArray.push(buildReference(copy, name, element.$refNode, element.$refText)); - } else { - copiedArray.push(element); - } - } - copy[name] = copiedArray; - } else { - copy[name] = value; +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 isAuthInvocation(node: AstNode) { + return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); +} - linkContentToContainer(copy); - return copy as unknown as T; +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; + } else if (isMemberAccessExpr(expr) && isDataModelField(expr.member.ref)) { + return expr.member.ref; + } else { + return undefined; + } } export function resolveImportUri(imp: ModelImport): URI | undefined { @@ -183,3 +206,23 @@ export function getContainingDataModel(node: Expression): DataModel | undefined } return undefined; } + +export function getModelFieldsWithBases(model: DataModel) { + if (model.$baseMerged) { + return model.fields; + } else { + 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; +} diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index d2f425e53..67ba27f99 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -5,7 +5,7 @@ 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'; @@ -364,6 +364,7 @@ describe('Prisma generator test', () => { output: name, generateClient: false, }); + console.log('Generated:', name); const content = fs.readFileSync(name, 'utf-8'); const dmmf = await getDMMF({ datamodel: content }); @@ -372,9 +373,7 @@ 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 () => { diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index e1f06d268..ec3be8f36 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -632,7 +632,9 @@ describe('Data Model Validation Tests', () => { `); expect(errors.length).toBe(1); - expect(errors[0]).toEqual(`Model A cannot be extended because it's not abstract`); + expect(errors[0]).toEqual( + `Model A cannot be extended because it's neither abstract nor marked as "@@delegate"` + ); // relation incomplete from multiple level inheritance expect( diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 9beda653a..cd516f5ec 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -19,11 +19,13 @@ import { ExpressionContext, getAttribute, getAttributeArg, + getAttributeArgLiteral, getAttributeArgs, getAuthModel, getDataModels, getLiteral, hasAttribute, + isDelegateModel, isAuthInvocation, isEnumFieldReference, isForeignKeyField, @@ -57,133 +59,202 @@ function generateModelMetadata( options: ModelMetaGeneratorOptions ) { 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)},`); - } - - const defaultValueProvider = generateDefaultValueProvider(f, sourceFile); - if (defaultValueProvider) { - writer.write(` - defaultValueProvider: ${defaultValueProvider},`); - } - - if (isAutoIncrement(f)) { - writer.write(` - isAutoIncrement: true,`); - } - - writer.write(` - },`); - } - }); - writer.write(','); + 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 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(() => { + 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,`); + } + + 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) { @@ -212,13 +283,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) { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index ed841dbc7..0bd98e63e 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -178,7 +178,7 @@ export function isDataModelFieldReference(node: AstNode): node is ReferenceExpr * Gets `@@id` fields declared at the data model level */ export function getModelIdFields(model: DataModel) { - const idAttr = model.attributes.find((attr) => attr.decl.ref?.name === '@@id'); + const idAttr = model.attributes.find((attr) => attr.decl.$refText === '@@id'); if (!idAttr) { return []; } @@ -196,7 +196,7 @@ export function getModelIdFields(model: DataModel) { * Gets `@@unique` fields declared at the data model level */ export function getModelUniqueFields(model: DataModel) { - const uniqueAttr = model.attributes.find((attr) => attr.decl.ref?.name === '@@unique'); + const uniqueAttr = model.attributes.find((attr) => attr.decl.$refText === '@@unique'); if (!uniqueAttr) { return []; } @@ -379,6 +379,10 @@ export function getAuthModel(dataModels: DataModel[]) { return authModel; } +export function isDelegateModel(node: AstNode) { + return isDataModel(node) && hasAttribute(node, '@@delegate'); +} + export function getIdFields(dataModel: DataModel) { const fieldLevelId = getModelFieldsWithBases(dataModel).find((f) => f.attributes.some((attr) => attr.decl.$refText === '@id') diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 88b463c80..52d700c63 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -956,7 +956,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 +1013,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 +1027,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 +1049,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; } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 2b501abc0..bd64d6461 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { DMMF } from '@prisma/generator-helper'; import type { Model } from '@zenstackhq/language/ast'; -import type { AuthUser, DbOperations, EnhancementOptions } from '@zenstackhq/runtime'; +import type { AuthUser, CrudContract, EnhancementKind, EnhancementOptions } from '@zenstackhq/runtime'; import { getDMMF } from '@zenstackhq/sdk'; import { execSync } from 'child_process'; import * as fs from 'fs'; @@ -24,7 +24,7 @@ import prismaPlugin from 'zenstack/plugins/prisma'; */ export const FILE_SPLITTER = '#FILE_SPLITTER#'; -export type FullDbClientContract = Record & { +export type FullDbClientContract = CrudContract & { $on(eventType: any, callback: (event: any) => void): void; $use(cb: any): void; $disconnect: () => Promise; @@ -81,7 +81,6 @@ datasource db { generator js { provider = 'prisma-client-js' - previewFeatures = ['clientExtensions'] } plugin enhancer { @@ -111,6 +110,8 @@ export type SchemaLoadOptions = { dbUrl?: string; pulseApiKey?: string; getPrismaOnly?: boolean; + enhancements?: EnhancementKind[]; + enhanceOptions?: Partial; }; const defaultOptions: SchemaLoadOptions = { @@ -283,8 +284,9 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { modelMeta, zodSchemas, logPrismaQuery: opt.logPrismaQuery, - transactionTimeout: 10000, - ...options, + transactionTimeout: 1000000, + kinds: opt.enhancements, + ...(options ?? opt.enhanceOptions), } ), enhanceRaw: enhance, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74426b3ad..9c8f28205 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -400,12 +400,18 @@ 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 diff --git a/tests/integration/package.json b/tests/integration/package.json index 40627f354..cace90307 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint . --ext .ts", - "test": "ZENSTACK_TEST=1 jest" + "test": "ZENSTACK_TEST=1 jest --runInBand" }, "keywords": [], "author": "", diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts index 987752bd2..6b5ae7c3a 100644 --- a/tests/integration/tests/cli/init.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -9,7 +9,8 @@ import { createProgram } from '../../../../packages/schema/src/cli'; import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; import { createNpmrc } from './share'; -describe('CLI init command tests', () => { +// eslint-disable-next-line jest/no-disabled-tests +describe.skip('CLI init command tests', () => { let origDir: string; beforeEach(() => { @@ -23,6 +24,7 @@ describe('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 .', { stdio: 'inherit', @@ -42,9 +44,7 @@ describe('CLI init command tests', () => { checkDependency('@zenstackhq/runtime', false, true); }); - // Disabled because it blows up memory on MAC, not sure why ... - // eslint-disable-next-line jest/no-disabled-tests - it.skip('init project t3 yarn std', async () => { + it('init project t3 yarn std', async () => { execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { stdio: 'inherit', env: { diff --git a/tests/integration/tests/enhancements/with-delegate/policy.test.ts b/tests/integration/tests/enhancements/with-delegate/policy.test.ts new file mode 100644 index 000000000..d0316595d --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/policy.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/polymorphism.test.ts b/tests/integration/tests/enhancements/with-delegate/polymorphism.test.ts new file mode 100644 index 000000000..0d0b24ca2 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/polymorphism.test.ts @@ -0,0 +1,1015 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { PrismaErrorCode } from '@zenstackhq/runtime'; + +describe('Polymorphism Test', () => { + const 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[] +} +`; + + 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('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({ + id: video.id, + createdAt: r.createdAt, + viewCount: r.viewCount, + url: r.url, + duration: r.duration, + assetType: 'Video', + videoType: 'RatedVideo', + }); + expect(video.rating).toBeUndefined(); + expect(video.owner).toMatchObject(user); + + const asset = await db.asset.findFirst({ where: { viewCount: r.viewCount }, include: { owner: true } }); + expect(asset).toMatchObject({ id: r.id, createdAt: r.createdAt, assetType: 'Video', viewCount: r.viewCount }); + expect(asset.url).toBeUndefined(); + expect(asset.duration).toBeUndefined(); + expect(asset.rating).toBeUndefined(); + expect(asset.videoType).toBeUndefined(); + expect(asset.owner).toMatchObject(user); + + 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, + }); + expect(imgAsset.format).toBeUndefined(); + expect(imgAsset.owner).toMatchObject(user); + }); + + 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' + ); + }); +}); 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 733391bd1..c3a84707e 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 { From 57316dfb884390a746c00121fb02bc1d1455ecc2 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 13 Feb 2024 13:17:41 +0800 Subject: [PATCH 008/127] chore: remove CLI config (#999) --- packages/schema/src/cli/index.ts | 2 - .../src/plugins/prisma/schema-generator.ts | 2 +- tests/integration/tests/cli/config.test.ts | 65 ------------------- 3 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 tests/integration/tests/cli/config.test.ts diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index d96ed1121..f4fee92b7 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')) diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 01a8efc60..bfc0cf770 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -142,7 +142,7 @@ export class PrismaSchemaGenerator { if (options.format === true) { try { // run 'prisma format' - await execSync(`npx prisma format --schema ${outFile}`); + await execSync(`npx prisma format --schema ${outFile}`, { stdio: 'ignore' }); } catch { warnings.push(`Failed to format Prisma schema file`); } diff --git a/tests/integration/tests/cli/config.test.ts b/tests/integration/tests/cli/config.test.ts deleted file mode 100644 index f047889fd..000000000 --- a/tests/integration/tests/cli/config.test.ts +++ /dev/null @@ -1,65 +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'; - -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' }); - }); -}); From c1da257f684696dcb434e53ec8e134c819e33d49 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 13 Feb 2024 15:28:59 +0800 Subject: [PATCH 009/127] merge from dev (#1001) --- packages/language/package.json | 4 +- packages/language/src/generated/ast.ts | 53 ++++-- packages/language/src/generated/grammar.ts | 97 ++++------ packages/language/src/generated/module.ts | 8 +- packages/language/src/zmodel.langium | 6 +- packages/language/syntaxes/zmodel.tmLanguage | 2 +- .../language/syntaxes/zmodel.tmLanguage.json | 2 +- .../plugins/tanstack-query/src/generator.ts | 171 ++++++++++-------- .../tanstack-query/src/runtime-v5/react.ts | 57 ++++++ packages/runtime/src/enhancements/omit.ts | 15 +- .../src/enhancements/policy/handler.ts | 26 ++- .../src/enhancements/policy/policy-utils.ts | 21 +++ packages/schema/package.json | 2 +- packages/schema/src/cli/index.ts | 1 - .../validator/datamodel-validator.ts | 22 +-- .../function-invocation-validator.ts | 1 + .../zmodel-completion-provider.ts | 10 +- .../src/language-server/zmodel-linker.ts | 7 - .../src/language-server/zmodel-module.ts | 7 + .../src/plugins/prisma/prisma-builder.ts | 6 +- .../src/plugins/prisma/schema-generator.ts | 8 +- packages/schema/src/plugins/zod/generator.ts | 6 +- packages/schema/src/res/stdlib.zmodel | 100 +++++++++- .../tests/generator/prisma-builder.test.ts | 2 +- .../schema/tests/schema/all-features.zmodel | 4 +- packages/schema/tests/schema/parser.test.ts | 2 - .../validation/attribute-validation.test.ts | 19 +- .../validation/datamodel-validation.test.ts | 13 +- packages/sdk/package.json | 2 +- packages/sdk/src/constants.ts | 1 + packages/sdk/src/zmodel-code-generator.ts | 8 +- packages/testtools/package.json | 2 +- pnpm-lock.yaml | 42 +++-- tests/integration/tests/cli/init.test.ts | 1 + .../enhancements/with-omit/with-omit.test.ts | 80 ++++++++ .../with-policy/field-validation.test.ts | 59 ++++++ .../tests/frameworks/trpc/generation.test.ts | 1 + .../tests/regression/issue-965.test.ts | 53 ++++++ .../tests/regression/issue-971.test.ts | 23 +++ .../tests/regression/issue-992.test.ts | 45 +++++ 40 files changed, 726 insertions(+), 263 deletions(-) create mode 100644 tests/integration/tests/regression/issue-965.test.ts create mode 100644 tests/integration/tests/regression/issue-971.test.ts create mode 100644 tests/integration/tests/regression/issue-992.test.ts diff --git a/packages/language/package.json b/packages/language/package.json index b7d65e3fc..a222a6d95 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -19,11 +19,11 @@ "author": "ZenStack Team", "license": "MIT", "devDependencies": { - "langium-cli": "1.2.0", + "langium-cli": "1.3.1", "plist2": "^1.1.3" }, "dependencies": { - "langium": "1.2.0" + "langium": "1.3.1" }, "contributes": { "languages": [ diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 7463fb9da..a95a748d9 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -1,10 +1,24 @@ /****************************************************************************** - * This file was generated by langium-cli 1.2.0. + * This file was generated by langium-cli 1.3.1. * DO NOT EDIT MANUALLY! ******************************************************************************/ /* eslint-disable */ -import { AstNode, AbstractAstReflection, Reference, ReferenceInfo, TypeMetaData } from 'langium'; +import type { AstNode, Reference, ReferenceInfo, TypeMetaData } from 'langium'; +import { AbstractAstReflection } from 'langium'; + +export const ZModelTerminals = { + WS: /\s+/, + INTERNAL_ATTRIBUTE_NAME: /@@@([_a-zA-Z][\w_]*\.)*[_a-zA-Z][\w_]*/, + MODEL_ATTRIBUTE_NAME: /@@([_a-zA-Z][\w_]*\.)*[_a-zA-Z][\w_]*/, + FIELD_ATTRIBUTE_NAME: /@([_a-zA-Z][\w_]*\.)*[_a-zA-Z][\w_]*/, + ID: /[_a-zA-Z][\w_]*/, + STRING: /"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/, + NUMBER: /[+-]?[0-9]+(\.[0-9]+)?/, + TRIPLE_SLASH_COMMENT: /\/\/\/[^\n\r]*/, + ML_COMMENT: /\/\*[\s\S]*?\*\//, + SL_COMMENT: /\/\/[^\n\r]*/, +}; export type AbstractDeclaration = Attribute | DataModel | DataSource | Enum | FunctionDecl | GeneratorDecl | Plugin; @@ -64,10 +78,10 @@ export function isReferenceTarget(item: unknown): item is ReferenceTarget { return reflection.isInstance(item, ReferenceTarget); } -export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'sort' | 'view' | string; +export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'view' | string; export function isRegularID(item: unknown): item is RegularID { - return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'sort' || item === 'view' || item === 'import' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item))); + return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item))); } export type TypeDeclaration = DataModel | Enum; @@ -81,7 +95,6 @@ export function isTypeDeclaration(item: unknown): item is TypeDeclaration { export interface Argument extends AstNode { readonly $container: InvocationExpr; readonly $type: 'Argument'; - name?: RegularID value: Expression } @@ -92,7 +105,7 @@ export function isArgument(item: unknown): item is Argument { } export interface ArrayExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ArrayExpr'; items: Array } @@ -163,7 +176,7 @@ export function isAttributeParamType(item: unknown): item is AttributeParamType } export interface BinaryExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'BinaryExpr'; left: Expression operator: '!' | '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '?' | '^' | 'in' | '||' @@ -177,7 +190,7 @@ export function isBinaryExpr(item: unknown): item is BinaryExpr { } export interface BooleanLiteral extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'BooleanLiteral'; value: Boolean } @@ -189,7 +202,7 @@ export function isBooleanLiteral(item: unknown): item is BooleanLiteral { } export interface ConfigArrayExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ConfigArrayExpr'; items: Array } @@ -440,7 +453,7 @@ export function isInternalAttribute(item: unknown): item is InternalAttribute { } export interface InvocationExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'InvocationExpr'; args: Array function: Reference @@ -453,7 +466,7 @@ export function isInvocationExpr(item: unknown): item is InvocationExpr { } export interface MemberAccessExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'MemberAccessExpr'; member: Reference operand: Expression @@ -490,7 +503,7 @@ export function isModelImport(item: unknown): item is ModelImport { } export interface NullExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'NullExpr'; value: 'null' } @@ -502,7 +515,7 @@ export function isNullExpr(item: unknown): item is NullExpr { } export interface NumberLiteral extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'NumberLiteral'; value: string } @@ -514,7 +527,7 @@ export function isNumberLiteral(item: unknown): item is NumberLiteral { } export interface ObjectExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ObjectExpr'; fields: Array } @@ -554,8 +567,8 @@ export function isPluginField(item: unknown): item is PluginField { export interface ReferenceArg extends AstNode { readonly $container: ReferenceExpr; readonly $type: 'ReferenceArg'; - name: 'sort' - value: 'Asc' | 'Desc' + name: string + value: Expression } export const ReferenceArg = 'ReferenceArg'; @@ -565,7 +578,7 @@ export function isReferenceArg(item: unknown): item is ReferenceArg { } export interface ReferenceExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ReferenceExpr'; args: Array target: Reference @@ -578,7 +591,7 @@ export function isReferenceExpr(item: unknown): item is ReferenceExpr { } export interface StringLiteral extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'StringLiteral'; value: string } @@ -590,7 +603,7 @@ export function isStringLiteral(item: unknown): item is StringLiteral { } export interface ThisExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ThisExpr'; value: 'this' } @@ -602,7 +615,7 @@ export function isThisExpr(item: unknown): item is ThisExpr { } export interface UnaryExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; + readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'UnaryExpr'; operand: Expression operator: '!' diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 5dbe02014..45aa3ff97 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -1,9 +1,10 @@ /****************************************************************************** - * This file was generated by langium-cli 1.2.0. + * This file was generated by langium-cli 1.3.1. * DO NOT EDIT MANUALLY! ******************************************************************************/ -import { loadGrammarFromJson, Grammar } from 'langium'; +import type { Grammar } from 'langium'; +import { loadGrammarFromJson } from 'langium'; let loadedZModelGrammar: Grammar | undefined; export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModelGrammar = loadGrammarFromJson(`{ @@ -1052,8 +1053,11 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "feature": "name", "operator": "=", "terminal": { - "$type": "Keyword", - "value": "sort" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@62" + }, + "arguments": [] } }, { @@ -1065,17 +1069,11 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "feature": "value", "operator": "=", "terminal": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Keyword", - "value": "Asc" - }, - { - "$type": "Keyword", - "value": "Desc" - } - ] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] } } ] @@ -1865,43 +1863,16 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "ParserRule", "name": "Argument", "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@46" - }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": ":" - } - ], - "cardinality": "?" + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" }, - { - "$type": "Assignment", - "feature": "value", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" - }, - "arguments": [] - } - } - ] + "arguments": [] + } }, "definesHiddenTokens": false, "entry": false, @@ -2723,10 +2694,6 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "Keyword", "value": "in" }, - { - "$type": "Keyword", - "value": "sort" - }, { "$type": "Keyword", "value": "view" @@ -3452,7 +3419,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "WS", "definition": { "$type": "RegexToken", - "regex": "\\\\s+" + "regex": "/\\\\s+/" }, "fragment": false }, @@ -3461,7 +3428,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "INTERNAL_ATTRIBUTE_NAME", "definition": { "$type": "RegexToken", - "regex": "@@@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*" + "regex": "/@@@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*/" }, "fragment": false, "hidden": false @@ -3471,7 +3438,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "MODEL_ATTRIBUTE_NAME", "definition": { "$type": "RegexToken", - "regex": "@@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*" + "regex": "/@@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*/" }, "fragment": false, "hidden": false @@ -3481,7 +3448,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "FIELD_ATTRIBUTE_NAME", "definition": { "$type": "RegexToken", - "regex": "@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*" + "regex": "/@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*/" }, "fragment": false, "hidden": false @@ -3491,7 +3458,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "ID", "definition": { "$type": "RegexToken", - "regex": "[_a-zA-Z][\\\\w_]*" + "regex": "/[_a-zA-Z][\\\\w_]*/" }, "fragment": false, "hidden": false @@ -3501,7 +3468,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "STRING", "definition": { "$type": "RegexToken", - "regex": "\\"(\\\\\\\\.|[^\\"\\\\\\\\])*\\"|'(\\\\\\\\.|[^'\\\\\\\\])*'" + "regex": "/\\"(\\\\\\\\.|[^\\"\\\\\\\\])*\\"|'(\\\\\\\\.|[^'\\\\\\\\])*'/" }, "fragment": false, "hidden": false @@ -3511,7 +3478,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "NUMBER", "definition": { "$type": "RegexToken", - "regex": "[+-]?[0-9]+(\\\\.[0-9]+)?" + "regex": "/[+-]?[0-9]+(\\\\.[0-9]+)?/" }, "fragment": false, "hidden": false @@ -3521,7 +3488,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "TRIPLE_SLASH_COMMENT", "definition": { "$type": "RegexToken", - "regex": "\\\\/\\\\/\\\\/[^\\\\n\\\\r]*" + "regex": "/\\\\/\\\\/\\\\/[^\\\\n\\\\r]*/" }, "fragment": false, "hidden": false @@ -3532,7 +3499,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "ML_COMMENT", "definition": { "$type": "RegexToken", - "regex": "\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\/" + "regex": "/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//" }, "fragment": false }, @@ -3542,7 +3509,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "name": "SL_COMMENT", "definition": { "$type": "RegexToken", - "regex": "\\\\/\\\\/[^\\\\n\\\\r]*" + "regex": "/\\\\/\\\\/[^\\\\n\\\\r]*/" }, "fragment": false } diff --git a/packages/language/src/generated/module.ts b/packages/language/src/generated/module.ts index ac0995108..b96dd1dee 100644 --- a/packages/language/src/generated/module.ts +++ b/packages/language/src/generated/module.ts @@ -1,17 +1,17 @@ /****************************************************************************** - * This file was generated by langium-cli 1.2.0. + * This file was generated by langium-cli 1.3.1. * DO NOT EDIT MANUALLY! ******************************************************************************/ -import { LangiumGeneratedServices, LangiumGeneratedSharedServices, LangiumSharedServices, LangiumServices, LanguageMetaData, Module } from 'langium'; +import type { LangiumGeneratedServices, LangiumGeneratedSharedServices, LangiumSharedServices, LangiumServices, LanguageMetaData, Module } from 'langium'; import { ZModelAstReflection } from './ast'; import { ZModelGrammar } from './grammar'; -export const ZModelLanguageMetaData: LanguageMetaData = { +export const ZModelLanguageMetaData = { languageId: 'zmodel', fileExtensions: ['.zmodel'], caseInsensitive: false -}; +} as const satisfies LanguageMetaData; export const ZModelGeneratedSharedModule: Module = { AstReflection: () => new ZModelAstReflection() diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index da445c792..8fcc72c34 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -80,7 +80,7 @@ fragment ReferenceArgList: args+=ReferenceArg (',' args+=ReferenceArg)*; ReferenceArg: - name=('sort') ':' value=('Asc' | 'Desc'); + name=ID ':' value=Expression; ObjectExpr: @@ -172,7 +172,7 @@ fragment ArgumentList: args+=Argument (',' args+=Argument)*; Argument: - (name=RegularID ':')? value=Expression; + value=Expression; // model DataModel: @@ -224,7 +224,7 @@ FunctionParamType: // https://github.com/langium/langium/discussions/1012 RegularID returns string: // include keywords that we'd like to work as ID in most places - ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'sort' | 'view' | 'import'; + ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import'; // attribute Attribute: diff --git a/packages/language/syntaxes/zmodel.tmLanguage b/packages/language/syntaxes/zmodel.tmLanguage index cf70fb761..6102b919d 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage +++ b/packages/language/syntaxes/zmodel.tmLanguage @@ -20,7 +20,7 @@ name keyword.control.zmodel match - \b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|sort|this|true|view)\b + \b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|view)\b name diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index 00c737c97..aad6a38c7 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage.json +++ b/packages/language/syntaxes/zmodel.tmLanguage.json @@ -10,7 +10,7 @@ }, { "name": "keyword.control.zmodel", - "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|sort|this|true|view)\\b" + "match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|view)\\b" }, { "name": "string.quoted.double.zmodel", diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index b8c601dc4..58836091a 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -73,68 +73,88 @@ function generateQueryHook( overrideReturnType?: string, overrideInputType?: string, overrideTypeParameters?: string[], - infinite = false, - optimisticUpdate = false + supportInfinite = false, + supportOptimistic = false ) { - const capOperation = upperCaseFirst(operation); - - const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; - const inputType = `Prisma.SelectSubset`; - - let defaultReturnType = `Prisma.${model}GetPayload`; - if (optimisticUpdate) { - defaultReturnType += '& { $optimistic?: boolean }'; + const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite')[] = ['']; + if (supportInfinite) { + generateModes.push('Infinite'); } - if (returnArray) { - defaultReturnType = `Array<${defaultReturnType}>`; + + if (target === 'react' && version === 'v5') { + // react-query v5 supports suspense query + generateModes.push('Suspense'); + if (supportInfinite) { + generateModes.push('SuspenseInfinite'); + } } - const returnType = overrideReturnType ?? defaultReturnType; - const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, version); + for (const generateMode of generateModes) { + const capOperation = upperCaseFirst(operation); - const func = sf.addFunction({ - name: `use${infinite ? 'Infinite' : ''}${capOperation}${model}`, - typeParameters: overrideTypeParameters ?? [ - `TArgs extends ${argsType}`, - `TQueryFnData = ${returnType} `, - 'TData = TQueryFnData', - 'TError = DefaultError', - ], - parameters: [ - { - name: optionalInput ? 'args?' : 'args', - type: inputType, - }, - { - name: 'options?', - type: optionsType, - }, - ...(optimisticUpdate - ? [ - { - name: 'optimisticUpdate', - type: 'boolean', - initializer: 'true', - }, - ] - : []), - ], - isExported: true, - }); + const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; + const inputType = `Prisma.SelectSubset`; - if (version === 'v5' && infinite && ['react', 'svelte'].includes(target)) { - // initialPageParam and getNextPageParam options are required in v5 - func.addStatements([`options = options ?? { initialPageParam: undefined, getNextPageParam: () => null };`]); - } + const infinite = generateMode.includes('Infinite'); + const suspense = generateMode.includes('Suspense'); + const optimistic = + supportOptimistic && + // infinite queries are not subject to optimistic updates + !infinite; + + let defaultReturnType = `Prisma.${model}GetPayload`; + if (optimistic) { + defaultReturnType += '& { $optimistic?: boolean }'; + } + if (returnArray) { + defaultReturnType = `Array<${defaultReturnType}>`; + } + + const returnType = overrideReturnType ?? defaultReturnType; + const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, suspense, version); + + const func = sf.addFunction({ + name: `use${generateMode}${capOperation}${model}`, + typeParameters: overrideTypeParameters ?? [ + `TArgs extends ${argsType}`, + `TQueryFnData = ${returnType} `, + 'TData = TQueryFnData', + 'TError = DefaultError', + ], + parameters: [ + { + name: optionalInput ? 'args?' : 'args', + type: inputType, + }, + { + name: 'options?', + type: optionsType, + }, + ...(optimistic + ? [ + { + name: 'optimisticUpdate', + type: 'boolean', + initializer: 'true', + }, + ] + : []), + ], + isExported: true, + }); + + if (version === 'v5' && infinite && ['react', 'svelte'].includes(target)) { + // initialPageParam and getNextPageParam options are required in v5 + func.addStatements([`options = options ?? { initialPageParam: undefined, getNextPageParam: () => null };`]); + } - func.addStatements([ - makeGetContext(target), - `return ${ - infinite ? 'useInfiniteModelQuery' : 'useModelQuery' - }('${model}', \`\${endpoint}/${lowerCaseFirst( - model - )}/${operation}\`, args, options, fetch${optimisticUpdate ? ', optimisticUpdate' : ''});`, - ]); + func.addStatements([ + makeGetContext(target), + `return use${generateMode}ModelQuery('${model}', \`\${endpoint}/${lowerCaseFirst( + model + )}/${operation}\`, args, options, fetch${optimistic ? ', optimisticUpdate' : ''});`, + ]); + } } function generateMutationHook( @@ -308,23 +328,8 @@ function generateModelHooks( undefined, undefined, undefined, - false, - true - ); - // infinite findMany - generateQueryHook( - target, - version, - sf, - model.name, - 'findMany', - true, true, - undefined, - undefined, - undefined, - true, - false + true ); } @@ -560,19 +565,29 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { `type DefaultError = Error;`, ]; switch (target) { - case 'react': + case 'react': { + const suspense = + version === 'v5' + ? [ + `import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, + `import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query';`, + ] + : []; return [ `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, + ...suspense, ]; - case 'vue': + } + case 'vue': { return [ `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; - case 'svelte': + } + case 'svelte': { return [ `import { derived } from 'svelte/store';`, `import type { MutationOptions, CreateQueryOptions, CreateInfiniteQueryOptions } from '@tanstack/svelte-query';`, @@ -582,6 +597,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; + } default: throw new PluginError(name, `Unsupported target: ${target}`); } @@ -592,6 +608,7 @@ function makeQueryOptions( returnType: string, dataType: string, infinite: boolean, + suspense: boolean, version: TanStackVersion ) { switch (target) { @@ -599,8 +616,10 @@ function makeQueryOptions( return infinite ? version === 'v4' ? `Omit, 'queryKey'>` - : `Omit>, 'queryKey'>` - : `Omit, 'queryKey'>`; + : `Omit>, 'queryKey'>` + : `Omit, 'queryKey'>`; case 'vue': return `Omit, 'queryKey'>`; case 'svelte': diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index 4871e8229..375cb2676 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -4,10 +4,14 @@ import { useMutation, useQuery, useQueryClient, + useSuspenseInfiniteQuery, + useSuspenseQuery, type InfiniteData, type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, + UseSuspenseInfiniteQueryOptions, + UseSuspenseQueryOptions, } from '@tanstack/react-query-v5'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; @@ -71,6 +75,33 @@ export function useModelQuery( }); } +/** + * Creates a react-query suspense query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @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 +) { + const reqUrl = makeUrl(url, args); + return useSuspenseQuery({ + queryKey: getQueryKey(model, url, args, false, optimisticUpdate), + queryFn: () => fetcher(reqUrl, undefined, fetch, false), + ...options, + }); +} + /** * Creates a react-query infinite query. * @@ -97,6 +128,32 @@ export function useInfiniteModelQuery( }); } +/** + * Creates a react-query infinite suspense query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The react-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns useSuspenseInfiniteQuery hook + */ +export function useSuspenseInfiniteModelQuery( + model: string, + url: string, + args: unknown, + options: Omit>, 'queryKey'>, + fetch?: FetchFn +) { + return useSuspenseInfiniteQuery({ + queryKey: getQueryKey(model, url, args, true), + queryFn: ({ pageParam }) => { + return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + }, + ...options, + }); +} + /** * Creates a react-query mutation * diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index e05a8a769..e51f9cb47 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -42,11 +42,18 @@ class OmitHandler extends DefaultPrismaProxyHandler { continue; } - if (fieldInfo.attributes?.find((attr) => attr.name === '@omit')) { + const shouldOmit = fieldInfo.attributes?.find((attr) => attr.name === '@omit'); + if (shouldOmit) { delete entityData[field]; - } else if (fieldInfo.isDataModel) { - // recurse - await this.doPostProcess(entityData[field], fieldInfo.type); + } + + if (fieldInfo.isDataModel) { + const items = + fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]]; + for (const item of items) { + // recurse + await this.doPostProcess(item, fieldInfo.type); + } } } } diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 1bc60a647..6ae173fcd 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -263,7 +263,7 @@ export class PolicyProxyHandler implements Pr // there's no nested write and we've passed input check, proceed with the create directly // validate zod schema if any - this.validateCreateInputSchema(this.model, args.data); + 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.policyUtils.makeIdSelection(this.model) }; @@ -319,12 +319,20 @@ export class PolicyProxyHandler implements Pr // visit the create payload const visitor = new NestedWriteVisitor(this.modelMeta, { create: async (model, args, context) => { - this.validateCreateInputSchema(model, args); + const validateResult = this.validateCreateInputSchema(model, args); + if (validateResult !== args) { + this.policyUtils.replace(args, validateResult); + } pushIdFields(model, context); }, createMany: async (model, args, context) => { - enumerate(args.data).forEach((item) => this.validateCreateInputSchema(model, item)); + enumerate(args.data).forEach((item) => { + const r = this.validateCreateInputSchema(model, item); + if (r !== item) { + this.policyUtils.replace(item, r); + } + }); pushIdFields(model, context); }, @@ -333,7 +341,9 @@ export class PolicyProxyHandler implements Pr throw this.policyUtils.validationError(`'where' field is required for connectOrCreate`); } - this.validateCreateInputSchema(model, args.create); + if (args.create) { + args.create = this.validateCreateInputSchema(model, args.create); + } const existing = await this.policyUtils.checkExistence(db, model, args.where); if (existing) { @@ -482,6 +492,9 @@ export class PolicyProxyHandler implements Pr parseResult.error ); } + return parseResult.data; + } else { + return data; } } @@ -513,7 +526,10 @@ export class PolicyProxyHandler implements Pr CrudFailureReason.ACCESS_POLICY_VIOLATION ); } else if (inputCheck === true) { - this.validateCreateInputSchema(this.model, item); + const r = this.validateCreateInputSchema(this.model, item); + if (r !== item) { + this.policyUtils.replace(item, r); + } } else if (inputCheck === undefined) { // static policy check is not possible, need to do post-create check needPostCreateCheck = true; diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 50ef3a3bc..1f4629359 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -1147,6 +1147,27 @@ export class PolicyUtil extends QueryUtils { return value ? deepcopy(value) : {}; } + /** + * Replace content of `target` object with `withObject` in-place. + */ + replace(target: any, withObject: any) { + if (!target || typeof target !== 'object' || !withObject || typeof withObject !== 'object') { + return; + } + + // remove missing keys + for (const key of Object.keys(target)) { + if (!(key in withObject)) { + delete target[key]; + } + } + + // overwrite keys + for (const [key, value] of Object.entries(withObject)) { + target[key] = value; + } + } + /** * Picks properties from an object. */ diff --git a/packages/schema/package.json b/packages/schema/package.json index 613cafb77..09693e0e7 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -87,7 +87,7 @@ "colors": "1.4.0", "commander": "^8.3.0", "get-latest-version": "^5.0.1", - "langium": "1.2.0", + "langium": "1.3.1", "lower-case-first": "^2.0.2", "mixpanel": "^0.17.0", "ora": "^5.4.1", diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index f4fee92b7..a3ea38238 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -110,7 +110,6 @@ export function createProgram() { .description('Run code generation.') .addOption(schemaOption) .addOption(new Option('-o, --output ', 'default output directory for built-in plugins')) - .addOption(configOption) .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(noVersionCheckOption) diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 3e4517444..ac727b917 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -6,7 +6,7 @@ import { isStringLiteral, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { analyzePolicies, getLiteral, getModelIdFields, getModelUniqueFields, isDelegateModel } 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'; @@ -34,23 +34,19 @@ export default class DataModelValidator implements AstValidator { const modelUniqueFields = getModelUniqueFields(dm); if ( + !dm.isAbstract && idFields.length === 0 && modelLevelIds.length === 0 && uniqueFields.length === 0 && modelUniqueFields.length === 0 ) { - const { allows, denies, hasFieldValidation } = analyzePolicies(dm); - if (allows.length > 0 || denies.length > 0 || hasFieldValidation) { - // TODO: relax this requirement to require only @unique fields - // when access policies or field validation is used, require an @id field - accept( - 'error', - 'Model must include a field with @id or @unique attribute, or a model-level @@id or @@unique attribute to use access policies', - { - node: dm, - } - ); - } + accept( + 'error', + 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.', + { + node: dm, + } + ); } else if (idFields.length > 0 && modelLevelIds.length > 0) { accept('error', 'Model cannot have both field-level @id and model-level @@id attributes', { node: dm, 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 50b974a53..a6af730f2 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -57,6 +57,7 @@ export default class FunctionInvocationValidator implements AstValidator ExpressionContext.DefaultValue) .with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy) .with('@@validate', () => ExpressionContext.ValidationRule) + .with('@@index', () => ExpressionContext.Index) .otherwise(() => undefined); // get the context allowed for the function diff --git a/packages/schema/src/language-server/zmodel-completion-provider.ts b/packages/schema/src/language-server/zmodel-completion-provider.ts index e100c870a..cd6dae0ca 100644 --- a/packages/schema/src/language-server/zmodel-completion-provider.ts +++ b/packages/schema/src/language-server/zmodel-completion-provider.ts @@ -61,7 +61,7 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { if (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) { const completions = this.getCompletionFromHint(context.node); if (completions) { - completions.forEach(acceptor); + completions.forEach((c) => acceptor(context, c)); return; } } @@ -131,7 +131,7 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { return; } - const customAcceptor = (item: CompletionValueItem) => { + const customAcceptor = (context: CompletionContext, item: CompletionValueItem) => { // attributes starting with @@@ are for internal use only if (item.insertText?.startsWith('@@@') || item.label?.startsWith('@@@')) { return; @@ -156,7 +156,7 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { return; } } - acceptor(item); + acceptor(context, item); }; return super.completionForCrossReference(context, crossRef, customAcceptor); @@ -168,11 +168,11 @@ export class ZModelCompletionProvider extends DefaultCompletionProvider { keyword: any, acceptor: CompletionAcceptor ): MaybePromise { - const customAcceptor = (item: CompletionValueItem) => { + const customAcceptor = (context: CompletionContext, item: CompletionValueItem) => { if (!this.filterKeywordForContext(context, keyword.value)) { return; } - acceptor(item); + acceptor(context, item); }; return super.completionForKeyword(context, keyword, customAcceptor); } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 5ab841c96..13de8b968 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -470,13 +470,6 @@ export class ZModelLinker extends DefaultLinker { } private resolveDataModel(node: DataModel, document: LangiumDocument, extraScopes: ScopeProvider[]) { - // if (node.superTypes.length > 0) { - // const providers = node.superTypes.map( - // (superType) => (name: string) => superType.ref?.fields.find((f) => f.name === name) - // ); - // extraScopes = [...providers, ...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 07dc223e0..c0c66ce43 100644 --- a/packages/schema/src/language-server/zmodel-module.ts +++ b/packages/schema/src/language-server/zmodel-module.ts @@ -2,12 +2,15 @@ import { ZModelGeneratedModule, ZModelGeneratedSharedModule } from '@zenstackhq/ import { DefaultConfigurationProvider, DefaultDocumentBuilder, + DefaultFuzzyMatcher, DefaultIndexManager, DefaultLangiumDocumentFactory, DefaultLangiumDocuments, DefaultLanguageServer, + DefaultNodeKindProvider, DefaultServiceRegistry, DefaultSharedModuleContext, + DefaultWorkspaceSymbolProvider, LangiumDefaultSharedServices, LangiumServices, LangiumSharedServices, @@ -77,6 +80,7 @@ export const ZModelModule: Module { @@ -85,6 +89,9 @@ export function createSharedModule( lsp: { Connection: () => context.connection, LanguageServer: (services) => new DefaultLanguageServer(services), + WorkspaceSymbolProvider: (services) => new DefaultWorkspaceSymbolProvider(services), + NodeKindProvider: () => new DefaultNodeKindProvider(), + FuzzyMatcher: () => new DefaultFuzzyMatcher(), }, workspace: { LangiumDocuments: (services) => new DefaultLangiumDocuments(services), diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index b65313940..594913f8c 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -293,7 +293,7 @@ export class FieldReference { } export class FieldReferenceArg { - constructor(public name: 'sort', public value: 'Asc' | 'Desc') {} + constructor(public name: string, public value: string) {} toString(): string { return `${this.name}: ${this.value}`; @@ -309,10 +309,10 @@ export class FunctionCall { } export class FunctionCallArg { - constructor(public name: string | undefined, public value: string) {} + constructor(public value: string) {} toString(): string { - return this.name ? `${this.name}: ${this.value}` : this.value; + return this.value; } } diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index bfc0cf770..6592d83b5 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -232,6 +232,10 @@ export class PrismaSchemaGenerator { return JSON.stringify(expr.value); } + private exprToText(expr: Expression) { + return new ZModelCodeGenerator({ quote: 'double' }).generate(expr); + } + private generateGenerator(prisma: PrismaModel, decl: GeneratorDecl, options: PluginOptions) { const generator = prisma.addGenerator( decl.name, @@ -573,7 +577,7 @@ export class PrismaSchemaGenerator { 'FieldReference', new PrismaFieldReference( resolved(node.target).name, - node.args.map((arg) => new PrismaFieldReferenceArg(arg.name, arg.value)) + node.args.map((arg) => new PrismaFieldReferenceArg(arg.name, this.exprToText(arg.value))) ) ); } else if (isInvocationExpr(node)) { @@ -596,7 +600,7 @@ export class PrismaSchemaGenerator { throw new PluginError(name, 'Function call argument must be literal or null'); }); - return new PrismaFunctionCallArg(arg.name, val); + return new PrismaFunctionCallArg(val); }) ); } diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 19ce18a3b..a09c4ad73 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -395,7 +395,7 @@ async function generateModelSchema(model: DataModel, project: Project, output: s //////////////////////////////////////////////// // schema for validating prisma create input (all fields optional) - let prismaCreateSchema = makePartial('baseSchema'); + let prismaCreateSchema = makePassthrough(makePartial('baseSchema')); if (refineFuncName) { prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`; } @@ -501,3 +501,7 @@ function makeOmit(schema: string, fields: string[]) { function makeMerge(schema1: string, schema2: string): string { return `${schema1}.merge(${schema2})`; } + +function makePassthrough(schema: string) { + return `${schema}.passthrough()`; +} diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 721dee538..fd470efb8 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -61,6 +61,9 @@ enum ExpressionContext { // used in @@validate ValidationRule + + // used in @@index + Index } /** @@ -200,7 +203,7 @@ attribute @@@completionHint(_ values: String[]) * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. */ -attribute @id(map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma +attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma /** * Defines a default value for a field. @@ -215,7 +218,7 @@ attribute @default(_ value: ContextType, map: String?) @@@prisma * @param sort: Allows you to specify in what order the entries of the constraint are stored in the database. The available options are Asc and Desc. * @param clustered: Boolean Defines whether the constraint is clustered or non-clustered. Defaults to false. */ -attribute @unique(map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma +attribute @unique(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma /** * Defines a multi-field ID (composite ID) on the model. @@ -227,7 +230,7 @@ attribute @unique(map: String?, length: Int?, sort: String?, clustered: Boolean? * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. */ -attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma +attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma /** * Defines a compound unique constraint for the specified fields. @@ -238,7 +241,7 @@ attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: * @param sort: Allows you to specify in what order the entries of the constraint are stored in the database. The available options are Asc and Desc. * @param clustered: Boolean Defines whether the constraint is clustered or non-clustered. Defaults to false. */ -attribute @@unique(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma +attribute @@unique(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma /** * Index types @@ -252,6 +255,84 @@ enum IndexType { Brin } +/** + * Operator class for index + */ +enum IndexOperatorClass { + // GIN + ArrayOps + JsonbOps + JsonbPathOps + + // Gist + InetOps + + // SpGist + TextOps + + // BRIN + BitMinMaxOps + VarBitMinMaxOps + BpcharBloomOps + BpcharMinMaxOps + ByteaBloomOps + ByteaMinMaxOps + DateBloomOps + DateMinMaxOps + DateMinMaxMultiOps + Float4BloomOps + Float4MinMaxOps + Float4MinMaxMultiOps + Float8BloomOps + Float8MinMaxOps + Float8MinMaxMultiOps + InetInclusionOps + InetBloomOps + InetMinMaxOps + InetMinMaxMultiOps + Int2BloomOps + Int2MinMaxOps + Int2MinMaxMultiOps + Int4BloomOps + Int4MinMaxOps + Int4MinMaxMultiOps + Int8BloomOps + Int8MinMaxOps + Int8MinMaxMultiOps + NumericBloomOps + NumericMinMaxOps + NumericMinMaxMultiOps + OidBloomOps + OidMinMaxOps + OidMinMaxMultiOps + TextBloomOps + TextMinMaxOps + TextMinMaxMultiOps + TimestampBloomOps + TimestampMinMaxOps + TimestampMinMaxMultiOps + TimestampTzBloomOps + TimestampTzMinMaxOps + TimestampTzMinMaxMultiOps + TimeBloomOps + TimeMinMaxOps + TimeMinMaxMultiOps + TimeTzBloomOps + TimeTzMinMaxOps + TimeTzMinMaxMultiOps + UuidBloomOps + UuidMinMaxOps + UuidMinMaxMultiOps +} + +/** + * Index sort order + */ +enum SortOrder { + Asc + Desc +} + /** * Defines an index in the database. * @@ -263,7 +344,7 @@ enum IndexType { * @params clustered: Defines whether the index is clustered or non-clustered. Defaults to false. * @params type: Allows you to specify an index access method. Defaults to BTree. */ -attribute @@index(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: String?, clustered: Boolean?, type: IndexType?) @@@prisma +attribute @@index(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?, type: IndexType?) @@@prisma /** * Defines meta information about the relation. @@ -604,7 +685,8 @@ attribute @@prisma.passthrough(_ text: String) */ attribute @@delegate(_ discriminator: FieldReference) -// /** -// * Marks a field to be the discriminator that identifies model's type in a polymorphic hierarchy. -// */ -// attribute @discriminator() +/** + * Used for specifying operator classes for GIN index. + */ +function raw(value: String): Any { +} @@@expressionContext([Index]) diff --git a/packages/schema/tests/generator/prisma-builder.test.ts b/packages/schema/tests/generator/prisma-builder.test.ts index 48e465362..a3944401c 100644 --- a/packages/schema/tests/generator/prisma-builder.test.ts +++ b/packages/schema/tests/generator/prisma-builder.test.ts @@ -102,7 +102,7 @@ describe('Prisma Builder Tests', () => { undefined, new AttributeArgValue( 'FunctionCall', - new FunctionCall('dbgenerated', [new FunctionCallArg(undefined, '"timestamp_id()"')]) + new FunctionCall('dbgenerated', [new FunctionCallArg('"timestamp_id()"')]) ) ), ]), diff --git a/packages/schema/tests/schema/all-features.zmodel b/packages/schema/tests/schema/all-features.zmodel index c47a7cf79..b567093fe 100644 --- a/packages/schema/tests/schema/all-features.zmodel +++ b/packages/schema/tests/schema/all-features.zmodel @@ -40,7 +40,7 @@ model Space extends Base { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt name String @length(4, 50) - slug String @unique @length(4, 16) + slug String @length(4, 16) owner User? @relation(fields: [ownerId], references: [id]) ownerId String? members SpaceUser[] @@ -58,6 +58,8 @@ model Space extends Base { // space admin can update and delete @@allow('update,delete', members?[user == auth() && role == ADMIN]) + + @@index([slug(ops: raw("gin_trgm_ops"))], type: Gin) } /* diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts index 9b4150cd5..25ada5ceb 100644 --- a/packages/schema/tests/schema/parser.test.ts +++ b/packages/schema/tests/schema/parser.test.ts @@ -224,7 +224,6 @@ describe('Parsing Tests', () => { expect(((model.attributes[1].args[0].value as ArrayExpr).items[0] as ReferenceExpr).args[0]).toEqual( expect.objectContaining({ name: 'sort', - value: 'Asc', }) ); @@ -232,7 +231,6 @@ describe('Parsing Tests', () => { expect((model.attributes[2].args[0].value as ReferenceExpr).args[0]).toEqual( expect.objectContaining({ name: 'sort', - value: 'Desc', }) ); }); diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index dfc1d650c..ac87665b1 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -161,11 +161,11 @@ describe('Attribute tests', () => { model A { x Int y String - @@id([x, y], name: 'x_y', map: '_x_y', length: 10, sort: 'Asc', clustered: true) + @@id([x, y], name: 'x_y', map: '_x_y', length: 10, sort: Asc, clustered: true) } model B { - id String @id(map: '_id', length: 10, sort: 'Asc', clustered: true) + id String @id(map: '_id', length: 10, sort: Asc, clustered: true) } `); @@ -175,7 +175,7 @@ describe('Attribute tests', () => { id String @id x Int y String - @@unique([x, y], name: 'x_y', map: '_x_y', length: 10, sort: 'Asc', clustered: true) + @@unique([x, y], name: 'x_y', map: '_x_y', length: 10, sort: Asc, clustered: true) } `); @@ -193,7 +193,7 @@ describe('Attribute tests', () => { ${prelude} model A { id String @id - x Int @unique(map: '_x', length: 10, sort: 'Asc', clustered: true) + x Int @unique(map: '_x', length: 10, sort: Asc, clustered: true) } `); @@ -222,7 +222,7 @@ describe('Attribute tests', () => { id String @id x Int y String - @@index([x(sort: Asc), y(sort: Desc)], name: 'myindex', map: '_myindex', length: 10, sort: 'asc', clustered: true, type: BTree) + @@index([x(sort: Asc), y(sort: Desc)], name: 'myindex', map: '_myindex', length: 10, sort: Asc, clustered: true, type: BTree) } `); @@ -251,6 +251,7 @@ describe('Attribute tests', () => { ${prelude} model _String { + id String @id _string String @db.String _string1 String @db.String(1) _text String @db.Text @@ -275,6 +276,7 @@ describe('Attribute tests', () => { } model _Boolean { + id String @id _boolean Boolean @db.Boolean _bit Boolean @db.Bit _bit1 Boolean @db.Bit(1) @@ -283,6 +285,7 @@ describe('Attribute tests', () => { } model _Int { + id String @id _int Int @db.Int _integer Int @db.Integer _smallInt Int @db.SmallInt @@ -298,12 +301,14 @@ describe('Attribute tests', () => { } model _BigInt { + id String @id _bigInt BigInt @db.BigInt _unsignedBigInt BigInt @db.UnsignedBigInt _int8 BigInt @db.Int8 } model _FloatDecimal { + id String @id _float Float @db.Float _decimal Decimal @db.Decimal _decimal1 Decimal @db.Decimal(10, 2) @@ -318,6 +323,7 @@ describe('Attribute tests', () => { } model _DateTime { + id String @id _dateTime DateTime @db.DateTime _dateTime2 DateTime @db.DateTime2 _smallDateTime DateTime @db.SmallDateTime @@ -334,11 +340,13 @@ describe('Attribute tests', () => { } model _Json { + id String @id _json Json @db.Json _jsonb Json @db.JsonB } model _Bytes { + id String @id _bytes Bytes @db.Bytes _byteA Bytes @db.ByteA _longBlob Bytes @db.LongBlob @@ -1150,6 +1158,7 @@ describe('Attribute tests', () => { } model M { + id String @id e E @default(E1) } `); diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index ec3be8f36..19535d5dd 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -120,16 +120,8 @@ describe('Data Model Validation Tests', () => { }); it('id field', async () => { - // no need for '@id' field when there's no access policy or field validation - await loadModel(` - ${prelude} - model M { - x Int - } - `); - const err = - 'Model must include a field with @id or @unique attribute, or a model-level @@id or @@unique attribute to use access policies'; + 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.'; expect( await loadModelWithError(` @@ -630,10 +622,9 @@ describe('Data Model Validation Tests', () => { b String } `); - expect(errors.length).toBe(1); expect(errors[0]).toEqual( - `Model A cannot be extended because it's neither abstract nor marked as "@@delegate"` + 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.' ); // relation incomplete from multiple level inheritance diff --git a/packages/sdk/package.json b/packages/sdk/package.json index ac8bcaf1d..8d81caac9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -23,7 +23,7 @@ "@prisma/internals-v5": "npm:@prisma/internals@^5.0.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", - "langium": "1.2.0", + "langium": "1.3.1", "lower-case-first": "^2.0.2", "prettier": "^2.8.3 || 3.x", "semver": "^7.5.2", diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index e038c6958..1e0d22d67 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -12,6 +12,7 @@ export enum ExpressionContext { DefaultValue = 'DefaultValue', AccessPolicy = 'AccessPolicy', ValidationRule = 'ValidationRule', + Index = 'Index', } export const STD_LIB_MODULE_NAME = 'stdlib.zmodel'; diff --git a/packages/sdk/src/zmodel-code-generator.ts b/packages/sdk/src/zmodel-code-generator.ts index 1b1f001e1..96aaa87d9 100644 --- a/packages/sdk/src/zmodel-code-generator.ts +++ b/packages/sdk/src/zmodel-code-generator.ts @@ -49,6 +49,7 @@ export interface ZModelCodeOptions { binaryExprNumberOfSpaces: number; unaryExprNumberOfSpaces: number; indent: number; + quote: 'single' | 'double'; } // a registry of generation handlers marked with @gen @@ -75,6 +76,7 @@ export class ZModelCodeGenerator { binaryExprNumberOfSpaces: options?.binaryExprNumberOfSpaces ?? 1, unaryExprNumberOfSpaces: options?.unaryExprNumberOfSpaces ?? 0, indent: options?.indent ?? 4, + quote: options?.quote ?? 'single', }; } @@ -224,7 +226,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(StringLiteral) private _generateLiteralExpr(ast: LiteralExpr) { - return `'${ast.value}'`; + return this.options.quote === 'single' ? `'${ast.value}'` : `"${ast.value}"`; } @gen(NumberLiteral) @@ -265,7 +267,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(ReferenceArg) private _generateReferenceArg(ast: ReferenceArg) { - return `${ast.name}:${ast.value}`; + return `${ast.name}:${this.generate(ast.value)}`; } @gen(MemberAccessExpr) @@ -321,7 +323,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ } private argument(ast: Argument) { - return `${ast.name ? ast.name + ': ' : ''}${this.generate(ast.value)}`; + return this.generate(ast.value); } private get binaryExprSpace() { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 84eb36b60..3472ddca0 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -24,7 +24,7 @@ "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "json5": "^2.2.3", - "langium": "1.2.0", + "langium": "1.3.1", "pg": "^8.11.1", "tmp": "^0.2.1", "vscode-uri": "^3.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c8f28205..602861b14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,12 +69,12 @@ importers: packages/language: dependencies: langium: - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.3.1 + version: 1.3.1 devDependencies: langium-cli: - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.3.1 + version: 1.3.1 plist2: specifier: ^1.1.3 version: 1.1.3 @@ -487,8 +487,8 @@ importers: specifier: ^5.0.1 version: 5.0.1 langium: - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.3.1 + version: 1.3.1 lower-case-first: specifier: ^2.0.2 version: 2.0.2 @@ -624,8 +624,8 @@ importers: specifier: workspace:* version: link:../runtime/dist langium: - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.3.1 + version: 1.3.1 lower-case-first: specifier: ^2.0.2 version: 2.0.2 @@ -748,8 +748,8 @@ importers: specifier: ^2.2.3 version: 2.2.3 langium: - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.3.1 + version: 1.3.1 pg: specifier: ^8.11.1 version: 8.11.1 @@ -10270,8 +10270,8 @@ packages: resolution: {integrity: sha512-dWl0Dbjm6Xm+kDxhPQJsCBTxrJzuGl0aP9rhr+TG8D3l+GL90N8O8lYUi7dTSAN2uuDqCtNgb6aEuQH5wsiV8Q==} dev: true - /langium-cli@1.2.0: - resolution: {integrity: sha512-DPyJUd4Hj8+OBNEcAQyJtW6e38+UPd758gTI7Ep0r/sDogrwJ/GJHx5nGA+r0ygpNcDPG+mS9Hw8Y05uCNNcoQ==} + /langium-cli@1.3.1: + resolution: {integrity: sha512-9faKpioKCjBD0Z4y165+wQlDFiDHOXYBlhPVgbV+neSnSB70belZLNfykAVa564360h7Br/5PogR5jW2n/tOKw==} engines: {node: '>=14.0.0'} hasBin: true dependencies: @@ -10279,12 +10279,20 @@ packages: commander: 10.0.1 fs-extra: 11.1.1 jsonschema: 1.4.1 - langium: 1.2.0 + langium: 1.3.1 + langium-railroad: 1.3.0 lodash: 4.17.21 dev: true - /langium@1.2.0: - resolution: {integrity: sha512-jFSptpFljYo9ZTHrq/GZflMUXiKo5KBNtsaIJtnIzDm9zC2FxsxejEFAtNL09262RVQt+zFeF/2iLAShFTGitw==} + /langium-railroad@1.3.0: + resolution: {integrity: sha512-I3gx79iF+Qpn2UjzfHLf2GENAD9mPdSZHL3juAZLBsxznw4se7MBrJX32oPr/35DTjU9q99wFCQoCXu7mcf+Bg==} + dependencies: + langium: 1.3.1 + railroad-diagrams: 1.0.0 + dev: true + + /langium@1.3.1: + resolution: {integrity: sha512-xC+DnAunl6cZIgYjRpgm3s1kYAB5/Wycsj24iYaXG9uai7SgvMaFZSrRvdA5rUK/lSta/CRvgF+ZFoEKEOFJ5w==} engines: {node: '>=14.0.0'} dependencies: chevrotain: 10.4.2 @@ -12657,6 +12665,10 @@ packages: resolution: {integrity: sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A==} dev: true + /railroad-diagrams@1.0.0: + resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + dev: true + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts index 6b5ae7c3a..2dc9bdbf6 100644 --- a/tests/integration/tests/cli/init.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -9,6 +9,7 @@ import { createProgram } from '../../../../packages/schema/src/cli'; import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; import { createNpmrc } from './share'; +// Skipping these tests as they seem to cause hangs intermittently when running with other tests // eslint-disable-next-line jest/no-disabled-tests describe.skip('CLI init command tests', () => { let origDir: 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 67b97776a..f7fcc7266 100644 --- a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts +++ b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts @@ -76,4 +76,84 @@ describe('Omit test', () => { expect(e.profile.image).toBeUndefined(); }); }); + + it('customization', async () => { + const { prisma, enhance } = await loadSchema(model, { + output: './zen', + enhancements: ['omit'], + }); + + const db = enhance(prisma, { loadPath: './zen' }); + const r = await db.user.create({ + include: { profile: true }, + data: { + id: '1', + password: 'abc123', + profile: { create: { image: 'an image' } }, + }, + }); + expect(r.password).toBeUndefined(); + expect(r.profile.image).toBeUndefined(); + + const db1 = enhance(prisma, { modelMeta: require(path.resolve('./zen/model-meta')).default }); + const r1 = await db1.user.create({ + include: { profile: true }, + data: { + id: '2', + password: 'abc123', + profile: { create: { image: 'an image' } }, + }, + }); + expect(r1.password).toBeUndefined(); + expect(r1.profile.image).toBeUndefined(); + }); + + it('to-many', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id @default(cuid()) + posts Post[] + + @@allow('all', true) + } + + model Post { + id String @id @default(cuid()) + user User @relation(fields: [userId], references: [id]) + userId String + images Image[] + + @@allow('all', true) + } + + model Image { + id String @id @default(cuid()) + post Post @relation(fields: [postId], references: [id]) + postId String + url String @omit + + @@allow('all', true) + } + `, + { enhancements: ['omit'] } + ); + + const db = enhance(); + const r = await db.user.create({ + include: { posts: { include: { images: true } } }, + data: { + posts: { + create: [ + { images: { create: { url: 'img1' } } }, + { images: { create: [{ url: 'img2' }, { url: 'img3' }] } }, + ], + }, + }, + }); + + expect(r.posts[0].images[0].url).toBeUndefined(); + expect(r.posts[1].images[0].url).toBeUndefined(); + expect(r.posts[1].images[1].url).toBeUndefined(); + }); }); 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 ca71841db..16f56dddd 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -35,6 +35,8 @@ describe('With Policy: field validation', () => { text3 String @length(min: 3) text4 String @length(max: 5) text5 String? @endsWith('xyz') + text6 String? @trim @lower + text7 String? @upper @@allow('all', true) } @@ -495,4 +497,61 @@ describe('With Policy: field validation', () => { }) ).toResolveTruthy(); }); + + it('string transformation', async () => { + await db.user.create({ + data: { + id: '1', + password: 'abc123!@#', + email: 'who@myorg.com', + handle: 'user1', + }, + }); + + await expect( + db.userData.create({ + data: { + userId: '1', + a: 1, + b: 0, + c: -1, + d: 0, + text1: 'abc123', + text2: 'def', + text3: 'aaa', + text4: 'abcab', + text6: ' AbC ', + text7: 'abc', + }, + }) + ).resolves.toMatchObject({ text6: 'abc', text7: 'ABC' }); + + await expect( + db.user.create({ + data: { + id: '2', + password: 'abc123!@#', + email: 'who@myorg.com', + handle: 'user2', + userData: { + create: { + a: 1, + b: 0, + c: -1, + d: 0, + text1: 'abc123', + text2: 'def', + text3: 'aaa', + text4: 'abcab', + text6: ' AbC ', + text7: 'abc', + }, + }, + }, + include: { userData: true }, + }) + ).resolves.toMatchObject({ + userData: expect.objectContaining({ text6: 'abc', text7: 'ABC' }), + }); + }); }); diff --git a/tests/integration/tests/frameworks/trpc/generation.test.ts b/tests/integration/tests/frameworks/trpc/generation.test.ts index 3c867bc0f..a58f5965d 100644 --- a/tests/integration/tests/frameworks/trpc/generation.test.ts +++ b/tests/integration/tests/frameworks/trpc/generation.test.ts @@ -21,6 +21,7 @@ describe('tRPC Routers Generation Tests', () => { `${path.join(__dirname, '../../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../../.build/zenstackhq-trpc-' + ver + '.tgz')}`, + `${path.join(__dirname, '../../../../../.build/zenstackhq-server-' + ver + '.tgz')}`, ]; const deps = depPkgs.join(' '); diff --git a/tests/integration/tests/regression/issue-965.test.ts b/tests/integration/tests/regression/issue-965.test.ts new file mode 100644 index 000000000..79bd92075 --- /dev/null +++ b/tests/integration/tests/regression/issue-965.test.ts @@ -0,0 +1,53 @@ +import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; + +describe('Regression: issue 965', () => { + it('regression1', async () => { + await loadModel(` + abstract model Base { + id String @id @default(cuid()) + } + + abstract model A { + URL String? @url + } + + abstract model B { + anotherURL String? @url + } + + abstract model C { + oneMoreURL String? @url + } + + model D extends Base, A, B { + } + + model E extends Base, B, C { + }`); + }); + + it('regression2', async () => { + await expect( + loadModelWithError(` + abstract model A { + URL String? @url + } + + abstract model B { + anotherURL String? @url + } + + abstract model C { + oneMoreURL String? @url + } + + model D extends A, B { + } + + model E extends B, C { + }`) + ).resolves.toContain( + 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.' + ); + }); +}); diff --git a/tests/integration/tests/regression/issue-971.test.ts b/tests/integration/tests/regression/issue-971.test.ts new file mode 100644 index 000000000..40990aa6a --- /dev/null +++ b/tests/integration/tests/regression/issue-971.test.ts @@ -0,0 +1,23 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 971', () => { + it('regression', async () => { + await loadSchema( + ` + abstract model Level1 { + id String @id @default(cuid()) + URL String? + @@validate(URL != null, "URL must be provided") // works + } + abstract model Level2 extends Level1 { + @@validate(URL != null, "URL must be provided") // works + } + abstract model Level3 extends Level2 { + @@validate(URL != null, "URL must be provided") // doesn't work + } + model Foo extends Level3 { + } + ` + ); + }); +}); diff --git a/tests/integration/tests/regression/issue-992.test.ts b/tests/integration/tests/regression/issue-992.test.ts new file mode 100644 index 000000000..40a1aac47 --- /dev/null +++ b/tests/integration/tests/regression/issue-992.test.ts @@ -0,0 +1,45 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 992', () => { + it('regression', async () => { + const { enhance, prisma } = await loadSchema( + ` + model Product { + id String @id @default(cuid()) + category Category @relation(fields: [categoryId], references: [id]) + categoryId String + + deleted Int @default(0) @omit + @@deny('read', deleted != 0) + @@allow('all', true) + } + + model Category { + id String @id @default(cuid()) + products Product[] + @@allow('all', true) + } + ` + ); + + await prisma.category.create({ + data: { + products: { + create: [ + { + deleted: 0, + }, + { + deleted: 0, + }, + ], + }, + }, + }); + + const db = enhance(); + const category = await db.category.findFirst({ include: { products: true } }); + expect(category.products[0].deleted).toBeUndefined(); + expect(category.products[1].deleted).toBeUndefined(); + }); +}); From 7b453f7745cad73fc81e7884faf473aecda99556 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 14 Feb 2024 21:24:49 +0800 Subject: [PATCH 010/127] fix: improve generated typing for polymorphic models (#1002) --- package.json | 5 +- .../src/enhancements/create-enhancement.ts | 56 ++++++++++--------- .../runtime/src/enhancements/default-auth.ts | 6 +- packages/runtime/src/enhancements/delegate.ts | 6 +- packages/runtime/src/enhancements/omit.ts | 8 +-- packages/runtime/src/enhancements/password.ts | 11 ++-- .../src/enhancements/policy/handler.ts | 6 +- .../runtime/src/enhancements/policy/index.ts | 4 +- .../src/enhancements/policy/policy-utils.ts | 4 +- packages/runtime/src/enhancements/proxy.ts | 13 +++-- .../runtime/src/enhancements/query-utils.ts | 4 +- .../src/plugins/enhancer/enhance/index.ts | 39 ++++++++++++- .../src/plugins/prisma/schema-generator.ts | 9 ++- packages/sdk/src/utils.ts | 10 ++++ .../integration/tests/misc/stacktrace.test.ts | 2 +- 15 files changed, 123 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 83d9f7072..fd0c2763d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "test-ci": "ZENSTACK_TEST=1 pnpm -r run test --silent --forceExit", "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\"" }, "keywords": [], "author": "", diff --git a/packages/runtime/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index dbca40874..1b9796970 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -32,60 +32,64 @@ export type TransactionIsolationLevel = | 'Snapshot' | 'Serializable'; -/** - * Options for {@link createEnhancement} - */ export type EnhancementOptions = { /** - * Policy definition + * The kinds of enhancements to apply. By default all enhancements are applied. */ - policy: PolicyDef; + kinds?: EnhancementKind[]; /** - * Model metadata + * Whether to log Prisma query */ - modelMeta: ModelMeta; + logPrismaQuery?: boolean; /** - * Zod schemas for validation + * Hook for transforming errors before they are thrown to the caller. */ - zodSchemas?: ZodSchemas; + errorTransformer?: ErrorTransformer; /** - * Whether to log Prisma query + * The `maxWait` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. */ - logPrismaQuery?: boolean; + transactionMaxWait?: number; /** - * The Node module that contains PrismaClient + * The `timeout` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - prismaModule: any; + transactionTimeout?: number; /** - * The kinds of enhancements to apply. By default all enhancements are applied. + * The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. */ - kinds?: EnhancementKind[]; + transactionIsolationLevel?: TransactionIsolationLevel; +}; +/** + * Options for {@link createEnhancement} + * + * @private + */ +export type InternalEnhancementOptions = EnhancementOptions & { /** - * Hook for transforming errors before they are thrown to the caller. + * Policy definition */ - errorTransformer?: ErrorTransformer; + policy: PolicyDef; /** - * The `maxWait` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. + * Model metadata */ - transactionMaxWait?: number; + modelMeta: ModelMeta; /** - * The `timeout` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. + * Zod schemas for validation */ - transactionTimeout?: number; + zodSchemas?: ZodSchemas; /** - * The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. + * The Node module that contains PrismaClient */ - transactionIsolationLevel?: TransactionIsolationLevel; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prismaModule: any; }; /** @@ -103,13 +107,15 @@ let hasDefaultAuth: boolean | undefined = undefined; * 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: EnhancementOptions, + options: InternalEnhancementOptions, context?: EnhancementContext ) { if (!prisma) { diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts index 9e0a64a4f..cce9af782 100644 --- a/packages/runtime/src/enhancements/default-auth.ts +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -4,7 +4,7 @@ import deepcopy from 'deepcopy'; import { FieldInfo, NestedWriteVisitor, PrismaWriteActionType, enumerate, getFields } from '../cross'; import { DbClientContract } from '../types'; -import { EnhancementContext, EnhancementOptions } from './create-enhancement'; +import { EnhancementContext, InternalEnhancementOptions } from './create-enhancement'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; /** @@ -14,7 +14,7 @@ import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './prox */ export function withDefaultAuth( prisma: DbClient, - options: EnhancementOptions, + options: InternalEnhancementOptions, context?: EnhancementContext ): DbClient { return makeProxy( @@ -31,7 +31,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { constructor( prisma: DbClientContract, model: string, - options: EnhancementOptions, + options: InternalEnhancementOptions, private readonly context?: EnhancementContext ) { super(prisma, model, options); diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index 0a1e39d8c..7032a965a 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -15,13 +15,13 @@ import { resolveField, } from '../cross'; import type { CrudContract, DbClientContract } from '../types'; -import type { EnhancementOptions } from './create-enhancement'; +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: EnhancementOptions): DbClient { +export function withDelegate(prisma: DbClient, options: InternalEnhancementOptions): DbClient { return makeProxy( prisma, options.modelMeta, @@ -34,7 +34,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { private readonly logger: Logger; private readonly queryUtils: QueryUtils; - constructor(prisma: DbClientContract, model: string, options: EnhancementOptions) { + constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { super(prisma, model, options); this.logger = new Logger(prisma); this.queryUtils = new QueryUtils(prisma, this.options); diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index e51f9cb47..fa834166d 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { enumerate, getModelFields, resolveField, type ModelMeta } from '../cross'; +import { enumerate, getModelFields, resolveField } from '../cross'; import { DbClientContract } from '../types'; -import { EnhancementOptions } from './create-enhancement'; +import { InternalEnhancementOptions } from './create-enhancement'; import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; /** @@ -11,7 +11,7 @@ import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; * * @private */ -export function withOmit(prisma: DbClient, options: EnhancementOptions): DbClient { +export function withOmit(prisma: DbClient, options: InternalEnhancementOptions): DbClient { return makeProxy( prisma, options.modelMeta, @@ -21,7 +21,7 @@ export function withOmit(prisma: DbClient, options: Enh } class OmitHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, options: EnhancementOptions) { + constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { super(prisma, model, options); } diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index 7fef04dd8..f83939792 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -3,9 +3,9 @@ import { hash } from 'bcryptjs'; import { DEFAULT_PASSWORD_SALT_LENGTH } from '../constants'; -import { NestedWriteVisitor, type ModelMeta, type PrismaWriteActionType } from '../cross'; +import { NestedWriteVisitor, type PrismaWriteActionType } from '../cross'; import { DbClientContract } from '../types'; -import { EnhancementOptions } from './create-enhancement'; +import { InternalEnhancementOptions } from './create-enhancement'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; /** @@ -13,7 +13,10 @@ import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './prox * * @private */ -export function withPassword(prisma: DbClient, options: EnhancementOptions): DbClient { +export function withPassword( + prisma: DbClient, + options: InternalEnhancementOptions +): DbClient { return makeProxy( prisma, options.modelMeta, @@ -23,7 +26,7 @@ export function withPassword(prisma: DbClient, op } class PasswordHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, options: EnhancementOptions) { + constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { super(prisma, model, options); } diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 6ae173fcd..383ee356f 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -16,8 +16,8 @@ import { type FieldInfo, type ModelMeta, } from '../../cross'; -import { type CrudContract, type DbClientContract, PolicyOperationKind } from '../../types'; -import type { EnhancementContext, EnhancementOptions } from '../create-enhancement'; +import { PolicyOperationKind, type CrudContract, type DbClientContract } from '../../types'; +import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; import { Logger } from '../logger'; import { PrismaProxyHandler } from '../proxy'; import { QueryUtils } from '../query-utils'; @@ -49,7 +49,7 @@ export class PolicyProxyHandler implements Pr constructor( private readonly prisma: DbClient, model: string, - private readonly options: EnhancementOptions, + private readonly options: InternalEnhancementOptions, private readonly context?: EnhancementContext ) { this.logger = new Logger(prisma); diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index 4d1e8b89d..e197e18c1 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -3,7 +3,7 @@ import { getIdFields } from '../../cross'; import { DbClientContract } from '../../types'; import { hasAllFields } from '../../validation'; -import type { EnhancementContext, EnhancementOptions } from '../create-enhancement'; +import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; import { makeProxy } from '../proxy'; import { PolicyProxyHandler } from './handler'; @@ -19,7 +19,7 @@ import { PolicyProxyHandler } from './handler'; */ export function withPolicy( prisma: DbClient, - options: EnhancementOptions, + options: InternalEnhancementOptions, context?: EnhancementContext ): DbClient { const { modelMeta, policy } = options; diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 1f4629359..00c6a51b6 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -19,7 +19,7 @@ import { 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, EnhancementOptions } from '../create-enhancement'; +import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; import { Logger } from '../logger'; import { QueryUtils } from '../query-utils'; import type { InputCheckFunc, PolicyDef, ReadFieldCheckFunc, ZodSchemas } from '../types'; @@ -38,7 +38,7 @@ export class PolicyUtil extends QueryUtils { constructor( private readonly db: DbClientContract, - options: EnhancementOptions, + options: InternalEnhancementOptions, context?: EnhancementContext, private readonly shouldLogQuery = false ) { diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index e0302f7e9..a3141ad0a 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -3,7 +3,7 @@ import { PRISMA_PROXY_ENHANCER } from '../constants'; import type { ModelMeta } from '../cross'; import type { DbClientContract } from '../types'; -import { EnhancementOptions } from './create-enhancement'; +import { InternalEnhancementOptions } from './create-enhancement'; import { createDeferredPromise } from './policy/promise'; /** @@ -67,7 +67,7 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { constructor( protected readonly prisma: DbClientContract, protected readonly model: string, - protected readonly options: EnhancementOptions + protected readonly options: InternalEnhancementOptions ) {} async findUnique(args: any): Promise { @@ -241,7 +241,7 @@ export function makeProxy( return propVal; } - return createHandlerProxy(makeHandler(target, prop), propVal, errorTransformer); + return createHandlerProxy(makeHandler(target, prop), propVal, prop, errorTransformer); }, }); @@ -252,6 +252,7 @@ export function makeProxy( function createHandlerProxy( handler: T, origTarget: any, + model: string, errorTransformer?: ErrorTransformer ): T { return new Proxy(handler, { @@ -282,7 +283,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) { @@ -308,9 +309,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 index f92353081..6959b922f 100644 --- a/packages/runtime/src/enhancements/query-utils.ts +++ b/packages/runtime/src/enhancements/query-utils.ts @@ -9,11 +9,11 @@ import { } from '../cross'; import { CrudContract, DbClientContract } from '../types'; import { getVersion } from '../version'; -import { EnhancementOptions } from './create-enhancement'; +import { InternalEnhancementOptions } from './create-enhancement'; import { prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; export class QueryUtils { - constructor(private readonly prisma: DbClientContract, private readonly options: EnhancementOptions) {} + constructor(private readonly prisma: DbClientContract, private readonly options: InternalEnhancementOptions) {} getIdFields(model: string) { return getIdFields(this.options.modelMeta, model, true); diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index c33de08b0..1d42b5912 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -95,7 +95,8 @@ async function processClientTypes(model: Model, prismaClientDir: string) { removeAuxRelationFields(desc, toRemove, traversal); fixDelegateUnionType(desc, delegateModels, toReplaceText, traversal); removeCreateFromDelegateInputTypes(desc, delegateModels, toRemove, traversal); - removeToplevelCreates(desc, delegateModels, toRemove, traversal); + removeDelegateToplevelCreates(desc, delegateModels, toRemove, traversal); + removeDiscriminatorFromConcreteInputTypes(desc, delegateModels, toRemove); }); toRemove.forEach((n) => n.remove()); @@ -134,7 +135,6 @@ function fixDelegateUnionType( delegateModels.forEach(([delegate, concreteModels]) => { if (name === `$${delegate.name}Payload`) { const discriminator = getDiscriminatorField(delegate); - // const discriminator = 'delegateType'; // delegate.fields.find((f) => hasAttribute(f, '@discriminator')); if (discriminator) { toReplaceText.push([ desc, @@ -178,7 +178,40 @@ function removeCreateFromDelegateInputTypes( }); } -function removeToplevelCreates( +function removeDiscriminatorFromConcreteInputTypes( + desc: Node, + delegateModels: [DataModel, DataModel[]][], + toRemove: (PropertySignature | MethodSignature)[] +) { + if (!desc.isKind(SyntaxKind.TypeAliasDeclaration)) { + return; + } + + const name = desc.getName(); + delegateModels.forEach(([delegate, concretes]) => { + const discriminator = getDiscriminatorField(delegate); + if (!discriminator) { + return; + } + + concretes.forEach((concrete) => { + // remove discriminator field from the create/update input of concrete models + const regex = new RegExp(`\\${concrete.name}(Unchecked)?(Create|Update).*Input`); + if (regex.test(name)) { + desc.forEachDescendant((d, innerTraversal) => { + if (d.isKind(SyntaxKind.PropertySignature)) { + if (d.getName() === discriminator.name) { + toRemove.push(d); + } + innerTraversal.skip(); + } + }); + } + }); + }); +} + +function removeDelegateToplevelCreates( desc: Node, delegateModels: [DataModel, DataModel[]][], toRemove: (PropertySignature | MethodSignature)[], diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 6592d83b5..72d2a02e6 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -297,7 +297,14 @@ export class PrismaSchemaGenerator { const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name); for (const field of decl.fields) { if (field.$inheritedFrom) { - if (field.$inheritedFrom.isAbstract || this.mode === 'logical' || isIdField(field)) { + 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 { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 0bd98e63e..01d5d274d 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -383,6 +383,16 @@ 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') diff --git a/tests/integration/tests/misc/stacktrace.test.ts b/tests/integration/tests/misc/stacktrace.test.ts index 08454d529..f652c5514 100644 --- a/tests/integration/tests/misc/stacktrace.test.ts +++ b/tests/integration/tests/misc/stacktrace.test.ts @@ -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(); From 56e474433ba0a251cd547a1cf73116a86f3aca00 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 16:10:46 -0800 Subject: [PATCH 011/127] chore: merge from dev (#1017) --- .github/workflows/build-test.yml | 2 +- .gitignore | 1 + CONTRIBUTING.md | 8 + jest.config.ts | 6 +- package.json | 6 +- packages/ide/jetbrains/CHANGELOG.md | 5 + .../plugins/tanstack-query/src/runtime/vue.ts | 4 +- packages/schema/src/cli/actions/repl.ts | 8 +- .../validator/datamodel-validator.ts | 10 +- .../validator/schema-validator.ts | 3 +- .../src/plugins/prisma/schema-generator.ts | 10 +- packages/schema/src/res/stdlib.zmodel | 4 +- packages/schema/src/utils/exec-utils.ts | 8 + packages/schema/tests/schema/stdlib.test.ts | 2 +- .../validation/attribute-validation.test.ts | 20 + .../validation/datamodel-validation.test.ts | 427 ++++++++++-------- .../validation/datasource-validation.test.ts | 25 +- .../validation/schema-validation.test.ts | 14 + packages/schema/tests/utils.ts | 38 +- packages/testtools/package.json | 2 +- packages/testtools/src/.npmrc.template | 1 - packages/testtools/src/package.template.json | 21 - packages/testtools/src/schema.ts | 67 ++- pnpm-lock.yaml | 262 +++++++++++ script/test-scaffold.ts | 24 + test-setup.ts | 9 + tests/integration/global-setup.js | 10 - tests/integration/jest.config.ts | 26 +- tests/integration/package.json | 2 +- tests/integration/tests/cli/generate.test.ts | 6 +- tests/integration/tests/cli/plugins.test.ts | 6 +- tests/integration/tests/schema/todo.zmodel | 2 +- 32 files changed, 732 insertions(+), 307 deletions(-) delete mode 100644 packages/testtools/src/.npmrc.template delete mode 100644 packages/testtools/src/package.template.json create mode 100644 script/test-scaffold.ts create mode 100644 test-setup.ts delete mode 100644 tests/integration/global-setup.js diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index cbdbce76c..8e8ce08f3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -90,4 +90,4 @@ jobs: run: pnpm install --frozen-lockfile - name: Test - run: pnpm run test-ci + run: pnpm run test-scaffold && pnpm run test-ci diff --git a/.gitignore b/.gitignore index 9b5f3d7c8..307f58a86 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist .npmcache coverage .build +.test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eed5723d..659fe2184 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,14 @@ I want to think you first for considering contributing to ZenStack 🙏🏻. It' pnpm build ``` +1. Scaffold the project used for testing + + ```bash + pnpm test-scaffold + ``` + + You only need to run this command once. + 1. Run tests ```bash diff --git a/jest.config.ts b/jest.config.ts index 917cf52f6..222e6fb2c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,15 +3,19 @@ * https://jestjs.io/docs/configuration */ +import path from 'path'; + export default { // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, + globalSetup: path.join(__dirname, './test-setup.ts'), + // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // The directory where Jest should output its coverage files - coverageDirectory: 'tests/coverage', + coverageDirectory: path.join(__dirname, '.test/coverage'), // An array of regexp pattern strings used to skip coverage collection coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], diff --git a/package.json b/package.json index fd0c2763d..f372eb616 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "scripts": { "build": "pnpm -r build", "lint": "pnpm -r lint", - "test": "ZENSTACK_TEST=1 pnpm -r run test --silent --forceExit", - "test-ci": "ZENSTACK_TEST=1 pnpm -r run test --silent --forceExit", + "test": "ZENSTACK_TEST=1 pnpm -r --parallel run test --silent --forceExit", + "test-ci": "ZENSTACK_TEST=1 pnpm -r --parallel run 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\"", @@ -33,6 +34,7 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tsup": "^8.0.1", + "tsx": "^4.7.1", "typescript": "^5.3.2" } } diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index 4f4625001..1fa15f2eb 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog ## [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 - Auto-completion is now supported inside attributes. diff --git a/packages/plugins/tanstack-query/src/runtime/vue.ts b/packages/plugins/tanstack-query/src/runtime/vue.ts index a0f1055e8..b0a35f5f3 100644 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime/vue.ts @@ -61,7 +61,7 @@ export function useModelQuery( model: string, url: string, args?: unknown, - options?: UseQueryOptions, + options?: Omit, 'queryKey'>, fetch?: FetchFn, optimisticUpdate = false ) { @@ -87,7 +87,7 @@ export function useInfiniteModelQuery( model: string, url: string, args?: unknown, - options?: UseInfiniteQueryOptions, + options?: Omit, 'queryKey'>, fetch?: FetchFn ) { return useInfiniteQuery({ diff --git a/packages/schema/src/cli/actions/repl.ts b/packages/schema/src/cli/actions/repl.ts index 6ca3c3503..df15e30fb 100644 --- a/packages/schema/src/cli/actions/repl.ts +++ b/packages/schema/src/cli/actions/repl.ts @@ -2,7 +2,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import colors from 'colors'; import path from 'path'; -import prettyRepl from 'pretty-repl'; import { inspect } from 'util'; // inspired by: https://github.com/Kinjalrk2k/prisma-console @@ -11,6 +10,13 @@ import { inspect } from 'util'; * CLI action for starting a REPL session */ export async function repl(projectPath: string, options: { prismaClient?: string; debug?: boolean; table?: boolean }) { + if (!process?.stdout?.isTTY && process?.versions?.bun) { + console.error('REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.'); + return; + } + + const prettyRepl = await import('pretty-repl') + console.log('Welcome to ZenStack REPL. See help with the ".help" command.'); console.log('Global variables:'); console.log(` ${colors.blue('db')} to access enhanced PrismaClient`); diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index ac727b917..ec5c3ef0b 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -5,6 +5,7 @@ import { isDataModel, isStringLiteral, ReferenceExpr, + isEnum, } from '@zenstackhq/language/ast'; import { getLiteral, getModelIdFields, getModelUniqueFields, isDelegateModel } from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium'; @@ -61,8 +62,13 @@ export default class DataModelValidator implements AstValidator { if (idField.type.optional) { accept('error', 'Field with @id attribute must not be optional', { node: idField }); } - if (idField.type.array || !idField.type.type || !SCALAR_TYPES.includes(idField.type.type)) { - accept('error', 'Field with @id attribute must be of scalar type', { node: idField }); + + const isArray = idField.type.array; + const isScalar = SCALAR_TYPES.includes(idField.type.type as typeof SCALAR_TYPES[number]) + const isValidType = isScalar || isEnum(idField.type.reference?.ref) + + if (isArray || !isValidType) { + accept('error', 'Field with @id attribute must be of scalar or enum type', { node: idField }); } }); } diff --git a/packages/schema/src/language-server/validator/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts index d3722638e..9e0512547 100644 --- a/packages/schema/src/language-server/validator/schema-validator.ts +++ b/packages/schema/src/language-server/validator/schema-validator.ts @@ -52,8 +52,9 @@ export default class SchemaValidator implements AstValidator { private validateImports(model: Model, accept: ValidationAcceptor) { model.imports.forEach((imp) => { const importedModel = resolveImport(this.documents, imp); + const importPath = imp.path.endsWith('.zmodel') ? imp.path : `${imp.path}.zmodel`; if (!importedModel) { - accept('error', `Cannot find model file ${imp.path}.zmodel`, { node: imp }); + accept('error', `Cannot find model file ${importPath}`, { node: imp }); } }); } diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 72d2a02e6..7eebc3040 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -56,7 +56,7 @@ import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; import telemetry from '../../telemetry'; -import { execSync } from '../../utils/exec-utils'; +import { execPackage } from '../../utils/exec-utils'; import { findPackageJson } from '../../utils/pkg-utils'; import { AttributeArgValue, @@ -142,7 +142,7 @@ export class PrismaSchemaGenerator { if (options.format === true) { try { // run 'prisma format' - await execSync(`npx prisma format --schema ${outFile}`, { stdio: 'ignore' }); + await execPackage(`prisma format --schema ${outFile}`, { stdio: 'ignore' }); } catch { warnings.push(`Failed to format Prisma schema file`); } @@ -151,18 +151,18 @@ export class PrismaSchemaGenerator { const generateClient = options.generateClient !== false; if (generateClient) { - let generateCmd = `npx prisma generate --schema "${outFile}"`; + let generateCmd = `prisma generate --schema "${outFile}"`; if (typeof options.generateArgs === 'string') { generateCmd += ` ${options.generateArgs}`; } try { // run 'prisma generate' - await execSync(generateCmd, { stdio: 'ignore' }); + await execPackage(generateCmd, { stdio: 'ignore' }); } catch { await this.trackPrismaSchemaError(outFile); try { // run 'prisma generate' again with output to the console - await execSync(generateCmd); + await execPackage(generateCmd); } catch { // noop } diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index fd470efb8..266b5c517 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -390,7 +390,7 @@ attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma /** * Add full text index (MySQL only). */ -attribute @@fulltext(_ fields: FieldReference[]) @@@prisma +attribute @@fulltext(_ fields: FieldReference[], map: String?) @@@prisma // String type modifiers @@ -479,7 +479,7 @@ attribute @db.Bytes() @@@targetField([BytesField]) @@@prisma attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma attribute @db.LongBlob() @@@targetField([BytesField]) @@@prisma attribute @db.Binary() @@@targetField([BytesField]) @@@prisma -attribute @db.VarBinary() @@@targetField([BytesField]) @@@prisma +attribute @db.VarBinary(_ x: Int?) @@@targetField([BytesField]) @@@prisma attribute @db.TinyBlob() @@@targetField([BytesField]) @@@prisma attribute @db.Blob() @@@targetField([BytesField]) @@@prisma attribute @db.MediumBlob() @@@targetField([BytesField]) @@@prisma diff --git a/packages/schema/src/utils/exec-utils.ts b/packages/schema/src/utils/exec-utils.ts index d88e42b3d..03060a4f9 100644 --- a/packages/schema/src/utils/exec-utils.ts +++ b/packages/schema/src/utils/exec-utils.ts @@ -8,3 +8,11 @@ export function execSync(cmd: string, options?: Omit & { 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, options?: Omit & { env?: Record }): void { + const packageManager = process?.versions?.bun ? 'bunx' : 'npx'; + execSync(`${packageManager} ${cmd}`, options) +} \ No newline at end of file diff --git a/packages/schema/tests/schema/stdlib.test.ts b/packages/schema/tests/schema/stdlib.test.ts index f4b1cc1fe..ad637be7a 100644 --- a/packages/schema/tests/schema/stdlib.test.ts +++ b/packages/schema/tests/schema/stdlib.test.ts @@ -24,7 +24,7 @@ describe('Stdlib Tests', () => { }` ); } - throw new SchemaLoadingError(validationErrors.map((e) => e.message)); + throw new SchemaLoadingError(validationErrors); } }); }); diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index ac87665b1..611f8dc60 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -226,6 +226,25 @@ describe('Attribute tests', () => { } `); + await loadModel(` + ${ prelude } + model A { + id String @id + x String + y String + z String + @@fulltext([x, y, z]) + } + + model B { + id String @id + x String + y String + z String + @@fulltext([x, y, z], map: "n") + } + `); + await loadModel(` ${prelude} model A { @@ -352,6 +371,7 @@ describe('Attribute tests', () => { _longBlob Bytes @db.LongBlob _binary Bytes @db.Binary _varBinary Bytes @db.VarBinary + _varBinarySized Bytes @db.VarBinary(100) _tinyBlob Bytes @db.TinyBlob _blob Bytes @db.Blob _mediumBlob Bytes @db.MediumBlob diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 19535d5dd..955f315c3 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -1,4 +1,4 @@ -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, safelyLoadModel, errorLike } from '../../utils'; describe('Data Model Validation Tests', () => { const prelude = ` @@ -9,20 +9,20 @@ describe('Data Model Validation Tests', () => { `; it('duplicated fields', async () => { - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int - x String - } - `) - ).toContain('Duplicated declaration name "x"'); + const result = await safelyLoadModel(` + ${prelude} + model M { + id String @id + x Int + x String + } + `); + + expect(result).toMatchObject(errorLike('Duplicated declaration name "x"')); }); it('scalar types', async () => { - await loadModel(` + const result = await safelyLoadModel(` ${prelude} model M { id String @id @@ -38,33 +38,36 @@ describe('Data Model Validation Tests', () => { i Bytes } `); + expect(result).toMatchObject({ status: 'fulfilled' }); }); it('Unsupported type valid arg', async () => { - await loadModel(` + const result = await safelyLoadModel(` ${prelude} model M { id String @id a Unsupported('foo') } `); + + expect(result).toMatchObject({ status: 'fulfilled' }); }); it('Unsupported type invalid arg', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id a Unsupported(123) } `) - ).toContain('Unsupported type argument must be a string literal'); + ).toMatchObject(errorLike('Unsupported type argument must be a string literal')); }); it('Unsupported type used in expression', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id @@ -72,79 +75,83 @@ describe('Data Model Validation Tests', () => { @@allow('all', a == 'a') } `) - ).toContain('Field of "Unsupported" type cannot be used in expressions'); + ).toMatchObject(errorLike('Field of "Unsupported" type cannot be used in expressions')); }); it('mix array and optional', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Int[]? } `) - ).toContain('Optional lists are not supported. Use either `Type[]` or `Type?`'); + ).toMatchObject(errorLike('Optional lists are not supported. Use either `Type[]` or `Type?`')); }); it('unresolved field type', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Integer } `) - ).toContain(`Could not resolve reference to TypeDeclaration named 'Integer'.`); + ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Integer[] } `) - ).toContain(`Could not resolve reference to TypeDeclaration named 'Integer'.`); + ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Integer? } `) - ).toContain(`Could not resolve reference to TypeDeclaration named 'Integer'.`); + ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); }); - it('id field', async () => { + describe('id field', () => { const err = 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.'; - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int - @@allow('all', x > 0) - } - `) - ).toContain(err); + it('should error when there are no unique fields', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int + @@allow('all', x > 0) + } + `); + expect(result).toMatchObject(errorLike(err)); + }); - // @unique used as id - await loadModel(` - ${prelude} - model M { - id Int @unique - x Int - @@allow('all', x > 0) - } - `); + it('should should use @unique when there is no @id', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + id Int @unique + x Int + @@allow('all', x > 0) + } + `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }); // @@unique used as id - await loadModel(` + it('should suceed when @@unique used as id', async () => { + const result = await safelyLoadModel(` ${prelude} model M { x Int @@ -152,128 +159,176 @@ describe('Data Model Validation Tests', () => { @@allow('all', x > 0) } `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int - @@deny('all', x <= 0) - } - `) - ).toContain(err); + it('should succeed when @id is an enum type', async () => { + const result = await safelyLoadModel(` + ${prelude} + enum E { + A + B + } + model M { + id E @id + } + `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @gt(0) - } - `) - ).toContain(err); + it('should succeed when @@id is an enum type', async () => { + const result = await safelyLoadModel(` + ${prelude} + enum E { + A + B + } + model M { + x Int + y E + @@id([x, y]) + } + `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @id - y Int @id - } - `) - ).toContain(`Model can include at most one field with @id attribute`); + it('should error when there are no id fields, even when denying access', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int + @@deny('all', x <= 0) + } + `); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @id - y Int - @@id([x, y]) - } - `) - ).toContain(`Model cannot have both field-level @id and model-level @@id attributes`); + expect(result).toMatchObject(errorLike(err)); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int? @id - } - `) - ).toContain(`Field with @id attribute must not be optional`); + it('should error when there are not id fields, without access restrictions', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int @gt(0) + } + `); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int? - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must not be optional`); + expect(result).toMatchObject(errorLike(err)); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int[] @id - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when there is more than one field marked as @id', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int @id + y Int @id + } + `); + expect(result).toMatchObject(errorLike(`Model can include at most one field with @id attribute`)); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int[] - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when both @id and @@id are used', async () => { + const result = await safelyLoadModel(` + ${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`) + ); + }); + + it('should error when @id used on optional field', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int? @id + } + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Json @id - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @@id used on optional field', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int? + @@id([x]) + } + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)); + }); - expect( - await loadModelWithError(` - ${prelude} - model M { - x Json - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @id used on list field', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int[] @id + } + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); - expect( - await loadModelWithError(` - ${prelude} - model Id { - id String @id - } - model M { - myId Id @id - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @@id used on list field', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Int[] + @@id([x]) + } + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); - expect( - await loadModelWithError(` - ${prelude} - model Id { - id String @id - } - model M { - myId Id - @@id([myId]) - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @id used on a Json field', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + x Json @id + } + `); + 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} + model M { + x Json + @@id([x]) + } + `); + 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} + 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`)); + }); + + it('should error when @@id used on a reference field', async () => { + const result = await safelyLoadModel(` + ${prelude} + model Id { + id String @id + } + model M { + myId Id + @@id([myId]) + } + `); + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); + }); }); it('relation', async () => { @@ -326,7 +381,7 @@ describe('Data Model Validation Tests', () => { // one-to-one incomplete expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -337,11 +392,13 @@ describe('Data Model Validation Tests', () => { id String @id } `) - ).toContain(`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( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -354,11 +411,11 @@ describe('Data Model Validation Tests', () => { a1 A } `) - ).toContain(`Fields "a", "a1" on model "B" refer to the same relation to model "A"`); + ).toMatchObject(errorLike(`Fields "a", "a1" on model "B" refer to the same relation to model "A"`)); // fields or references missing expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -371,11 +428,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain(`Both "fields" and "references" must be provided`); + ).toMatchObject(errorLike(`Both "fields" and "references" must be provided`)); // one-to-one inconsistent attribute expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -388,11 +445,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain(`"fields" and "references" must be provided only on one side of relation field`); + ).toMatchObject(errorLike(`"fields" and "references" must be provided only on one side of relation field`)); // references mismatch expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { myId Int @id @@ -405,11 +462,11 @@ describe('Data Model Validation Tests', () => { aId String @unique } `) - ).toContain(`values of "references" and "fields" must have the same type`); + ).toMatchObject(errorLike(`values of "references" and "fields" must have the same type`)); // "fields" and "references" typing consistency expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id Int @id @@ -422,11 +479,11 @@ describe('Data Model Validation Tests', () => { aId String @unique } `) - ).toContain(`values of "references" and "fields" must have the same type`); + ).toMatchObject(errorLike(`values of "references" and "fields" must have the same type`)); // one-to-one missing @unique expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -439,13 +496,15 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain( - `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( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -457,13 +516,15 @@ describe('Data Model Validation Tests', () => { a A } `) - ).toContain( - `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( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -476,11 +537,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain(`Relation field needs to be list or optional`); + ).toMatchObject(errorLike(`Relation field needs to be list or optional`)); // unresolved field expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -492,7 +553,7 @@ describe('Data Model Validation Tests', () => { a A @relation(fields: [aId], references: [id]) } `) - ).toContain(`Could not resolve reference to ReferenceTarget named 'aId'.`); + ).toMatchObject(errorLike(`Could not resolve reference to ReferenceTarget named 'aId'.`)); // enum as foreign key await loadModel(` @@ -607,7 +668,7 @@ describe('Data Model Validation Tests', () => { }); it('abstract base type', async () => { - const errors = await loadModelWithError(` + const errors = await safelyLoadModel(` ${prelude} abstract model Base { @@ -623,13 +684,13 @@ describe('Data Model Validation Tests', () => { } `); - expect(errors[0]).toEqual( - 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.' + 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( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model User { id Int @id @default(autoincrement()) @@ -649,6 +710,8 @@ describe('Data Model Validation Tests', () => { a String } `) - ).toContain(`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"`) + ); }); }); diff --git a/packages/schema/tests/schema/validation/datasource-validation.test.ts b/packages/schema/tests/schema/validation/datasource-validation.test.ts index 19be1f076..469ba5ac1 100644 --- a/packages/schema/tests/schema/validation/datasource-validation.test.ts +++ b/packages/schema/tests/schema/validation/datasource-validation.test.ts @@ -1,18 +1,21 @@ -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, loadModelWithError, safelyLoadModel } from '../../utils'; describe('Datasource Validation Tests', () => { it('missing fields', async () => { - expect( - await loadModelWithError(` + const result = await safelyLoadModel(` datasource db { } - `) - ).toEqual( - expect.arrayContaining([ - 'datasource must include a "provider" field', - 'datasource must include a "url" field', - ]) - ); + `); + + expect(result).toMatchObject({ + status: 'rejected', + reason: { + cause: [ + { message: 'datasource must include a "provider" field' }, + { message: 'datasource must include a "url" field' }, + ] + } + }) }); it('dup fields', async () => { @@ -41,7 +44,7 @@ describe('Datasource Validation Tests', () => { provider = 'abc' } `) - ).toContainEqual(expect.stringContaining('Provider "abc" is not supported')); + ).toContain('Provider "abc" is not supported'); }); it('invalid url value', async () => { diff --git a/packages/schema/tests/schema/validation/schema-validation.test.ts b/packages/schema/tests/schema/validation/schema-validation.test.ts index 5f1cc6254..ca0efa697 100644 --- a/packages/schema/tests/schema/validation/schema-validation.test.ts +++ b/packages/schema/tests/schema/validation/schema-validation.test.ts @@ -39,6 +39,20 @@ describe('Toplevel Schema Validation Tests', () => { ).toContain('Cannot find model file models/abc.zmodel'); }); + it('not existing import with extension', async () => { + expect( + await loadModelWithError(` + import 'models/abc.zmodel' + datasource db1 { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + model X {id String @id } + `) + ).toContain('Cannot find model file models/abc.zmodel'); + }) + it('multiple auth models', async () => { expect( await loadModelWithError(` diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index 4dcd45170..dc424b93a 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -7,9 +7,21 @@ import { URI } from 'vscode-uri'; import { createZModelServices } from '../src/language-server/zmodel-module'; import { mergeBaseModel } from '../src/utils/ast-utils'; -export class SchemaLoadingError extends Error { - constructor(public readonly errors: string[]) { - super('Schema error:\n' + errors.join('\n')); +type Errorish = Error | { message: string; stack?: string } | string; + +export class SchemaLoadingError extends Error { + cause: Errors + constructor(public readonly errors: Errors) { + const stack = errors.find((e): e is typeof e & { stack: string } => typeof e === 'object' && 'stack' in e)?.stack; + const message = errors.map((e) => (typeof e === 'string' ? e : e.message)).join('\n'); + + super(`Schema error:\n${ message }`); + + if (stack) { + const shiftedStack = stack.split('\n').slice(1).join('\n'); + this.stack = shiftedStack + } + this.cause = errors } } @@ -23,11 +35,11 @@ export async function loadModel(content: string, validate = true, verbose = true const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); if (doc.parseResult.lexerErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.lexerErrors.map((e) => e.message)); + throw new SchemaLoadingError(doc.parseResult.lexerErrors); } if (doc.parseResult.parserErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.parserErrors.map((e) => e.message)); + throw new SchemaLoadingError(doc.parseResult.parserErrors); } await shared.workspace.DocumentBuilder.build([stdLib, doc], { @@ -46,7 +58,7 @@ export async function loadModel(content: string, validate = true, verbose = true ); } } - throw new SchemaLoadingError(validationErrors.map((e) => e.message)); + throw new SchemaLoadingError(validationErrors); } const model = (await doc.parseResult.value) as Model; @@ -65,7 +77,19 @@ export async function loadModelWithError(content: string, verbose = false) { if (!(err instanceof SchemaLoadingError)) { throw err; } - return (err as SchemaLoadingError).errors; + return (err as SchemaLoadingError).message; } throw new Error('No error is thrown'); } + +export async function safelyLoadModel(content: string, validate = true, verbose = false) { + const [ result ] = await Promise.allSettled([ loadModel(content, validate, verbose) ]); + + return result +} + +export const errorLike = (msg: string) => ({ + reason: { + message: expect.stringContaining(msg) + }, +}) diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 3472ddca0..9d92c19df 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -11,7 +11,7 @@ "scripts": { "clean": "rimraf dist", "lint": "eslint src --ext ts", - "build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./LICENSE ./README.md dist && copyfiles -u 1 src/package.template.json src/.npmrc.template dist && pnpm pack dist --pack-destination '../../../.build'", + "build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./LICENSE ./README.md dist && pnpm pack dist --pack-destination '../../../.build'", "watch": "tsc --watch", "prepublishOnly": "pnpm build" }, diff --git a/packages/testtools/src/.npmrc.template b/packages/testtools/src/.npmrc.template deleted file mode 100644 index 14f2c2865..000000000 --- a/packages/testtools/src/.npmrc.template +++ /dev/null @@ -1 +0,0 @@ -cache=/.npmcache diff --git a/packages/testtools/src/package.template.json b/packages/testtools/src/package.template.json deleted file mode 100644 index cd443d32d..000000000 --- a/packages/testtools/src/package.template.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "test-run", - "version": "1.0.0", - "description": "", - "main": "index.js", - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@prisma/client": "^5.7.1", - "@zenstackhq/runtime": "file:/packages/runtime/dist", - "@zenstackhq/swr": "file:/packages/plugins/swr/dist", - "@zenstackhq/trpc": "file:/packages/plugins/trpc/dist", - "@zenstackhq/openapi": "file:/packages/plugins/openapi/dist", - "prisma": "^5.7.1", - "typescript": "^4.9.3", - "zenstack": "file:/packages/schema/dist", - "zod": "^3.22.4", - "decimal.js": "^10.4.2" - } -} diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index bd64d6461..0dd45140b 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -35,14 +35,23 @@ export type FullDbClientContract = CrudContract & { }; export function run(cmd: string, env?: Record, cwd?: string) { - // const start = Date.now(); - execSync(cmd, { - stdio: 'pipe', - encoding: 'utf-8', - env: { ...process.env, DO_NOT_TRACK: '1', ...env }, - cwd, - }); - // console.log('Execution took', Date.now() - start, 'ms', '-', cmd); + try { + const start = Date.now(); + execSync(cmd, { + stdio: 'pipe', + encoding: 'utf-8', + env: { ...process.env, DO_NOT_TRACK: '1', ...env }, + cwd, + }); + console.log('Execution took', Date.now() - start, 'ms', '-', cmd); + } catch (err) { + console.error('Command failed:', cmd, err); + throw err; + } +} + +export function installPackage(pkg: string, dev = false) { + run(`npm install ${dev ? '-D' : ''} --no-audit --no-fund ${pkg}`); } function normalizePath(p: string) { @@ -90,7 +99,7 @@ plugin enhancer { plugin zod { provider = '@core/zod' - preserveTsFiles = true + // preserveTsFiles = true modelOnly = ${!options.fullZod} } `; @@ -134,21 +143,29 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { const { name: projectRoot } = tmp.dirSync({ unsafeCleanup: true }); - const root = getWorkspaceRoot(__dirname); + const workspaceRoot = getWorkspaceRoot(__dirname); - if (!root) { + if (!workspaceRoot) { throw new Error('Could not find workspace root'); } - const pkgContent = fs.readFileSync(path.join(__dirname, 'package.template.json'), { encoding: 'utf-8' }); - fs.writeFileSync(path.join(projectRoot, 'package.json'), pkgContent.replaceAll('', root)); - - const npmrcContent = fs.readFileSync(path.join(__dirname, '.npmrc.template'), { encoding: 'utf-8' }); - fs.writeFileSync(path.join(projectRoot, '.npmrc'), npmrcContent.replaceAll('', root)); - console.log('Workdir:', projectRoot); process.chdir(projectRoot); + // copy project structure from scaffold (prepared by test-setup.ts) + fs.cpSync(path.join(workspaceRoot, '.test/scaffold'), projectRoot, { recursive: true, force: true }); + + // install local deps + const localInstallDeps = [ + 'packages/schema/dist', + 'packages/runtime/dist', + 'packages/plugins/swr/dist', + 'packages/plugins/trpc/dist', + 'packages/plugins/openapi/dist', + ]; + + run(`npm i --no-audit --no-fund ${localInstallDeps.map((d) => path.join(workspaceRoot, d)).join(' ')}`); + let zmodelPath = path.join(projectRoot, 'schema.zmodel'); const files = schema.split(FILE_SPLITTER); @@ -185,16 +202,16 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { } } - run('npm install'); - const outputArg = opt.output ? ` --output ${opt.output}` : ''; if (opt.customSchemaFilePath) { - run(`npx zenstack generate --schema ${zmodelPath} --no-dependency-check${outputArg}`, { + run(`npx zenstack generate --no-version-check --schema ${zmodelPath} --no-dependency-check${outputArg}`, { NODE_PATH: './node_modules', }); } else { - run(`npx zenstack generate --no-dependency-check${outputArg}`, { NODE_PATH: './node_modules' }); + run(`npx zenstack generate --no-version-check --no-dependency-check${outputArg}`, { + NODE_PATH: './node_modules', + }); } if (opt.pushDb) { @@ -205,10 +222,10 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { opt.extraDependencies?.push('@prisma/extension-pulse'); } - opt.extraDependencies?.forEach((dep) => { - console.log(`Installing dependency ${dep}`); - run(`npm install ${dep}`); - }); + if (opt.extraDependencies) { + console.log(`Installing dependency ${opt.extraDependencies.join(' ')}`); + installPackage(opt.extraDependencies.join(' ')); + } opt.copyDependencies?.forEach((dep) => { const pkgJson = JSON.parse(fs.readFileSync(path.join(dep, 'package.json'), { encoding: 'utf-8' })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 602861b14..d78c2eea2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: tsup: specifier: ^8.0.1 version: 8.0.1(ts-node@10.9.1)(typescript@5.3.2) + tsx: + specifier: ^4.7.1 + version: 4.7.1 typescript: specifier: ^5.3.2 version: 5.3.2 @@ -1603,6 +1606,15 @@ packages: tslib: 2.6.0 dev: true + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.17.19: resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} engines: {node: '>=12'} @@ -1621,6 +1633,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.19.4: resolution: {integrity: sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==} engines: {node: '>=12'} @@ -1657,6 +1678,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.19.4: resolution: {integrity: sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ==} engines: {node: '>=12'} @@ -1684,6 +1714,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.19.4: resolution: {integrity: sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g==} engines: {node: '>=12'} @@ -1711,6 +1750,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.19.4: resolution: {integrity: sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==} engines: {node: '>=12'} @@ -1738,6 +1786,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.19.4: resolution: {integrity: sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw==} engines: {node: '>=12'} @@ -1765,6 +1822,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.19.4: resolution: {integrity: sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ==} engines: {node: '>=12'} @@ -1792,6 +1858,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.19.4: resolution: {integrity: sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw==} engines: {node: '>=12'} @@ -1819,6 +1894,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.19.4: resolution: {integrity: sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA==} engines: {node: '>=12'} @@ -1846,6 +1930,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.19.4: resolution: {integrity: sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg==} engines: {node: '>=12'} @@ -1873,6 +1966,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.19.4: resolution: {integrity: sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ==} engines: {node: '>=12'} @@ -1909,6 +2011,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.19.4: resolution: {integrity: sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg==} engines: {node: '>=12'} @@ -1936,6 +2047,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.19.4: resolution: {integrity: sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw==} engines: {node: '>=12'} @@ -1963,6 +2083,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.19.4: resolution: {integrity: sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw==} engines: {node: '>=12'} @@ -1990,6 +2119,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.19.4: resolution: {integrity: sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig==} engines: {node: '>=12'} @@ -2017,6 +2155,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.19.4: resolution: {integrity: sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg==} engines: {node: '>=12'} @@ -2044,6 +2191,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.19.4: resolution: {integrity: sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg==} engines: {node: '>=12'} @@ -2071,6 +2227,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.19.4: resolution: {integrity: sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A==} engines: {node: '>=12'} @@ -2098,6 +2263,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.19.4: resolution: {integrity: sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw==} engines: {node: '>=12'} @@ -2125,6 +2299,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.19.4: resolution: {integrity: sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw==} engines: {node: '>=12'} @@ -2152,6 +2335,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.19.4: resolution: {integrity: sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w==} engines: {node: '>=12'} @@ -2179,6 +2371,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.19.4: resolution: {integrity: sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg==} engines: {node: '>=12'} @@ -2206,6 +2407,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.19.4: resolution: {integrity: sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA==} engines: {node: '>=12'} @@ -7808,6 +8018,37 @@ packages: '@esbuild/win32-x64': 0.18.14 dev: true + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: true + /esbuild@0.19.4: resolution: {integrity: sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA==} engines: {node: '>=12'} @@ -8635,6 +8876,12 @@ packages: get-intrinsic: 1.2.1 dev: true + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /giget@1.1.3: resolution: {integrity: sha512-zHuCeqtfgqgDwvXlR84UNgnJDuUHQcNI5OqWqFxxuk2BshuKbYhJWdxBsEo4PvKqoGh23lUAIvBNpChMLv7/9Q==} hasBin: true @@ -12996,6 +13243,10 @@ packages: engines: {node: '>=8'} dev: true + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} @@ -14293,6 +14544,17 @@ packages: typescript: 5.3.2 dev: true + /tsx@4.7.1: + resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /tty-table@4.2.1: resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==} engines: {node: '>=8.0.0'} diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts new file mode 100644 index 000000000..ddf3c999a --- /dev/null +++ b/script/test-scaffold.ts @@ -0,0 +1,24 @@ +import path from 'path'; +import fs from 'fs'; +import { execSync } from 'child_process'; + +const scaffoldPath = path.join(__dirname, '../.test/scaffold'); +if (fs.existsSync(scaffoldPath)) { + fs.rmSync(scaffoldPath, { recursive: true, force: true }); +} +fs.mkdirSync(scaffoldPath, { recursive: true }); + +function run(cmd: string) { + console.log(`Running: ${cmd}, in ${scaffoldPath}`); + try { + execSync(cmd, { cwd: scaffoldPath, stdio: 'ignore' }); + } catch (err) { + console.error(`Test project scaffolding cmd error: ${err}`); + throw err; + } +} + +run('npm init -y'); +run('npm i --no-audit --no-fund typescript prisma @prisma/client zod decimal.js'); + +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/global-setup.js b/tests/integration/global-setup.js deleted file mode 100644 index 0d4b8e23e..000000000 --- a/tests/integration/global-setup.js +++ /dev/null @@ -1,10 +0,0 @@ -const { execSync } = require('child_process'); - -module.exports = function () { - console.log('npm install'); - execSync('npm install', { - encoding: 'utf-8', - stdio: 'inherit', - cwd: 'test-run', - }); -}; diff --git a/tests/integration/jest.config.ts b/tests/integration/jest.config.ts index 346f6faad..67a118269 100644 --- a/tests/integration/jest.config.ts +++ b/tests/integration/jest.config.ts @@ -1,30 +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 { - // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true, - - // A map from regular expressions to paths to transformers - transform: { '^.+\\.tsx?$': 'ts-jest' }, - - testTimeout: 300000, - + ...baseConfig, setupFilesAfterEnv: ['./test-setup.ts'], - - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // The directory where Jest should output its coverage files - coverageDirectory: 'tests/coverage', - - // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', - - // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ['json', 'text', 'lcov', 'clover'], }; diff --git a/tests/integration/package.json b/tests/integration/package.json index cace90307..40627f354 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint . --ext .ts", - "test": "ZENSTACK_TEST=1 jest --runInBand" + "test": "ZENSTACK_TEST=1 jest" }, "keywords": [], "author": "", diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 544ae501a..21ecd9300 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /// +import { installPackage } from '@zenstackhq/testtools'; import * as fs from 'fs'; import path from 'path'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; -import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; import { createNpmrc } from './share'; describe('CLI generate command tests', () => { @@ -43,8 +43,8 @@ model Post { // set up project fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); createNpmrc(); - execSync('npm install prisma @prisma/client zod'); - execSync(`npm install ${path.join(__dirname, '../../../../packages/runtime/dist')}`); + installPackage('prisma @prisma/client zod'); + installPackage(path.join(__dirname, '../../../../packages/runtime/dist')); // set up schema fs.writeFileSync('schema.zmodel', MODEL, 'utf-8'); diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 19dfb4dce..36d6dd1a9 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /// -import { getWorkspaceNpmCacheFolder, run } from '@zenstackhq/testtools'; +import { getWorkspaceNpmCacheFolder, installPackage, run } from '@zenstackhq/testtools'; import * as fs from 'fs'; import * as path from 'path'; import * as tmp from 'tmp'; @@ -94,8 +94,8 @@ describe('CLI Plugins Tests', () => { switch (pm) { case 'npm': - run('npm install ' + deps); - run('npm install -D ' + devDeps); + installPackage(deps); + installPackage(devDeps, true); break; // case 'yarn': // run('yarn add ' + deps); diff --git a/tests/integration/tests/schema/todo.zmodel b/tests/integration/tests/schema/todo.zmodel index c3a84707e..524f3152c 100644 --- a/tests/integration/tests/schema/todo.zmodel +++ b/tests/integration/tests/schema/todo.zmodel @@ -13,7 +13,7 @@ generator js { plugin zod { provider = '@core/zod' - preserveTsFiles = true + // preserveTsFiles = true } /* From 3aa1f220f13c7760f5552e9d045c38ca4f725fd7 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:45:03 +0000 Subject: [PATCH 012/127] chore(dependabot.yml): add Dependabot configuration file to manage daily updates for npm packages in various directories --- .github/dependabot.yml | 156 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f3699f479 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,156 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: dependabot.yml +# Path: \.github\dependabot.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + +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: "/" \ No newline at end of file From 24b6c26720d5a0f9cd6d64431288473cd9ee5a97 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:45:20 +0000 Subject: [PATCH 013/127] feat(release): add release manifest and main config files for version 2.0.0-alpha.1 feat(release): define package names and components in release main config file feat(release): set up configuration for automated versioning and release process --- .github/release/.release-manifest.json | 14 ++++++ .github/release/release-main-config.json | 60 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .github/release/.release-manifest.json create mode 100644 .github/release/release-main-config.json diff --git a/.github/release/.release-manifest.json b/.github/release/.release-manifest.json new file mode 100644 index 000000000..5b18bf702 --- /dev/null +++ b/.github/release/.release-manifest.json @@ -0,0 +1,14 @@ +{ + ".": "2.0.0-alpha.1", + "packages/ide/jetbrains": "2.0.0-alpha.1", + "packages/language": "2.0.0-alpha.1", + "packages/misc/redwood": "2.0.0-alpha.1", + "packages/plugins/openapi": "2.0.0-alpha.1", + "packages/plugins/swr": "2.0.0-alpha.1", + "packages/plugins/tanstack-query": "2.0.0-alpha.1", + "packages/plugins/trpc": "2.0.0-alpha.1", + "packages/runtime": "2.0.0-alpha.1", + "packages/sdk": "2.0.0-alpha.1", + "packages/server": "2.0.0-alpha.1", + "packages/testtools": "2.0.0-alpha.1" +} \ 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..06bb2df22 --- /dev/null +++ b/.github/release/release-main-config.json @@ -0,0 +1,60 @@ +{ + "packages": { + ".": { + "package-name": "zenstack-monorepo", + "component": "Monorepo" + }, + "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" + } + }, + "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please), and approved by the ZenStack Team.", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "sequential-calls": true, + "separate-pull-requests": true, + "versioning": "default", + "release-type": "node", + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} \ No newline at end of file From 5fc45726103c9ee89313c336571856ee2f08d6a6 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:46:06 +0000 Subject: [PATCH 014/127] feat(codeql.yml): add CodeQL workflow for security analysis on push, pull request, and schedule events targeting main, develop, and release/* branches. Set permissions for job execution and analysis. Include steps to harden runner, checkout repository, initialize CodeQL, autobuild, and perform CodeQL analysis for javascript and typescript languages. --- .github/workflows/codeql.yml | 112 +++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..028a774f2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,112 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: codeql.yml +# Path: \.github\workflows\codeql.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + +name: Security - CodeQL + +on: + merge_group: + push: + branches: + - main + - develop + - release/* + pull_request: + branches: + - main + - develop + - release/* + 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@eb238b55efaa70779f274895e782ed17c84f2895 # 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@1500a131381b66de0c52ac28abb13cd79f4b7ecc # 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@1500a131381b66de0c52ac28abb13cd79f4b7ecc # 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@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 + with: + category: "/language:${{matrix.language}}" From 520b4698d8bcb3615a837b0a3efb85ff9f363696 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:46:49 +0000 Subject: [PATCH 015/127] feat(workflows): add management-changelog.yml file for release workflow feat(workflows): configure release workflow to trigger on push to main, dev, and release branches feat(workflows): set permissions for contents to read and write in release job feat(workflows): add steps to harden runner for runtime security in release job feat(workflows): add release-please-action for automated releases in release job feat(workflows): add actions/checkout and actions/setup-node for release job setup feat(workflows): add pnpm installation and publishing steps in release job --- .github/workflows/management-changelog.yml | 103 +++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/management-changelog.yml diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml new file mode 100644 index 000000000..c150977d9 --- /dev/null +++ b/.github/workflows/management-changelog.yml @@ -0,0 +1,103 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: management-changelog.yml +# Path: \.github\workflows\management-changelog.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + + + +on: + push: + branches: + - main # Your main branch + - dev # Your development branch + - release/* # Your releases branch + # - v2 # Your current v2 branch - disabled for now because I don't know if you want to have this tag on this branch or not -_- + # TODO: Rename your V2 brach to release/v2 for proper versioning if you intend to use a Release branch method + +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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit + + # This tells you useful infomation about the workflow, but it's not required (hence commented out) - it's just nice to have + # - name: Workflow Telemetry + # uses: catchpoint/workflow-telemetry-action@6705383eabd01833acfe8412ec697384830e1455 # v1.8.7 + # with: + # comment_on_pr: false + # theme: dark + # proc_trace_sys_enable: true + + - uses: google-github-actions/release-please-action@v4 + id: release + with: + config-file: '.github/releases/release-main-config.json' + manifest-file: '.github/releases/.release-manifest.json' + target-branch: ${{ github.ref_name == 'develop' && 'main' || github.ref_name }} + include-component-in-tag: true + + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.release_created }} + + - uses: actions/setup-node@v4 + if: ${{ steps.release.outputs.release_created }} + with: + node-version: 12 + registry-url: 'https://registry.npmjs.org' + + - run: pnpm i # Install using pnpm + if: ${{ steps.release.outputs.release_created }} + + - run: pnpm publish-all # Publish using pre-defined pnpm script + if: ${{ steps.release.outputs.release_created }} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + \ No newline at end of file From 545f6688a5e85171255dfc75148a0b39ef450cb2 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:47:22 +0000 Subject: [PATCH 016/127] feat(security-defender-for-devops.yml): add GitHub Actions workflow for Microsoft Defender For DevOps security checks --- .../security-defender-for-devops.yml | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/security-defender-for-devops.yml diff --git a/.github/workflows/security-defender-for-devops.yml b/.github/workflows/security-defender-for-devops.yml new file mode 100644 index 000000000..96b702ce0 --- /dev/null +++ b/.github/workflows/security-defender-for-devops.yml @@ -0,0 +1,107 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: security-defender-for-devops.yml +# Path: \.github\workflows\security-defender-for-devops.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + +# 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. +# +# 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 + - develop + - release/* + pull_request: + branches: + - main + - develop + - release/* + 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit + + # checks out the repository + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + with: + dotnet-version: | + 5.0.x + 6.0.x + + - name: Run Microsoft Security DevOps + uses: microsoft/security-devops-action@e94440350ed10e2806d47cd0d7504a2c51abdbe9 # v1.6.0 + id: msdo + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 + with: + sarif_file: ${{ steps.msdo.outputs.sarifFile }} From 2b43adc9fcfa5e7dd2e915c2ea9cc8efe6d7ba2b Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:47:34 +0000 Subject: [PATCH 017/127] feat(security-dependency-review.yml): add security dependency review workflow to scan and block PRs with known-vulnerable packages --- .../workflows/security-dependency-review.yml | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/security-dependency-review.yml diff --git a/.github/workflows/security-dependency-review.yml b/.github/workflows/security-dependency-review.yml new file mode 100644 index 000000000..060baa17e --- /dev/null +++ b/.github/workflows/security-dependency-review.yml @@ -0,0 +1,71 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: security-dependency-review.yml +# Path: \.github\workflows\security-dependency-review.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + +# 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@eb238b55efaa70779f274895e782ed17c84f2895 # 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@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2.5.1 From 2d8452de270c6bde0f55a386500b8f61fb112847 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:47:49 +0000 Subject: [PATCH 018/127] feat(security-ossar.yml): add GitHub workflow for security scanning using OSSAR feat(security-ossar.yml): integrate open source static analysis tools with GitHub code scanning feat(security-ossar.yml): schedule security scans on main, develop, and release branches --- .github/workflows/security-ossar.yml | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/security-ossar.yml diff --git a/.github/workflows/security-ossar.yml b/.github/workflows/security-ossar.yml new file mode 100644 index 000000000..889d890c5 --- /dev/null +++ b/.github/workflows/security-ossar.yml @@ -0,0 +1,110 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: security-ossar.yml +# Path: \.github\workflows\security-ossar.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + +# 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 + - develop + - release/* + pull_request: + branches: + - main + - develop + - release/* + 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit + + - name: Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@6705383eabd01833acfe8412ec697384830e1455 # 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@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 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@786a16a90ba92b4ae6228fe7382fb16ef5c51000 # v1 + id: ossar + + # Upload results to the Security tab + - name: Upload OSSAR results + uses: github/codeql-action/upload-sarif@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 + with: + sarif_file: ${{ steps.ossar.outputs.sarifFile }} From 30e5a02c7b84d93d23a0e00416b3382b56963c2c Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:48:02 +0000 Subject: [PATCH 019/127] feat(security-scorecard.yml): add GitHub Actions workflow for security scorecard analysis to enhance supply-chain security monitoring and compliance with best practices --- .github/workflows/security-scorecard.yml | 117 +++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/security-scorecard.yml diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml new file mode 100644 index 000000000..09fb11865 --- /dev/null +++ b/.github/workflows/security-scorecard.yml @@ -0,0 +1,117 @@ +### +# @format +# ----- +# Project: zenstack-monorepo +# File: security-scorecard.yml +# Path: \.github\workflows\security-scorecard.yml +# Created Date: Monday, February 19th 2024 +# Author: Jonathan Stevens, jonathan@resnovas.com +# Github: https://github.com/TGTGamer +# ----- +# Contributing: Please read through our contributing guidelines. +# Included are directions for opening issues, coding standards, +# and notes on development. These can be found at +# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md +# ----- +# Code of Conduct: This project abides by the Contributor Covenant, v2.0 +# Please interact in ways that contribute to an open, welcoming, diverse, +# inclusive, and healthy community. Our Code of Conduct can be found at +# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md +# ----- +# Copyright (c) 2024 ZenstackHQ - All Rights Reserved +# LICENSE: MIT License (MIT) +# ----- +# This program has been provided under confidence of the copyright holder and +# is licensed for copying, distribution and modification under the terms +# of the MIT License (MIT) published as the License, +# or (at your option) any later version of this license. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MIT License for more details. +# You should have received a copy of the MIT License +# along with this program. If not, please write to: jonathan@resnovas.com, +# or see https://opensource.org/licenses/MIT +# ----- +# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE +### + + + +# 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 + - develop + +# 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit + + - name: Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@6705383eabd01833acfe8412ec697384830e1455 # 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@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # 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@3cea5372237819ed00197afe530f5a7ea3e805c8 # 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@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + with: + sarif_file: results.sarif From 6335688d2f08f2464945c3be5604fb435133f891 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 02:53:04 +0000 Subject: [PATCH 020/127] chore(workflows): update workflow files to include v2 branch in branch filtering for push and pull_request events to align with project requirements --- .github/workflows/build-test.yml | 18 ++++++++++++++++-- .github/workflows/codeql.yml | 2 ++ .../workflows/security-defender-for-devops.yml | 2 ++ .github/workflows/security-ossar.yml | 2 ++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 8e8ce08f3..7f496b80a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,8 +8,22 @@ env: DO_NOT_TRACK: '1' on: - pull_request: - branches: ['dev', 'main', 'v2'] + merge_group: + push: + branches: + - main + - develop + - release/* + - v2 + pull_request: + branches: + - main + - develop + - release/* + - v2 + +permissions: + contents: read jobs: build-test: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 028a774f2..e9a91101a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,11 +45,13 @@ on: - main - develop - release/* + - v2 pull_request: branches: - main - develop - release/* + - v2 schedule: - cron: "0 0 * * 1" diff --git a/.github/workflows/security-defender-for-devops.yml b/.github/workflows/security-defender-for-devops.yml index 96b702ce0..258ef2a1a 100644 --- a/.github/workflows/security-defender-for-devops.yml +++ b/.github/workflows/security-defender-for-devops.yml @@ -63,11 +63,13 @@ on: - main - develop - release/* + - v2 pull_request: branches: - main - develop - release/* + - v2 schedule: - cron: '34 12 * * 0' diff --git a/.github/workflows/security-ossar.yml b/.github/workflows/security-ossar.yml index 889d890c5..c53a45d04 100644 --- a/.github/workflows/security-ossar.yml +++ b/.github/workflows/security-ossar.yml @@ -53,11 +53,13 @@ on: - main - develop - release/* + - v2 pull_request: branches: - main - develop - release/* + - v2 schedule: - cron: '41 3 * * 5' From b9b784c2ba53ca51abfb5d0ea3b5e543cd7f7c9e Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 03:08:07 +0000 Subject: [PATCH 021/127] fix(workflows): correct paths for config-file and manifest-file in release-please-action configuration --- .github/workflows/management-changelog.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml index c150977d9..ae83efc55 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -78,8 +78,8 @@ jobs: - uses: google-github-actions/release-please-action@v4 id: release with: - config-file: '.github/releases/release-main-config.json' - manifest-file: '.github/releases/.release-manifest.json' + config-file: '.github/release/release-main-config.json' + manifest-file: '.github/release/.release-manifest.json' target-branch: ${{ github.ref_name == 'develop' && 'main' || github.ref_name }} include-component-in-tag: true From e3596ee87b2b37bf2f922ef3672a6bf88bce555b Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 03:19:50 +0000 Subject: [PATCH 022/127] chore(release-main-config.json): update component names to use underscores instead of spaces for consistency and readability. --- .github/release/release-main-config.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index 06bb2df22..4d1ed0cd5 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -6,7 +6,7 @@ }, "packages/ide/jetbrains": { "package-name": "jetbrains", - "component": "JetBrains IDE" + "component": "JetBrains_IDE" }, "packages/language": { "package-name": "@zenstackhq/language", @@ -18,7 +18,7 @@ }, "packages/plugins/openapi": { "package-name": "@zenstackhq/openapi", - "component": "OpenAPI Plugin" + "component": "OpenAPI_Plugin" }, "packages/plugins/swr": { "package-name": "@zenstackhq/swr", @@ -26,11 +26,11 @@ }, "packages/plugins/tanstack-query": { "package-name": "@zenstackhq/tanstack-query", - "component": "Tanstack Query Plugin" + "component": "Tanstack_Query_Plugin" }, "packages/plugins/trpc": { "package-name": "@zenstackhq/trpc", - "component": "tRPC Plugin" + "component": "tRPC_Plugin" }, "packages/runtime": { "package-name": "@zenstackhq/runtime", @@ -46,7 +46,7 @@ }, "packages/testtools": { "package-name": "@zenstackhq/testtools", - "component": "Test Tools" + "component": "Test_Tools" } }, "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please), and approved by the ZenStack Team.", From 440da31c4774bef70d50a711520d6ed8b5351542 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 19 Feb 2024 03:21:11 +0000 Subject: [PATCH 023/127] chore(release-main-config.json): update component value for SWR Plugin to use underscore instead of space for consistency across components --- .github/release/release-main-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index 4d1ed0cd5..731a26788 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -22,7 +22,7 @@ }, "packages/plugins/swr": { "package-name": "@zenstackhq/swr", - "component": "SWR Plugin" + "component": "SWR_Plugin" }, "packages/plugins/tanstack-query": { "package-name": "@zenstackhq/tanstack-query", From 0a83a6e66289b83ac47a796f5122158abd41575c Mon Sep 17 00:00:00 2001 From: Jonathan S Date: Tue, 20 Feb 2024 19:51:29 +0000 Subject: [PATCH 024/127] Apply suggestions from code review --- .github/dependabot.yml | 37 ----------------- .github/workflows/build-test.yml | 4 +- .github/workflows/codeql.yml | 41 +------------------ .github/workflows/management-changelog.yml | 39 +----------------- .../security-defender-for-devops.yml | 41 +------------------ .../workflows/security-dependency-review.yml | 37 ----------------- .github/workflows/security-ossar.yml | 41 +------------------ .github/workflows/security-scorecard.yml | 39 +----------------- 8 files changed, 10 insertions(+), 269 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f3699f479..bba71a575 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: dependabot.yml -# Path: \.github\dependabot.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### version: 2 updates: diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 7f496b80a..54a3cb45d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,13 +12,13 @@ on: push: branches: - main - - develop + - dev - release/* - v2 pull_request: branches: - main - - develop + - dev - release/* - v2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e9a91101a..187f90653 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: codeql.yml -# Path: \.github\workflows\codeql.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### name: Security - CodeQL @@ -43,13 +6,13 @@ on: push: branches: - main - - develop + - dev - release/* - v2 pull_request: branches: - main - - develop + - dev - release/* - v2 schedule: diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml index ae83efc55..12a968f46 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: management-changelog.yml -# Path: \.github\workflows\management-changelog.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### @@ -80,7 +43,7 @@ jobs: with: config-file: '.github/release/release-main-config.json' manifest-file: '.github/release/.release-manifest.json' - target-branch: ${{ github.ref_name == 'develop' && 'main' || github.ref_name }} + target-branch: ${{ github.ref_name == 'dev' && 'main' || github.ref_name }} include-component-in-tag: true - uses: actions/checkout@v4 diff --git a/.github/workflows/security-defender-for-devops.yml b/.github/workflows/security-defender-for-devops.yml index 258ef2a1a..a75c6f4a8 100644 --- a/.github/workflows/security-defender-for-devops.yml +++ b/.github/workflows/security-defender-for-devops.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: security-defender-for-devops.yml -# Path: \.github\workflows\security-defender-for-devops.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by @@ -61,13 +24,13 @@ on: push: branches: - main - - develop + - dev - release/* - v2 pull_request: branches: - main - - develop + - dev - release/* - v2 schedule: diff --git a/.github/workflows/security-dependency-review.yml b/.github/workflows/security-dependency-review.yml index 060baa17e..f76681581 100644 --- a/.github/workflows/security-dependency-review.yml +++ b/.github/workflows/security-dependency-review.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: security-dependency-review.yml -# Path: \.github\workflows\security-dependency-review.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### # Dependency Review Action # diff --git a/.github/workflows/security-ossar.yml b/.github/workflows/security-ossar.yml index c53a45d04..eae2de45f 100644 --- a/.github/workflows/security-ossar.yml +++ b/.github/workflows/security-ossar.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: security-ossar.yml -# Path: \.github\workflows\security-ossar.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by @@ -51,13 +14,13 @@ on: push: branches: - main - - develop + - dev - release/* - v2 pull_request: branches: - main - - develop + - dev - release/* - v2 schedule: diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml index 09fb11865..57875f59d 100644 --- a/.github/workflows/security-scorecard.yml +++ b/.github/workflows/security-scorecard.yml @@ -1,40 +1,3 @@ -### -# @format -# ----- -# Project: zenstack-monorepo -# File: security-scorecard.yml -# Path: \.github\workflows\security-scorecard.yml -# Created Date: Monday, February 19th 2024 -# Author: Jonathan Stevens, jonathan@resnovas.com -# Github: https://github.com/TGTGamer -# ----- -# Contributing: Please read through our contributing guidelines. -# Included are directions for opening issues, coding standards, -# and notes on development. These can be found at -# https://github.com/zenstack-monorepo/blob/develop/CONTRIBUTING.md -# ----- -# Code of Conduct: This project abides by the Contributor Covenant, v2.0 -# Please interact in ways that contribute to an open, welcoming, diverse, -# inclusive, and healthy community. Our Code of Conduct can be found at -# https://github.com/zenstack-monorepo/blob/develop/CODE_OF_CONDUCT.md -# ----- -# Copyright (c) 2024 ZenstackHQ - All Rights Reserved -# LICENSE: MIT License (MIT) -# ----- -# This program has been provided under confidence of the copyright holder and -# is licensed for copying, distribution and modification under the terms -# of the MIT License (MIT) published as the License, -# or (at your option) any later version of this license. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# MIT License for more details. -# You should have received a copy of the MIT License -# along with this program. If not, please write to: jonathan@resnovas.com, -# or see https://opensource.org/licenses/MIT -# ----- -# DELETING THIS NOTICE AUTOMATICALLY VOIDS YOUR LICENSE -### @@ -54,7 +17,7 @@ on: push: branches: - main - - develop + - dev # Declare default permissions as read only. permissions: From d296e5fff66316c5c70fe87699485aaf99b12fe1 Mon Sep 17 00:00:00 2001 From: Jonathan S Date: Tue, 20 Feb 2024 20:49:23 +0000 Subject: [PATCH 025/127] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/security-defender-for-devops.yml | 2 +- .github/workflows/security-dependency-review.yml | 1 + .github/workflows/security-ossar.yml | 2 +- .github/workflows/security-scorecard.yml | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 187f90653..0a2173708 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,7 +49,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 + 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. diff --git a/.github/workflows/security-defender-for-devops.yml b/.github/workflows/security-defender-for-devops.yml index a75c6f4a8..dc32fc584 100644 --- a/.github/workflows/security-defender-for-devops.yml +++ b/.github/workflows/security-defender-for-devops.yml @@ -63,7 +63,7 @@ jobs: 6.0.x - name: Run Microsoft Security DevOps - uses: microsoft/security-devops-action@e94440350ed10e2806d47cd0d7504a2c51abdbe9 # v1.6.0 + uses: microsoft/security-devops-action@v1.6.0 id: msdo - name: Upload results to Security tab diff --git a/.github/workflows/security-dependency-review.yml b/.github/workflows/security-dependency-review.yml index f76681581..17b0f0506 100644 --- a/.github/workflows/security-dependency-review.yml +++ b/.github/workflows/security-dependency-review.yml @@ -31,4 +31,5 @@ jobs: 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 uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2.5.1 diff --git a/.github/workflows/security-ossar.yml b/.github/workflows/security-ossar.yml index eae2de45f..10db124ae 100644 --- a/.github/workflows/security-ossar.yml +++ b/.github/workflows/security-ossar.yml @@ -65,7 +65,7 @@ jobs: # Run open source static analysis tools - name: Run OSSAR - uses: github/ossar-action@786a16a90ba92b4ae6228fe7382fb16ef5c51000 # v1 + uses: github/ossar-action@v1 id: ossar # Upload results to the Security tab diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml index 57875f59d..1222d331c 100644 --- a/.github/workflows/security-scorecard.yml +++ b/.github/workflows/security-scorecard.yml @@ -57,6 +57,7 @@ jobs: 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 uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 with: results_file: results.sarif From 6867e795d7a683da1db601bbf2de2c77d0d05ed3 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:33:47 -0800 Subject: [PATCH 026/127] fix: merge errors in github workflow files and formatting issues --- .github/dependabot.yml | 229 +++++++++--------- .github/workflows/build-test.yml | 28 +-- .../security-defender-for-devops.yml | 88 ++++--- .../workflows/security-dependency-review.yml | 38 ++- .github/workflows/security-ossar.yml | 107 ++++---- .github/workflows/security-scorecard.yml | 126 +++++----- 6 files changed, 301 insertions(+), 315 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bba71a575..59ef81ad3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,119 +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: '/' + + - 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/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: '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: "/" \ No newline at end of file + - package-ecosystem: 'github-actions' + schedule: + interval: 'daily' + time: '02:00' + commit-message: + prefix: ':arrow_up: maint' + include: scope + directory: '/' diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 54a3cb45d..368551623 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,22 +8,22 @@ env: DO_NOT_TRACK: '1' on: - merge_group: - push: - branches: - - main - - dev - - release/* - - v2 - pull_request: - branches: - - main - - dev - - release/* - - v2 + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 permissions: - contents: read + contents: read jobs: build-test: diff --git a/.github/workflows/security-defender-for-devops.yml b/.github/workflows/security-defender-for-devops.yml index dc32fc584..526cebf1e 100644 --- a/.github/workflows/security-defender-for-devops.yml +++ b/.github/workflows/security-defender-for-devops.yml @@ -1,9 +1,3 @@ - -# 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. -# # 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). @@ -20,53 +14,53 @@ 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' + 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 + contents: read + security-events: read jobs: - MSDO: - # currently only windows latest is supported - runs-on: windows-latest - permissions: - security-events: write + MSDO: + # currently only windows latest is supported + runs-on: windows-latest + permissions: + security-events: write - steps: - - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 - with: - egress-policy: audit + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit - # checks out the repository - - uses: actions/checkout@v4 + # checks out the repository + - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 - with: - dotnet-version: | - 5.0.x - 6.0.x + - 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: 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@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 - with: - sarif_file: ${{ steps.msdo.outputs.sarifFile }} + - 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 index 17b0f0506..09018a429 100644 --- a/.github/workflows/security-dependency-review.yml +++ b/.github/workflows/security-dependency-review.yml @@ -1,4 +1,3 @@ - # Dependency Review Action # # This Action will scan dependency manifest files that change as part of a Pull Request, @@ -8,28 +7,27 @@ # # Source repository: https://github.com/actions/dependency-review-action name: Security - Dependency Review -on: - merge_group: - pull_request: +on: + merge_group: + pull_request: permissions: - contents: read + contents: read jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 - with: - egress-policy: audit + 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. + # 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 - uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2.5.1 + - 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 index 10db124ae..244f2b147 100644 --- a/.github/workflows/security-ossar.yml +++ b/.github/workflows/security-ossar.yml @@ -1,4 +1,3 @@ - # 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 @@ -10,66 +9,66 @@ name: Security - OSSAR on: - merge_group: - push: - branches: - - main - - dev - - release/* - - v2 - pull_request: - branches: - - main - - dev - - release/* - - v2 - schedule: - - cron: '41 3 * * 5' + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + schedule: + - cron: '41 3 * * 5' permissions: - contents: read + 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 + 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 - with: - egress-policy: audit + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit - - name: Workflow Telemetry - uses: catchpoint/workflow-telemetry-action@6705383eabd01833acfe8412ec697384830e1455 # 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 + - 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. + # 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@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 - with: - dotnet-version: | - 5.0.x - 6.0.x + - 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 + # 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@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 - with: - sarif_file: ${{ steps.ossar.outputs.sarifFile }} + # 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 index 1222d331c..2e8dd159a 100644 --- a/.github/workflows/security-scorecard.yml +++ b/.github/workflows/security-scorecard.yml @@ -1,81 +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 + # 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 +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 + 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 - with: - egress-policy: audit + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2.6.1 + with: + egress-policy: audit - - name: Workflow Telemetry - uses: catchpoint/workflow-telemetry-action@6705383eabd01833acfe8412ec697384830e1455 # 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 + - 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. + # 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 - uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # 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 + - 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@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 - with: - name: SARIF file - path: results.sarif - retention-days: 5 + # 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@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 - with: - sarif_file: results.sarif + # 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 From 889b7d930166d4310464e5e8b6e5e7da8ddd461a Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 19:39:10 -0800 Subject: [PATCH 027/127] chore: try out release flow (#1023) --- .github/workflows/codeql.yml | 122 ++++++++++----------- .github/workflows/management-changelog.yml | 110 +++++++++---------- package.json | 3 +- 3 files changed, 114 insertions(+), 121 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0a2173708..2452a7ca3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,77 +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" + merge_group: + push: + branches: + - main + - dev + - release/* + - v2 + pull_request: + branches: + - main + - dev + - release/* + - v2 + schedule: + - cron: '0 0 * * 1' permissions: - contents: read - + 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 + 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 - with: - egress-policy: audit + 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 + - 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. + # 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@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 + # 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 + # ℹ️ 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. + # 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 + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2.22.12 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml index 12a968f46..6db4bf970 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -1,66 +1,60 @@ - - - on: - push: - branches: - - main # Your main branch - - dev # Your development branch - - release/* # Your releases branch - # - v2 # Your current v2 branch - disabled for now because I don't know if you want to have this tag on this branch or not -_- - # TODO: Rename your V2 brach to release/v2 for proper versioning if you intend to use a Release branch method + push: + branches: + - main # Your main branch + - dev # Your development branch + - release/* # Your releases branch + - v2 # Temp V2 integration branch permissions: - contents: read + 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@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 - with: - egress-policy: audit - - # This tells you useful infomation about the workflow, but it's not required (hence commented out) - it's just nice to have - # - name: Workflow Telemetry - # uses: catchpoint/workflow-telemetry-action@6705383eabd01833acfe8412ec697384830e1455 # v1.8.7 - # with: - # comment_on_pr: false - # theme: dark - # proc_trace_sys_enable: true - - - 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' - target-branch: ${{ github.ref_name == 'dev' && 'main' || github.ref_name }} - include-component-in-tag: true - - - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.release_created }} - - - uses: actions/setup-node@v4 - if: ${{ steps.release.outputs.release_created }} - with: - node-version: 12 - registry-url: 'https://registry.npmjs.org' - - - run: pnpm i # Install using pnpm - if: ${{ steps.release.outputs.release_created }} - - - run: pnpm publish-all # Publish using pre-defined pnpm script - if: ${{ steps.release.outputs.release_created }} + release: + permissions: + contents: write + pull-requests: write env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - - \ No newline at end of file + 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 + + # This tells you useful infomation about the workflow, but it's not required (hence commented out) - it's just nice to have + # - name: Workflow Telemetry + # uses: catchpoint/workflow-telemetry-action@v1.8.7 + # with: + # comment_on_pr: false + # theme: dark + # proc_trace_sys_enable: true + + - 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' + target-branch: ${{ github.ref_name == 'dev' && 'main' || github.ref_name }} + include-component-in-tag: true + + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.release_created }} + + - uses: actions/setup-node@v4 + if: ${{ steps.release.outputs.release_created }} + with: + node-version: 12 + 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/package.json b/package.json index f372eb616..a4d1c41b7 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "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\"" + "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": "", From 1b7763591ecdb34cea6e1f44f248daaf43f43bab Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 20:11:55 -0800 Subject: [PATCH 028/127] chore: tweak settings of release-please (#1036) --- .github/release/release-main-config.json | 118 ++++++++++----------- .github/workflows/management-changelog.yml | 9 -- 2 files changed, 59 insertions(+), 68 deletions(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index 731a26788..111bdb732 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -1,60 +1,60 @@ { - "packages": { - ".": { - "package-name": "zenstack-monorepo", - "component": "Monorepo" - }, - "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" - } - }, - "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please), and approved by the ZenStack Team.", - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "sequential-calls": true, - "separate-pull-requests": true, - "versioning": "default", - "release-type": "node", - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" -} \ No newline at end of file + "packages": { + ".": { + "package-name": "zenstack-monorepo", + "component": "Monorepo" + }, + "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" + } + }, + "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please).", + "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/management-changelog.yml b/.github/workflows/management-changelog.yml index 6db4bf970..5ced9f9d2 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -26,21 +26,12 @@ jobs: with: egress-policy: audit - # This tells you useful infomation about the workflow, but it's not required (hence commented out) - it's just nice to have - # - name: Workflow Telemetry - # uses: catchpoint/workflow-telemetry-action@v1.8.7 - # with: - # comment_on_pr: false - # theme: dark - # proc_trace_sys_enable: true - - 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' target-branch: ${{ github.ref_name == 'dev' && 'main' || github.ref_name }} - include-component-in-tag: true - uses: actions/checkout@v4 if: ${{ steps.release.outputs.release_created }} From 18645dfe5d5711be6795644885310e8379fcdddf Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 21:32:03 -0800 Subject: [PATCH 029/127] chore: try trimming initial changelog (#1038) --- .github/release/release-main-config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index 111bdb732..a57c0005e 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -49,6 +49,7 @@ "component": "Test_Tools" } }, + "last-release-sha": "7b453f7745cad73fc81e7884faf473aecda99556", "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please).", "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, From f40d7e3718d4210137a2e131d28b5491d065b914 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:44:24 -0800 Subject: [PATCH 030/127] chore: release 2.0.0-alpha.2 Release-As: 2.0.0-alpha.2 From 32380ff8ef3999d7b0ba39c9fb7ca3acaa9c8a3c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:08:06 -0800 Subject: [PATCH 031/127] chore: release 2.0.0-alpha.2 Release-As: 2.0.0-alpha.2 --- .github/release/release-main-config.json | 37 ++++++++-------------- .github/workflows/management-changelog.yml | 1 + 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index a57c0005e..55eef6c0d 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -1,55 +1,44 @@ { "packages": { ".": { - "package-name": "zenstack-monorepo", - "component": "Monorepo" + "package-name": "ZenStack" }, "packages/ide/jetbrains": { - "package-name": "jetbrains", - "component": "JetBrains_IDE" + "package-name": "jetbrains" }, "packages/language": { - "package-name": "@zenstackhq/language", - "component": "Language" + "package-name": "@zenstackhq/language" }, "packages/misc/redwood": { - "package-name": "@zenstackhq/redwood", - "component": "Redwood" + "package-name": "@zenstackhq/redwood" }, "packages/plugins/openapi": { - "package-name": "@zenstackhq/openapi", - "component": "OpenAPI_Plugin" + "package-name": "@zenstackhq/openapi" }, "packages/plugins/swr": { - "package-name": "@zenstackhq/swr", - "component": "SWR_Plugin" + "package-name": "@zenstackhq/swr" }, "packages/plugins/tanstack-query": { - "package-name": "@zenstackhq/tanstack-query", - "component": "Tanstack_Query_Plugin" + "package-name": "@zenstackhq/tanstack-query" }, "packages/plugins/trpc": { - "package-name": "@zenstackhq/trpc", - "component": "tRPC_Plugin" + "package-name": "@zenstackhq/trpc" }, "packages/runtime": { - "package-name": "@zenstackhq/runtime", - "component": "Runtime" + "package-name": "@zenstackhq/runtime" }, "packages/sdk": { - "package-name": "@zenstackhq/sdk", - "component": "SDK" + "package-name": "@zenstackhq/sdk" }, "packages/server": { - "package-name": "@zenstackhq/server", - "component": "Server" + "package-name": "@zenstackhq/server" }, "packages/testtools": { - "package-name": "@zenstackhq/testtools", - "component": "Test_Tools" + "package-name": "@zenstackhq/testtools" } }, "last-release-sha": "7b453f7745cad73fc81e7884faf473aecda99556", + "include-component-in-tag": false, "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please).", "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml index 5ced9f9d2..3a6579585 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -31,6 +31,7 @@ jobs: 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 From 175bbc10be0ad64a1ef4f1a6dfb04ccc0844159c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:12:55 -0800 Subject: [PATCH 032/127] chore: release 2.0.0-alpha.2 Release-As: 2.0.0-alpha.2 --- .github/release/release-main-config.json | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index 55eef6c0d..c5bda8b03 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -1,40 +1,52 @@ { "packages": { ".": { - "package-name": "ZenStack" + "package-name": "zenstack-monorepo", + "component": "Monorepo" }, "packages/ide/jetbrains": { - "package-name": "jetbrains" + "package-name": "jetbrains", + "component": "JetBrains_IDE" }, "packages/language": { - "package-name": "@zenstackhq/language" + "package-name": "@zenstackhq/language", + "component": "Language" }, "packages/misc/redwood": { - "package-name": "@zenstackhq/redwood" + "package-name": "@zenstackhq/redwood", + "component": "Redwood" }, "packages/plugins/openapi": { - "package-name": "@zenstackhq/openapi" + "package-name": "@zenstackhq/openapi", + "component": "OpenAPI_Plugin" }, "packages/plugins/swr": { - "package-name": "@zenstackhq/swr" + "package-name": "@zenstackhq/swr", + "component": "SWR_Plugin" }, "packages/plugins/tanstack-query": { - "package-name": "@zenstackhq/tanstack-query" + "package-name": "@zenstackhq/tanstack-query", + "component": "Tanstack_Query_Plugin" }, "packages/plugins/trpc": { - "package-name": "@zenstackhq/trpc" + "package-name": "@zenstackhq/trpc", + "component": "tRPC_Plugin" }, "packages/runtime": { - "package-name": "@zenstackhq/runtime" + "package-name": "@zenstackhq/runtime", + "component": "Runtime" }, "packages/sdk": { - "package-name": "@zenstackhq/sdk" + "package-name": "@zenstackhq/sdk", + "component": "SDK" }, "packages/server": { - "package-name": "@zenstackhq/server" + "package-name": "@zenstackhq/server", + "component": "Server" }, "packages/testtools": { - "package-name": "@zenstackhq/testtools" + "package-name": "@zenstackhq/testtools", + "component": "Test_Tools" } }, "last-release-sha": "7b453f7745cad73fc81e7884faf473aecda99556", From 432752b1d33de38ebba9fad09f113f854e375178 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:35:16 -0800 Subject: [PATCH 033/127] chore: release 2.0.0-alpha.2 Release-As: 2.0.0-alpha.2 --- .github/release/release-main-config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index c5bda8b03..d1373339c 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -2,7 +2,8 @@ "packages": { ".": { "package-name": "zenstack-monorepo", - "component": "Monorepo" + "component": "Monorepo", + "exclude-paths": ["tests", ".github"] }, "packages/ide/jetbrains": { "package-name": "jetbrains", From c2f84f962f4005af398755ce1d359ca3e80a740d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:38:27 -0800 Subject: [PATCH 034/127] chore: release v2.0.0-alpha.2 (#1043) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/release/.release-manifest.json | 22 +++++++------- packages/ide/jetbrains/CHANGELOG.md | 7 +++++ packages/ide/jetbrains/package.json | 30 ++++++++++---------- packages/language/CHANGELOG.md | 8 ++++++ packages/language/package.json | 4 +-- packages/misc/redwood/CHANGELOG.md | 8 ++++++ packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/CHANGELOG.md | 8 ++++++ packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/CHANGELOG.md | 8 ++++++ packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/CHANGELOG.md | 8 ++++++ packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/CHANGELOG.md | 8 ++++++ packages/plugins/trpc/package.json | 6 ++-- packages/runtime/CHANGELOG.md | 8 ++++++ packages/runtime/package.json | 2 +- packages/sdk/CHANGELOG.md | 8 ++++++ packages/sdk/package.json | 2 +- packages/server/CHANGELOG.md | 8 ++++++ packages/server/package.json | 2 +- packages/testtools/CHANGELOG.md | 8 ++++++ packages/testtools/package.json | 2 +- 23 files changed, 127 insertions(+), 38 deletions(-) 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 create mode 100644 packages/runtime/CHANGELOG.md create mode 100644 packages/sdk/CHANGELOG.md create mode 100644 packages/server/CHANGELOG.md create mode 100644 packages/testtools/CHANGELOG.md diff --git a/.github/release/.release-manifest.json b/.github/release/.release-manifest.json index 5b18bf702..1857a084d 100644 --- a/.github/release/.release-manifest.json +++ b/.github/release/.release-manifest.json @@ -1,14 +1,14 @@ { ".": "2.0.0-alpha.1", - "packages/ide/jetbrains": "2.0.0-alpha.1", - "packages/language": "2.0.0-alpha.1", - "packages/misc/redwood": "2.0.0-alpha.1", - "packages/plugins/openapi": "2.0.0-alpha.1", - "packages/plugins/swr": "2.0.0-alpha.1", - "packages/plugins/tanstack-query": "2.0.0-alpha.1", - "packages/plugins/trpc": "2.0.0-alpha.1", - "packages/runtime": "2.0.0-alpha.1", - "packages/sdk": "2.0.0-alpha.1", - "packages/server": "2.0.0-alpha.1", - "packages/testtools": "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/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index 1fa15f2eb..6f0f0e7ff 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -1,5 +1,12 @@ # 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)) + ## [Unreleased] ### 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/package.json b/packages/ide/jetbrains/package.json index 4e7fc26df..47dfec1b8 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,17 +1,17 @@ { - "name": "jetbrains", - "version": "2.0.0-alpha.1", - "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-alpha.2", + "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 a222a6d95..fb5e62fec 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", @@ -23,7 +23,7 @@ "plist2": "^1.1.3" }, "dependencies": { - "langium": "1.3.1" + "langium": "1.3.1" }, "contributes": { "languages": [ 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 a1195e7d8..cb75056e7 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": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "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 2faa34d87..4b97e6824 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": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { 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 bedc7fd13..1399a71c9 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": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { 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 5e625ca17..e4350e61c 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": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { 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 2f24da31f..27d7b6ca0 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": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { @@ -20,7 +20,9 @@ "directory": "dist", "linkDirectory": true }, - "keywords": ["trpc"], + "keywords": [ + "trpc" + ], "author": "ZenStack Team", "license": "MIT", "dependencies": { 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 1d5c8fd37..8292bcb7c 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", 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 8d81caac9..671f87155 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { 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/package.json b/packages/server/package.json index f05366228..2744a1c15 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", 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 9d92c19df..9b9379a8d 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 300175998c50d45224c1993de1bcaf7a54f1b36c Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 22:53:23 -0800 Subject: [PATCH 035/127] chore: remove temp hard-coded sha from release-please config (#1044) --- .github/release/release-main-config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index d1373339c..0cff78ac9 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -50,7 +50,6 @@ "component": "Test_Tools" } }, - "last-release-sha": "7b453f7745cad73fc81e7884faf473aecda99556", "include-component-in-tag": false, "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please).", "bump-minor-pre-major": true, From 06342d2ad872e10be3ee6573d9fb0ced963461a4 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 23:01:25 -0800 Subject: [PATCH 036/127] chore: fix missing pnpm install in release workflow (#1045) --- .github/workflows/management-changelog.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml index 3a6579585..d7460a53c 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -37,10 +37,15 @@ jobs: - 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: 12 + node-version: 20.x registry-url: 'https://registry.npmjs.org' - run: pnpm i # Install using pnpm From 6d043a546b3476a89722bcac6dfe866e2da7eb82 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 21 Feb 2024 11:48:10 -0800 Subject: [PATCH 037/127] chore: fix missing version bumps (#1047) --- .github/release/release-main-config.json | 1 + package.json | 2 +- packages/schema/package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json index 0cff78ac9..57e614a77 100644 --- a/.github/release/release-main-config.json +++ b/.github/release/release-main-config.json @@ -52,6 +52,7 @@ }, "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, diff --git a/package.json b/package.json index a4d1c41b7..55b925bd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/schema/package.json b/packages/schema/package.json index 09693e0e7..5cbb1b316 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": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "author": { "name": "ZenStack Team" }, From e5b5a0fee34e46bee5557229f6f5894629c6ad96 Mon Sep 17 00:00:00 2001 From: Augustin <43639468+Azzerty23@users.noreply.github.com> Date: Thu, 22 Feb 2024 04:08:48 +0100 Subject: [PATCH 038/127] fix: default auth without user context (#1015) --- .../runtime/src/enhancements/default-auth.ts | 11 ++++--- .../enhancements/with-policy/auth.test.ts | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts index cce9af782..bbbd35861 100644 --- a/packages/runtime/src/enhancements/default-auth.ts +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -15,7 +15,7 @@ import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './prox export function withDefaultAuth( prisma: DbClient, options: InternalEnhancementOptions, - context?: EnhancementContext + context: EnhancementContext = {} ): DbClient { return makeProxy( prisma, @@ -32,14 +32,10 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { prisma: DbClientContract, model: string, options: InternalEnhancementOptions, - private readonly context?: EnhancementContext + private readonly context: EnhancementContext ) { super(prisma, model, options); - if (!this.context?.user) { - throw new Error(`Using \`auth()\` in \`@default\` requires a user context`); - } - this.userContext = this.context.user; } @@ -95,6 +91,9 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { } 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/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index f5b4e2f4f..e1fff4f73 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -505,4 +505,33 @@ describe('With Policy: auth() test', () => { ]) ); }); + + 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(); + }); }); From 6edfd662eec0590ac7750761707540c1b302308a Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 24 Feb 2024 16:16:33 -0800 Subject: [PATCH 039/127] chore: merge from dev (#1054) --- jest.config.ts | 4 +- package.json | 4 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- .../src/enhancements/policy/handler.ts | 65 +++++++-- .../src/enhancements/policy/policy-utils.ts | 21 ++- packages/schema/package.json | 2 +- packages/schema/src/cli/cli-util.ts | 4 +- .../validator/datamodel-validator.ts | 19 ++- .../src/plugins/prisma/schema-generator.ts | 4 +- packages/schema/src/utils/pkg-utils.ts | 53 +++++-- .../validation/datamodel-validation.test.ts | 4 +- packages/server/package.json | 2 +- script/set-test-env.ts | 1 + script/test-global-setup.ts | 9 ++ tests/integration/package.json | 2 +- .../with-policy/field-validation.test.ts | 136 ++++++++++++------ .../enhancements/with-policy/refactor.test.ts | 88 +++++++----- .../tests/regression/issue-1014.test.ts | 52 +++++++ .../tests/regression/issue-177.test.ts | 27 ++++ .../tests/schema/refactor-pg.zmodel | 6 +- .../integration/tests/tsconfig.template.json | 10 -- 24 files changed, 389 insertions(+), 132 deletions(-) create mode 100644 script/set-test-env.ts create mode 100644 script/test-global-setup.ts create mode 100644 tests/integration/tests/regression/issue-1014.test.ts create mode 100644 tests/integration/tests/regression/issue-177.test.ts delete mode 100644 tests/integration/tests/tsconfig.template.json diff --git a/jest.config.ts b/jest.config.ts index 222e6fb2c..b08a6426f 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,7 +9,9 @@ export default { // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, - globalSetup: path.join(__dirname, './test-setup.ts'), + globalSetup: path.join(__dirname, './script/test-global-setup.ts'), + + setupFiles: [path.join(__dirname, './script/set-test-env.ts')], // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, diff --git a/package.json b/package.json index 55b925bd4..febc93264 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "build": "pnpm -r build", "lint": "pnpm -r lint", - "test": "ZENSTACK_TEST=1 pnpm -r --parallel run test --silent --forceExit", - "test-ci": "ZENSTACK_TEST=1 pnpm -r --parallel run test --silent --forceExit", + "test": "pnpm -r --parallel run test --silent --forceExit", + "test-ci": "pnpm -r --parallel run 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/", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 4b97e6824..3ea7a4397 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -17,7 +17,7 @@ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && copyfiles -u 1 ./src/plugin.zmodel dist && pnpm pack dist --pack-destination '../../../../.build'", "watch": "tsc --watch", "lint": "eslint src --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "keywords": [ diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 1399a71c9..7af3405d2 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -13,7 +13,7 @@ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'", "watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\"", "lint": "eslint src --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index e4350e61c..1416132c4 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -69,7 +69,7 @@ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && tsup-node --config ./tsup-v5.config.ts && node scripts/postbuild && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'", "watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\" \"tsup-node --config ./tsup-v5.config.ts --watch\"", "lint": "eslint src --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 27d7b6ca0..4b56afd1c 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -13,7 +13,7 @@ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist && pnpm pack dist --pack-destination '../../../../.build'", "watch": "tsc --watch", "lint": "eslint src --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 383ee356f..f31e145f9 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -481,7 +481,7 @@ export class PolicyProxyHandler implements Pr // Validates the given create payload against Zod schema if any private validateCreateInputSchema(model: string, data: any) { const schema = this.policyUtils.getZodSchema(model, 'create'); - if (schema) { + if (schema && data) { const parseResult = schema.safeParse(data); if (!parseResult.success) { throw this.policyUtils.deniedByPolicy( @@ -514,11 +514,18 @@ export class PolicyProxyHandler implements Pr args = this.policyUtils.clone(args); - // do static input validation and check if post-create checks are needed + // 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.policyUtils.checkInputGuard(this.model, item, 'create'); if (inputCheck === false) { + // unconditionally deny throw this.policyUtils.deniedByPolicy( this.model, 'create', @@ -526,14 +533,10 @@ export class PolicyProxyHandler implements Pr CrudFailureReason.ACCESS_POLICY_VIOLATION ); } else if (inputCheck === true) { - const r = this.validateCreateInputSchema(this.model, item); - if (r !== item) { - this.policyUtils.replace(item, r); - } + // unconditionally allow } else if (inputCheck === undefined) { // static policy check is not possible, need to do post-create check needPostCreateCheck = true; - break; } } @@ -808,7 +811,13 @@ export class PolicyProxyHandler implements Pr // check if the update actually writes to this model let thisModelUpdate = false; - const updatePayload: any = (args as any).data ?? args; + const updatePayload = (args as any).data ?? args; + + const validatedPayload = this.validateUpdateInputSchema(model, updatePayload); + if (validatedPayload !== updatePayload) { + this.policyUtils.replace(updatePayload, validatedPayload); + } + if (updatePayload) { for (const key of Object.keys(updatePayload)) { const field = resolveField(this.modelMeta, model, key); @@ -879,6 +888,8 @@ export class PolicyProxyHandler implements Pr ); } + args.data = this.validateUpdateInputSchema(model, args.data); + const updateGuard = this.policyUtils.getAuthGuard(db, model, 'update'); if (this.policyUtils.isTrue(updateGuard) || this.policyUtils.isFalse(updateGuard)) { // injects simple auth guard into where clause @@ -939,7 +950,10 @@ export class PolicyProxyHandler implements Pr await _registerPostUpdateCheck(model, uniqueFilter); // convert upsert to update - context.parent.update = { where: args.where, data: args.update }; + context.parent.update = { + where: args.where, + data: this.validateUpdateInputSchema(model, args.update), + }; delete context.parent.upsert; // continue visiting the new payload @@ -1038,6 +1052,37 @@ export class PolicyProxyHandler implements Pr return { result, postWriteChecks }; } + // Validates the given update payload against Zod schema if any + private validateUpdateInputSchema(model: string, data: any) { + const schema = this.policyUtils.getZodSchema(model, 'update'); + if (schema && data) { + // update payload can contain non-literal fields, like: + // { x: { increment: 1 } } + // we should only validate literal fields + + const literalData = Object.entries(data).reduce( + (acc, [k, v]) => ({ ...acc, ...(typeof v !== 'object' ? { [k]: v } : {}) }), + {} + ); + + const parseResult = schema.safeParse(literalData); + if (!parseResult.success) { + throw this.policyUtils.deniedByPolicy( + model, + 'update', + `input failed validation: ${fromZodError(parseResult.error)}`, + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parseResult.error + ); + } + + // schema may have transformed field values, use it to overwrite the original data + return { ...data, ...parseResult.data }; + } else { + return data; + } + } + private isUnsafeMutate(model: string, args: any) { if (!args) { return false; @@ -1072,6 +1117,8 @@ export class PolicyProxyHandler implements Pr args = this.policyUtils.clone(args); this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); + args.data = this.validateUpdateInputSchema(this.model, args.data); + if (this.policyUtils.hasAuthGuard(this.model, 'postUpdate') || this.policyUtils.getZodSchema(this.model)) { // use a transaction to do post-update checks const postWriteChecks: PostWriteCheckRecord[] = []; diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 00c6a51b6..5df9bed70 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -317,7 +317,7 @@ export class PolicyUtil extends QueryUtils { /** * Checks if the given model has a policy guard for the given operation. */ - hasAuthGuard(model: string, operation: PolicyOperationKind): boolean { + hasAuthGuard(model: string, operation: PolicyOperationKind) { const guard = this.policy.guard[lowerCaseFirst(model)]; if (!guard) { return false; @@ -326,6 +326,21 @@ export class PolicyUtil extends QueryUtils { return typeof provider !== 'boolean' || provider !== true; } + /** + * Checks if the given model has any field-level override policy guard for the given operation. + */ + hasOverrideAuthGuard(model: string, operation: PolicyOperationKind) { + const guard = this.requireGuard(model); + switch (operation) { + case 'read': + return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX)); + case 'update': + return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX)); + default: + return false; + } + } + /** * Checks model creation policy based on static analysis to the input args. * @@ -632,7 +647,7 @@ export class PolicyUtil extends QueryUtils { preValue?: any ) { let guard = this.getAuthGuard(db, model, operation, preValue); - if (this.isFalse(guard)) { + if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy( model, operation, @@ -805,7 +820,7 @@ export class PolicyUtil extends QueryUtils { */ tryReject(db: CrudContract, model: string, operation: PolicyOperationKind) { const guard = this.getAuthGuard(db, model, operation); - if (this.isFalse(guard)) { + if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); } } diff --git a/packages/schema/package.json b/packages/schema/package.json index 5cbb1b316..762e20089 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -73,7 +73,7 @@ "bundle": "rimraf bundle && pnpm lint --max-warnings=0 && node build/bundle.js --minify", "watch": "tsc --watch", "lint": "eslint src tests --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build", "postinstall": "node bin/post-install.js" }, diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 3a92d393c..13b82f01f 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -13,7 +13,7 @@ import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/cons import { ZModelFormatter } from '../language-server/zmodel-formatter'; import { createZModelServices, ZModelServices } from '../language-server/zmodel-module'; import { mergeBaseModel, resolveImport, resolveTransitiveImports } from '../utils/ast-utils'; -import { findPackageJson } from '../utils/pkg-utils'; +import { findUp } from '../utils/pkg-utils'; import { getVersion } from '../utils/version-utils'; import { CliError } from './cli-error'; @@ -289,7 +289,7 @@ export async function formatDocument(fileName: string) { export function getDefaultSchemaLocation() { // handle override from package.json - const pkgJsonPath = findPackageJson(); + const pkgJsonPath = findUp(['package.json']); if (pkgJsonPath) { const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); if (typeof pkgJson?.zenstack?.schema === 'string') { diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index ec5c3ef0b..1d442f12b 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -64,8 +64,8 @@ export default class DataModelValidator implements AstValidator { } const isArray = idField.type.array; - const isScalar = SCALAR_TYPES.includes(idField.type.type as typeof SCALAR_TYPES[number]) - const isValidType = isScalar || isEnum(idField.type.reference?.ref) + const isScalar = SCALAR_TYPES.includes(idField.type.type as (typeof SCALAR_TYPES)[number]); + const isValidType = isScalar || isEnum(idField.type.reference?.ref); if (isArray || !isValidType) { accept('error', 'Field with @id attribute must be of scalar or enum type', { node: idField }); @@ -121,7 +121,7 @@ export default class DataModelValidator implements AstValidator { fields = (arg.value as ArrayExpr).items as ReferenceExpr[]; if (fields.length === 0) { if (accept) { - accept('error', `"fields" value cannot be emtpy`, { + accept('error', `"fields" value cannot be empty`, { node: arg, }); } @@ -131,7 +131,7 @@ export default class DataModelValidator implements AstValidator { references = (arg.value as ArrayExpr).items as ReferenceExpr[]; if (references.length === 0) { if (accept) { - accept('error', `"references" value cannot be emtpy`, { + accept('error', `"references" value cannot be empty`, { node: arg, }); } @@ -157,6 +157,17 @@ export default class DataModelValidator implements AstValidator { } } else { for (let i = 0; i < fields.length; i++) { + if (!field.type.optional && fields[i].$resolvedType?.nullable) { + // if relation is not optional, then fk field must not be nullable + if (accept) { + accept( + 'error', + `relation "${field.name}" is not optional, but field "${fields[i].target.$refText}" is optional`, + { node: fields[i].target.ref! } + ); + } + } + if (!fields[i].$resolvedType) { if (accept) { accept('error', `field reference is unresolved`, { node: fields[i] }); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 7eebc3040..ed997ed51 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -57,7 +57,7 @@ import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; import telemetry from '../../telemetry'; import { execPackage } from '../../utils/exec-utils'; -import { findPackageJson } from '../../utils/pkg-utils'; +import { findUp } from '../../utils/pkg-utils'; import { AttributeArgValue, ModelFieldType, @@ -666,7 +666,7 @@ function isDescendantOf(model: DataModel, superModel: DataModel): boolean { export function getDefaultPrismaOutputFile(schemaPath: string) { // handle override from package.json - const pkgJsonPath = findPackageJson(path.dirname(schemaPath)); + 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') { diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index ca4ca127d..ce41dac34 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -1,20 +1,40 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import { execSync } from './exec-utils'; export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; -function findUp(names: string[], cwd: string): string | undefined { - let dir = cwd; - // eslint-disable-next-line no-constant-condition - while (true) { - const target = names.find((name) => fs.existsSync(path.join(dir, name))); - if (target) return target; - - const up = path.resolve(dir, '..'); - if (up === dir) return undefined; // it'll fail anyway - dir = up; - } +/** + * A type named FindUp that takes a type parameter e which extends boolean. + * If e extends true, it returns a union type of string[] or undefined. + * If e does not extend true, it returns a union type of string or undefined. + * + * @export + * @template e A type parameter that extends boolean + */ +export type FindUp = e extends true ? string[] | undefined : string | undefined +/** + * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. + * Optionally return a single path or multiple paths. + * If multiple allowed, return all paths found. + * If no paths are found, return undefined. + * + * @export + * @template [e=false] + * @param names An array of strings representing names to search for within the directory + * @param cwd A string representing the current working directory + * @param [multiple=false as e] A boolean flag indicating whether to search for multiple levels. Useful for finding node_modules directories... + * @param [result=[]] An array of strings representing the accumulated results used in multiple results + * @returns Path(s) to a specific file or folder within the directory or parent directories + */ +export function findUp(names: string[], cwd: string = process.cwd(), multiple: e = false as e, result: string[] = []): FindUp { + if (!names.some((name) => !!name)) return undefined; + const target = names.find((name) => fs.existsSync(path.join(cwd, name))); + if (multiple == false && target) return path.join(cwd, target) as FindUp; + if (target) result.push(path.join(cwd, target)); + const up = path.resolve(cwd, '..'); + if (up === cwd) return (multiple && result.length > 0 ? result : undefined) as FindUp; // it'll fail anyway + return findUp(names, up, multiple, result); } function getPackageManager(projectPath = '.'): PackageManagers { @@ -85,6 +105,11 @@ export function ensurePackage( } } +/** + * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. + * It iterates through the directory structure going one level up at a time until it finds a package.json file. If no package.json file is found, it returns undefined. + * @deprecated Use findUp instead @see findUp + */ export function findPackageJson(searchPath?: string) { let currDir = searchPath ?? process.cwd(); while (currDir) { @@ -102,7 +127,7 @@ export function findPackageJson(searchPath?: string) { } export function getPackageJson(searchPath?: string) { - const pkgJsonPath = findPackageJson(searchPath); + const pkgJsonPath = findUp(['package.json'], searchPath ?? process.cwd()); if (pkgJsonPath) { return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); } else { diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 955f315c3..1de69ee26 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -10,7 +10,7 @@ describe('Data Model Validation Tests', () => { it('duplicated fields', async () => { const result = await safelyLoadModel(` - ${prelude} + ${ prelude } model M { id String @id x Int @@ -128,7 +128,7 @@ 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) diff --git a/packages/server/package.json b/packages/server/package.json index 2744a1c15..f988b0e93 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -9,7 +9,7 @@ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../.build'", "watch": "tsc --watch", "lint": "eslint src --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/script/set-test-env.ts b/script/set-test-env.ts new file mode 100644 index 000000000..4db61d8d1 --- /dev/null +++ b/script/set-test-env.ts @@ -0,0 +1 @@ +process.env.ZENSTACK_TEST = '1'; diff --git a/script/test-global-setup.ts b/script/test-global-setup.ts new file mode 100644 index 000000000..514cccae7 --- /dev/null +++ b/script/test-global-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/package.json b/tests/integration/package.json index 40627f354..8aed0b6c8 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint . --ext .ts", - "test": "ZENSTACK_TEST=1 jest" + "test": "jest" }, "keywords": [], "author": "", 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 16f56dddd..84a3496d1 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -45,7 +45,7 @@ describe('With Policy: field validation', () => { id String @id @default(cuid()) user User @relation(fields: [userId], references: [id]) userId String - slug String @regex("^[0-9a-zA-Z]{4,16}$") + slug String @regex("^[0-9a-zA-Z]{4,16}$") @lower @@allow('all', true) } @@ -508,50 +508,104 @@ describe('With Policy: field validation', () => { }, }); - await expect( - db.userData.create({ - data: { - userId: '1', - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - text6: ' AbC ', - text7: 'abc', + let ud = await db.userData.create({ + data: { + userId: '1', + a: 1, + b: 0, + c: -1, + d: 0, + text1: 'abc123', + text2: 'def', + text3: 'aaa', + text4: 'abcab', + text6: ' AbC ', + text7: 'abc', + }, + }); + expect(ud).toMatchObject({ text6: 'abc', text7: 'ABC' }); + + ud = await db.userData.update({ + where: { id: ud.id }, + data: { + text4: 'xyz', + text6: ' bCD ', + text7: 'bcd', + }, + }); + expect(ud).toMatchObject({ text4: 'xyz', text6: 'bcd', text7: 'BCD' }); + + let u = await db.user.create({ + data: { + id: '2', + password: 'abc123!@#', + email: 'who@myorg.com', + handle: 'user2', + userData: { + create: { + a: 1, + b: 0, + c: -1, + d: 0, + text1: 'abc123', + text2: 'def', + text3: 'aaa', + text4: 'abcab', + text6: ' AbC ', + text7: 'abc', + }, }, - }) - ).resolves.toMatchObject({ text6: 'abc', text7: 'ABC' }); + }, + include: { userData: true }, + }); + expect(u.userData).toMatchObject({ + text6: 'abc', + text7: 'ABC', + }); - await expect( - db.user.create({ - data: { - id: '2', - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user2', - userData: { - create: { - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - text6: ' AbC ', - text7: 'abc', - }, + u = await db.user.update({ + where: { id: u.id }, + data: { + userData: { + update: { + data: { text4: 'xyz', text6: ' bCD ', text7: 'bcd' }, }, }, - include: { userData: true }, - }) - ).resolves.toMatchObject({ - userData: expect.objectContaining({ text6: 'abc', text7: 'ABC' }), + }, + include: { userData: true }, + }); + expect(u.userData).toMatchObject({ text4: 'xyz', text6: 'bcd', text7: 'BCD' }); + + // upsert create + u = await db.user.update({ + where: { id: u.id }, + data: { + tasks: { + upsert: { + where: { id: 'unknown' }, + create: { slug: 'SLUG1' }, + update: {}, + }, + }, + }, + include: { tasks: true }, + }); + expect(u.tasks[0]).toMatchObject({ slug: 'slug1' }); + + // upsert update + u = await db.user.update({ + where: { id: u.id }, + data: { + tasks: { + upsert: { + where: { id: u.tasks[0].id }, + create: {}, + update: { slug: 'SLUG2' }, + }, + }, + }, + include: { tasks: true }, }); + expect(u.tasks[0]).toMatchObject({ slug: 'slug2' }); }); }); diff --git a/tests/integration/tests/enhancements/with-policy/refactor.test.ts b/tests/integration/tests/enhancements/with-policy/refactor.test.ts index 0cd490f6c..3c725697d 100644 --- a/tests/integration/tests/enhancements/with-policy/refactor.test.ts +++ b/tests/integration/tests/enhancements/with-policy/refactor.test.ts @@ -144,12 +144,15 @@ describe('With Policy: refactor tests', () => { // read back check await expect( anonDb.user.create({ - data: { id: 1, email: 'user1@zenstack.dev' }, + data: { id: 1, email: 'User1@zenstack.dev' }, }) ).rejects.toThrow(/not allowed to be read back/); // success - await expect(user1Db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(user1Db.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ + // email to lower + email: 'user1@zenstack.dev', + }); // nested creation failure await expect( @@ -202,7 +205,7 @@ describe('With Policy: refactor tests', () => { posts: { create: { id: 2, - title: 'Post 2', + title: ' Post 2 ', published: true, comments: { create: { @@ -213,8 +216,14 @@ describe('With Policy: refactor tests', () => { }, }, }, + include: { posts: true }, }) - ).toResolveTruthy(); + ).resolves.toMatchObject({ + posts: expect.arrayContaining([ + // title is trimmed + expect.objectContaining({ title: 'Post 2' }), + ]), + }); // create with connect: posts await expect( @@ -389,7 +398,7 @@ describe('With Policy: refactor tests', () => { data: [ { id: 7, title: 'Post 7.1' }, { id: 7, title: 'Post 7.2' }, - { id: 8, title: 'Post 8' }, + { id: 8, title: ' Post 8 ' }, ], skipDuplicates: true, }, @@ -400,7 +409,10 @@ describe('With Policy: refactor tests', () => { // success await expect(adminDb.user.findUnique({ where: { id: 7 } })).toResolveTruthy(); await expect(adminDb.post.findUnique({ where: { id: 7 } })).toResolveTruthy(); - await expect(adminDb.post.findUnique({ where: { id: 8 } })).toResolveTruthy(); + await expect(adminDb.post.findUnique({ where: { id: 8 } })).resolves.toMatchObject({ + // title is trimmed + title: 'Post 8', + }); }); it('createMany', async () => { @@ -412,11 +424,18 @@ describe('With Policy: refactor tests', () => { await expect( user1Db.post.createMany({ data: [ - { id: 1, title: 'Post 1', authorId: 1 }, + { id: 1, title: ' Post 1 ', authorId: 1 }, { id: 2, title: 'Post 2', authorId: 1 }, ], }) - ).resolves.toMatchObject({ count: 2 }); + ).toResolveTruthy(); + + await expect(user1Db.post.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'Post 1' }), // title is trimmed + expect.objectContaining({ title: 'Post 2' }), + ]) + ); // unique constraint violation await expect( @@ -502,8 +521,8 @@ describe('With Policy: refactor tests', () => { user2Db.user.update({ where: { id: 1 }, data: { email: 'user2@zenstack.dev' } }) ).toBeRejectedByPolicy(); await expect( - adminDb.user.update({ where: { id: 1 }, data: { email: 'user1-nice@zenstack.dev' } }) - ).toResolveTruthy(); + adminDb.user.update({ where: { id: 1 }, data: { email: 'User1-nice@zenstack.dev' } }) + ).resolves.toMatchObject({ email: 'user1-nice@zenstack.dev' }); // update nested profile await expect( @@ -561,9 +580,10 @@ describe('With Policy: refactor tests', () => { await expect( user1Db.user.update({ where: { id: 1 }, - data: { posts: { update: { where: { id: 1 }, data: { published: false } } } }, + data: { posts: { update: { where: { id: 1 }, data: { title: ' New ', published: false } } } }, + include: { posts: true }, }) - ).toResolveTruthy(); + ).resolves.toMatchObject({ posts: expect.arrayContaining([expect.objectContaining({ title: 'New' })]) }); // update nested comment prevent update of toplevel await expect( @@ -588,23 +608,24 @@ describe('With Policy: refactor tests', () => { await expect(adminDb.comment.findFirst({ where: { content: 'Comment 2 updated' } })).toResolveFalsy(); // update with create - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - create: { - id: 3, - title: 'Post 3', - published: true, - comments: { - create: { author: { connect: { id: 1 } }, content: 'Comment 3' }, - }, + const r1 = await user1Db.user.update({ + where: { id: 1 }, + data: { + posts: { + create: { + id: 3, + title: 'Post 3', + published: true, + comments: { + create: { author: { connect: { id: 1 } }, content: ' Comment 3 ' }, }, }, }, - }) - ).toResolveTruthy(); + }, + include: { posts: { include: { comments: true } } }, + }); + expect(r1.posts[r1.posts.length - 1].comments[0].content).toEqual('Comment 3'); + await expect( user1Db.user.update({ where: { id: 1 }, @@ -636,7 +657,7 @@ describe('With Policy: refactor tests', () => { posts: { createMany: { data: [ - { id: 4, title: 'Post 4' }, + { id: 4, title: ' Post 4 ' }, { id: 5, title: 'Post 5' }, ], }, @@ -644,6 +665,7 @@ describe('With Policy: refactor tests', () => { }, }) ).toResolveTruthy(); + await expect(user1Db.post.findUnique({ where: { id: 4 } })).resolves.toMatchObject({ title: 'Post 4' }); await expect( user1Db.user.update({ include: { posts: true }, @@ -723,12 +745,13 @@ describe('With Policy: refactor tests', () => { posts: { update: { where: { id: 1 }, - data: { title: 'Post1-1' }, + data: { title: ' Post1-1' }, }, }, }, }) ).toResolveTruthy(); + await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post1-1' }); await expect( user1Db.user.update({ where: { id: 1 }, @@ -799,14 +822,14 @@ describe('With Policy: refactor tests', () => { posts: { upsert: { where: { id: 1 }, - update: { title: 'Post 1-1' }, // update + update: { title: ' Post 2' }, // update create: { id: 7, title: 'Post 1' }, }, }, }, }) ).toResolveTruthy(); - await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post 1-1' }); + await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post 2' }); await expect( user1Db.user.update({ where: { id: 1 }, @@ -815,7 +838,7 @@ describe('With Policy: refactor tests', () => { upsert: { where: { id: 7 }, update: { title: 'Post 7-1' }, - create: { id: 7, title: 'Post 7' }, // create + create: { id: 7, title: ' Post 7' }, // create }, }, }, @@ -1094,9 +1117,10 @@ describe('With Policy: refactor tests', () => { ).toBeRejectedByPolicy(); await expect( user1Db.post.updateMany({ - data: { title: 'My post' }, + data: { title: ' My post' }, }) ).resolves.toMatchObject({ count: 2 }); + await expect(user1Db.post.findFirst()).resolves.toMatchObject({ title: 'My post' }); }); it('delete single', async () => { diff --git a/tests/integration/tests/regression/issue-1014.test.ts b/tests/integration/tests/regression/issue-1014.test.ts new file mode 100644 index 000000000..ad862db42 --- /dev/null +++ b/tests/integration/tests/regression/issue-1014.test.ts @@ -0,0 +1,52 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1014', () => { + it('update', async () => { + const { prisma, enhance } = await loadSchema( + ` + model User { + id Int @id() @default(autoincrement()) + name String + posts Post[] + } + + model Post { + id Int @id() @default(autoincrement()) + title String + content String? + author User? @relation(fields: [authorId], references: [id]) + authorId Int? @allow('update', true, true) + + @@allow('read', true) + } + ` + ); + + const db = enhance(); + + const user = await prisma.user.create({ data: { name: 'User1' } }); + const post = await prisma.post.create({ data: { title: 'Post1' } }); + await expect(db.post.update({ where: { id: post.id }, data: { authorId: user.id } })).toResolveTruthy(); + }); + + it('read', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Post { + id Int @id() @default(autoincrement()) + title String @allow('read', true, true) + content String + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const post = await prisma.post.create({ data: { title: 'Post1', content: 'Content' } }); + await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); + await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ + title: 'Post1', + }); + }); +}); diff --git a/tests/integration/tests/regression/issue-177.test.ts b/tests/integration/tests/regression/issue-177.test.ts new file mode 100644 index 000000000..d270580c5 --- /dev/null +++ b/tests/integration/tests/regression/issue-177.test.ts @@ -0,0 +1,27 @@ +import { loadModelWithError } from '@zenstackhq/testtools'; + +describe('issue 177', () => { + it('regression', async () => { + await expect( + loadModelWithError( + ` + model Foo { + id String @id @default(cuid()) + + bar Bar @relation(fields: [barId1, barId2], references: [id1, id2]) + barId1 String? + barId2 String + } + + model Bar { + id1 String @default(cuid()) + id2 String @default(cuid()) + foos Foo[] + + @@id([id1, id2]) + } + ` + ) + ).resolves.toContain('relation "bar" is not optional, but field "barId1" is optional'); + }); +}); diff --git a/tests/integration/tests/schema/refactor-pg.zmodel b/tests/integration/tests/schema/refactor-pg.zmodel index f52f36c98..d0b4579e1 100644 --- a/tests/integration/tests/schema/refactor-pg.zmodel +++ b/tests/integration/tests/schema/refactor-pg.zmodel @@ -5,7 +5,7 @@ enum Role { model User { id Int @id @default(autoincrement()) - email String @unique @email + email String @unique @email @lower role Role @default(USER) profile Profile? posts Post[] @@ -52,7 +52,7 @@ model Image { model Post { id Int @id @default(autoincrement()) - title String @length(1, 8) + title String @length(1, 8) @trim published Boolean @default(false) comments Comment[] author User @relation(fields: [authorId], references: [id], onDelete: Cascade) @@ -67,7 +67,7 @@ model Post { model Comment { id Int @id @default(autoincrement()) - content String + content String @trim author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId Int diff --git a/tests/integration/tests/tsconfig.template.json b/tests/integration/tests/tsconfig.template.json deleted file mode 100644 index 18a6bedec..000000000 --- a/tests/integration/tests/tsconfig.template.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - } -} From ca2a314a927053703e4dbc76542499159b8bf6a8 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 26 Feb 2024 17:03:41 -0800 Subject: [PATCH 040/127] fix: foreign key constraint ambiguity in generated delegate prisma schema (#1060) --- .../src/plugins/prisma/schema-generator.ts | 5 ++ .../with-delegate/regressions.test.ts | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 tests/integration/tests/enhancements/with-delegate/regressions.test.ts diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index ed997ed51..9a8ccb0eb 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -456,6 +456,11 @@ export class PrismaSchemaGenerator { new PrismaFieldAttribute('@relation', [ new PrismaAttributeArg('fields', args), new PrismaAttributeArg('references', args), + // generate a `map` argument for foreign key constraint disambiguation + new PrismaAttributeArg( + 'map', + new PrismaAttributeArgValue('String', `${relationField.name}_fk`) + ), ]) ); } else { diff --git a/tests/integration/tests/enhancements/with-delegate/regressions.test.ts b/tests/integration/tests/enhancements/with-delegate/regressions.test.ts new file mode 100644 index 000000000..77166e275 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/regressions.test.ts @@ -0,0 +1,53 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression tests', () => { + it('FK Constraint Ambiguity', 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' }); + }); +}); From 0ac596f3683328d9547ebc72156de2f7e53fb810 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 2 Mar 2024 14:02:08 -0800 Subject: [PATCH 041/127] chore: merge from dev (#1072) --- README.md | 30 ++++- packages/README.md | 122 ------------------ packages/language/package.json | 2 +- .../plugins/tanstack-query/src/generator.ts | 12 +- .../tanstack-query/src/runtime-v5/index.ts | 2 +- .../tanstack-query/src/runtime-v5/react.ts | 16 ++- .../tanstack-query/src/runtime-v5/svelte.ts | 12 +- .../tanstack-query/src/runtime/common.ts | 19 ++- .../tanstack-query/src/runtime/index.ts | 2 +- .../tanstack-query/src/runtime/react.ts | 12 +- .../tanstack-query/src/runtime/svelte.ts | 12 +- .../plugins/tanstack-query/src/runtime/vue.ts | 14 +- .../src/plugins/zod/utils/schema-gen.ts | 2 +- .../validation/datamodel-validation.test.ts | 76 ++++++----- packages/server/tests/api/rest.test.ts | 90 ++++++++++++- packages/server/tests/utils.ts | 1 + tests/integration/tests/plugins/zod.test.ts | 44 +++++++ 17 files changed, 274 insertions(+), 194 deletions(-) delete mode 100644 packages/README.md diff --git a/README.md b/README.md index bb640ae6c..ed37238f8 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,35 @@ Join our [discord server](https://discord.gg/Ykhr738dUe) for chat and updates! If you like ZenStack, join us to make it a better tool! Please use the [Contributing Guide](CONTRIBUTING.md) for details on how to get started, and don't hesitate to join [Discord](https://discord.gg/Ykhr738dUe) to share your thoughts. -Please also consider [sponsoring our work](https://github.com/sponsors/zenstackhq) to speed up the development of ZenStack. Thank you! +Please also consider [sponsoring our work](https://github.com/sponsors/zenstackhq) to speed up the development. Your contribution will be 100% used as a bounty reward to encourage community members to help fix bugs, add features, and improve documentation. + +## Sponsors + +Thank you for your support! + + + + + + + +
Johann Rohn
Johann Rohn
Benjamin Zecirovic
Benjamin Zecirovic
+ +## Contributors + +Thanks to all the contributors who have helped make ZenStack better! + +#### Source + + + + + +#### Docs + + + + ## License diff --git a/packages/README.md b/packages/README.md deleted file mode 100644 index d4f076584..000000000 --- a/packages/README.md +++ /dev/null @@ -1,122 +0,0 @@ - - -## What it is - -ZenStack is a toolkit that simplifies the development of a web app's backend. It supercharges [Prisma ORM](https://prisma.io) with a powerful access control layer and unleashes its full potential for web development. - -Our goal is to let you save time writing boilerplate code and focus on building real features! - -## How it works - -ZenStack extended Prisma schema language for supporting custom attributes and functions and, based on that, implemented a flexible access control layer around Prisma. - -```prisma -// schema.zmodel - -model Post { - id String @id - title String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - - // 🔐 allow logged-in users to read published posts - @@allow('read', auth() != null && published) - - // 🔐 allow full CRUD by author - @@allow('all', author == auth()) -} -``` - -At runtime, transparent proxies are created around Prisma clients for intercepting queries and mutations to enforce access policies. Moreover, framework integration packages help you wrap an access-control-enabled Prisma client into backend APIs that can be safely called from the frontend. - -```ts -// Next.js example: pages/api/model/[...path].ts - -import { requestHandler } from '@zenstackhq/next'; -import { enhance } from '@zenstackhq/runtime'; -import { getSessionUser } from '@lib/auth'; -import { prisma } from '@lib/db'; - -export default requestHandler({ - getPrisma: (req, res) => enhance(prisma, { user: getSessionUser(req, res) }), -}); -``` - -Plugins can generate strong-typed client libraries that talk to the APIs: - -```tsx -// React example: components/MyPosts.tsx - -import { usePost } from '@lib/hooks'; - -const MyPosts = () => { - // Post CRUD hooks - const { findMany } = usePost(); - - // list all posts that're visible to the current user, together with their authors - const { data: posts } = findMany({ - include: { author: true }, - orderBy: { createdAt: 'desc' }, - }); - - return ( -
    - {posts?.map((post) => ( -
  • - {post.title} by {post.author.name} -
  • - ))} -
- ); -}; -``` - -## Links - -- [Home](https://zenstack.dev) -- [Documentation](https://zenstack.dev/docs) -- [Community chat](https://go.zenstack.dev/chat) -- [Twitter](https://twitter.com/zenstackhq) -- [Blog](https://dev.to/zenstack) - -## Features - -- Access control and data validation rules right inside your Prisma schema -- Auto-generated RESTful API and client library -- End-to-end type safety -- Extensible: custom attributes, functions, and a plugin system -- Framework agnostic -- Uncompromised performance - -## Examples - -Check out the [Collaborative Todo App](https://zenstack-todo.vercel.app/) for a running example. You can find the source code below: - -- [Next.js + React hooks implementation](https://github.com/zenstackhq/sample-todo-nextjs) -- [Next.js + tRPC implementation](https://github.com/zenstackhq/sample-todo-trpc) - -## Community - -Join our [discord server](https://go.zenstack.dev/chat) for chat and updates! - -## License - -[MIT](LICENSE) diff --git a/packages/language/package.json b/packages/language/package.json index fb5e62fec..0b7ea3673 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -23,7 +23,7 @@ "plist2": "^1.1.3" }, "dependencies": { - "langium": "1.3.1" + "langium": "1.3.1" }, "contributes": { "languages": [ diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 58836091a..a6cd75a5c 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -211,7 +211,7 @@ function generateMutationHook( { name: `_mutation`, initializer: ` - useModelMutation<${argsType}, ${ + useModelMutation<${argsType}, DefaultError, ${ overrideReturnType ?? model }, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst( model @@ -560,9 +560,9 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, - `import type { PickEnumerable, CheckSelect } from '${runtimeImportBase}';`, + `import type { PickEnumerable, CheckSelect, QueryError } from '${runtimeImportBase}';`, `import metadata from './__model_meta';`, - `type DefaultError = Error;`, + `type DefaultError = QueryError;`, ]; switch (target) { case 'react': { @@ -638,11 +638,11 @@ function makeQueryOptions( function makeMutationOptions(target: string, returnType: string, argsType: string) { switch (target) { case 'react': - return `UseMutationOptions<${returnType}, unknown, ${argsType}>`; + return `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`; case 'vue': - return `UseMutationOptions<${returnType}, unknown, ${argsType}, unknown>`; + return `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`; case 'svelte': - return `MutationOptions<${returnType}, unknown, ${argsType}>`; + return `MutationOptions<${returnType}, DefaultError, ${argsType}>`; default: throw new PluginError(name, `Unsupported target: ${target}`); } diff --git a/packages/plugins/tanstack-query/src/runtime-v5/index.ts b/packages/plugins/tanstack-query/src/runtime-v5/index.ts index 302b775fc..2954d4683 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/index.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/index.ts @@ -1,2 +1,2 @@ export * from '../runtime/prisma-types'; -export { type FetchFn, getQueryKey } from '../runtime/common'; +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 375cb2676..92194535f 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + UseSuspenseInfiniteQueryOptions, + UseSuspenseQueryOptions, useInfiniteQuery, useMutation, useQuery, @@ -10,14 +12,11 @@ import { type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, - UseSuspenseInfiniteQueryOptions, - UseSuspenseQueryOptions, } from '@tanstack/react-query-v5'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; import { DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, @@ -25,6 +24,7 @@ import { setupInvalidation, setupOptimisticUpdate, type APIContext, + type FetchFn, } from '../runtime/common'; /** @@ -167,12 +167,18 @@ export function useSuspenseInfiniteModelQuery( * @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( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts index 5f479138e..7de2202d6 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts @@ -16,13 +16,13 @@ import { Readable, derived } from 'svelte/store'; import { APIContext, DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, marshal, setupInvalidation, setupOptimisticUpdate, + type FetchFn, } from '../runtime/common'; export { APIContext as RequestHandlerContext } from '../runtime/common'; @@ -147,12 +147,18 @@ function isStore(opt: unknown): opt is Readable { * @param invalidateQueries Whether to invalidate queries after mutation. * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts index ea64bd3e2..b0d45b246 100644 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ b/packages/plugins/tanstack-query/src/runtime/common.ts @@ -25,6 +25,21 @@ export const QUERY_KEY_PREFIX = 'zenstack'; */ export type FetchFn = (url: string, options?: RequestInit) => Promise; +/** + * Type for query and mutation errors. + */ +export type QueryError = Error & { + /** + * Additional error information. + */ + info?: unknown; + + /** + * HTTP status code. + */ + status?: number; +}; + /** * Context type for configuring the hooks. */ @@ -64,9 +79,7 @@ export async function fetcher( // policy doesn't allow mutation result to be read back, just return undefined return undefined as any; } - const error: Error & { info?: unknown; status?: number } = new Error( - 'An error occurred while fetching the data.' - ); + const error: QueryError = new Error('An error occurred while fetching the data.'); error.info = errData.error; error.status = res.status; throw error; diff --git a/packages/plugins/tanstack-query/src/runtime/index.ts b/packages/plugins/tanstack-query/src/runtime/index.ts index 909c0c4bf..0894bc461 100644 --- a/packages/plugins/tanstack-query/src/runtime/index.ts +++ b/packages/plugins/tanstack-query/src/runtime/index.ts @@ -1,2 +1,2 @@ export * from './prisma-types'; -export { type FetchFn, getQueryKey } from './common'; +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 2f75d88eb..607b57430 100644 --- a/packages/plugins/tanstack-query/src/runtime/react.ts +++ b/packages/plugins/tanstack-query/src/runtime/react.ts @@ -12,7 +12,6 @@ import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; import { DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, @@ -20,6 +19,7 @@ import { setupInvalidation, setupOptimisticUpdate, type APIContext, + type FetchFn, } from './common'; /** @@ -110,12 +110,18 @@ export function useInfiniteModelQuery( * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime/svelte.ts b/packages/plugins/tanstack-query/src/runtime/svelte.ts index 88c675a82..dbd0342aa 100644 --- a/packages/plugins/tanstack-query/src/runtime/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime/svelte.ts @@ -13,13 +13,13 @@ import { getContext, setContext } from 'svelte'; import { APIContext, DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, marshal, setupInvalidation, setupOptimisticUpdate, + type FetchFn, } from './common'; export { APIContext as RequestHandlerContext } from './common'; @@ -109,12 +109,18 @@ export function useInfiniteModelQuery( * @param optimisticUpdate Whether to enable automatic optimistic update. * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime/vue.ts b/packages/plugins/tanstack-query/src/runtime/vue.ts index b0a35f5f3..049b66907 100644 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime/vue.ts @@ -14,13 +14,13 @@ import { inject, provide } from 'vue'; import { APIContext, DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, marshal, setupInvalidation, setupOptimisticUpdate, + type FetchFn, } from './common'; export { APIContext as RequestHandlerContext } from './common'; @@ -113,12 +113,18 @@ export function useInfiniteModelQuery( * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, @@ -168,5 +174,5 @@ export function useModelMutation(finalOptions); + return useMutation(finalOptions); } diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 74d3c18b7..ec181e8d4 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -172,7 +172,7 @@ function makeZodSchema(field: DataModelField) { schema = 'z.boolean()'; break; case 'DateTime': - schema = 'z.date()'; + schema = 'z.coerce.date()'; break; case 'Bytes': schema = 'z.union([z.string(), z.instanceof(Uint8Array)])'; diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 1de69ee26..736a202cb 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -199,102 +199,100 @@ describe('Data Model Validation Tests', () => { 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]) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 770d05017..a7ff47d9a 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -40,10 +40,11 @@ describe('REST server tests', () => { id Int @id @default(autoincrement()) createdAt DateTime @default (now()) updatedAt DateTime @updatedAt - title String + title String @length(1, 10) author User? @relation(fields: [authorId], references: [myId]) authorId String? published Boolean @default(false) + publishedAt DateTime? viewCount Int @default(0) comments Comment[] setting Setting? @@ -1293,6 +1294,49 @@ describe('REST server tests', () => { }); }); + it('creates an item with date coercion', async () => { + const r = await handler({ + method: 'post', + path: '/post', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + id: 1, + title: 'Post1', + published: true, + publishedAt: '2024-03-02T05:00:00.000Z', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(201); + }); + + it('creates an item with zod violation', async () => { + const r = await handler({ + method: 'post', + path: '/post', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + id: 1, + title: 'a very very long long title', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(400); + expect(r.body.errors[0].code).toBe('invalid-payload'); + }); + it('creates an item with collection relations', async () => { await prisma.post.create({ data: { id: 1, title: 'Post1' }, @@ -1586,6 +1630,50 @@ describe('REST server tests', () => { }); }); + it('update an item with date coercion', async () => { + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + + const r = await handler({ + method: 'put', + path: '/post/1', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + published: true, + publishedAt: '2024-03-02T05:00:00.000Z', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(200); + }); + + it('update an item with zod violation', async () => { + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + + const r = await handler({ + method: 'put', + path: '/post/1', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + publishedAt: '2024-13-01', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(400); + expect(r.body.errors[0].code).toBe('invalid-payload'); + }); + it('update a single relation', async () => { await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); await prisma.post.create({ diff --git a/packages/server/tests/utils.ts b/packages/server/tests/utils.ts index d1e0a0ffc..472a6818d 100644 --- a/packages/server/tests/utils.ts +++ b/packages/server/tests/utils.ts @@ -20,6 +20,7 @@ model Post { author User? @relation(fields: [authorId], references: [id]) authorId String? published Boolean @default(false) + publishedAt DateTime? viewCount Int @default(0) @@allow('all', author == auth()) diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index fd12d8b60..dd82f6786 100644 --- a/tests/integration/tests/plugins/zod.test.ts +++ b/tests/integration/tests/plugins/zod.test.ts @@ -503,6 +503,50 @@ describe('Zod plugin tests', () => { ).toBeFalsy(); }); + it('does date coercion', async () => { + const { zodSchemas } = await loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator js { + provider = 'prisma-client-js' + } + + plugin zod { + provider = "@core/zod" + } + + model Model { + id Int @id @default(autoincrement()) + dt DateTime + } + `, + { addPrelude: false, pushDb: false } + ); + const schemas = zodSchemas.models; + + expect( + schemas.ModelCreateSchema.safeParse({ + dt: new Date(), + }).success + ).toBeTruthy(); + + expect( + schemas.ModelCreateSchema.safeParse({ + dt: '2023-01-01T00:00:00.000Z', + }).success + ).toBeTruthy(); + + expect( + schemas.ModelCreateSchema.safeParse({ + dt: '2023-13-01', + }).success + ).toBeFalsy(); + }); + it('generate for selected models full', async () => { const { projectDir } = await loadSchema( ` From 5b103badea7876b7dfc1da91c26eca3213ddd413 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 3 Mar 2024 18:16:04 -0800 Subject: [PATCH 042/127] perf: improve polymorphism code generation speed (#1073) --- .../src/plugins/enhancer/delegate/index.ts | 4 +- .../src/plugins/enhancer/enhance/index.ts | 370 +++++++++++------- packages/schema/src/plugins/prisma/index.ts | 2 +- .../src/plugins/prisma/schema-generator.ts | 45 ++- .../tests/generator/prisma-generator.test.ts | 26 +- ...regressions.test.ts => issue-1058.test.ts} | 4 +- .../with-delegate/issue-1064.test.ts | 291 ++++++++++++++ .../with-delegate/polymorphism.test.ts | 77 +++- 8 files changed, 658 insertions(+), 161 deletions(-) rename tests/integration/tests/enhancements/with-delegate/{regressions.test.ts => issue-1058.test.ts} (94%) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1064.test.ts diff --git a/packages/schema/src/plugins/enhancer/delegate/index.ts b/packages/schema/src/plugins/enhancer/delegate/index.ts index 5e4cffdfa..d3f85576d 100644 --- a/packages/schema/src/plugins/enhancer/delegate/index.ts +++ b/packages/schema/src/plugins/enhancer/delegate/index.ts @@ -5,8 +5,8 @@ import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; import path from 'path'; export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { - const prismaGenerator = new PrismaSchemaGenerator(); - await prismaGenerator.generate(model, { + const prismaGenerator = new PrismaSchemaGenerator(model); + await prismaGenerator.generate({ provider: '@internal', schemaPath: options.schemaPath, output: path.join(outDir, 'delegate.prisma'), diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 1d42b5912..06caf6950 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -6,19 +6,24 @@ import { isDelegateModel, type PluginOptions, } from '@zenstackhq/sdk'; -import { DataModelField, isDataModel, isReferenceExpr, type DataModel, type Model } from '@zenstackhq/sdk/ast'; +import { DataModel, DataModelField, isDataModel, isReferenceExpr, type Model } from '@zenstackhq/sdk/ast'; import path from 'path'; import { - ForEachDescendantTraversalControl, - MethodSignature, + FunctionDeclarationStructure, + InterfaceDeclaration, + ModuleDeclaration, Node, Project, - PropertySignature, + SourceFile, SyntaxKind, TypeAliasDeclaration, + VariableStatement, } from 'ts-morph'; import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; +// information of delegate models and their sub models +type DelegateInfo = [DataModel, DataModel[]][]; + export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { const outFile = path.join(outDir, 'enhance.ts'); let logicalPrismaClientDir: string | undefined; @@ -34,7 +39,11 @@ import modelMeta from './model-meta'; import policy from './policy'; ${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; -${logicalPrismaClientDir ? `import { PrismaClient as EnhancedPrismaClient } from '${logicalPrismaClientDir}';` : ''} +${ + logicalPrismaClientDir + ? `import type { PrismaClient as EnhancedPrismaClient } from '${logicalPrismaClientDir}/index-fixed';` + : '' +} export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions) { return createEnhancement(prisma, { @@ -58,29 +67,33 @@ function hasDelegateModel(model: Model) { } async function generateLogicalPrisma(model: Model, options: PluginOptions, outDir: string) { - const prismaGenerator = new PrismaSchemaGenerator(); + const prismaGenerator = new PrismaSchemaGenerator(model); const prismaClientOutDir = './.delegate'; - await prismaGenerator.generate(model, { - provider: '@internal', + await prismaGenerator.generate({ + provider: '@internal', // doesn't matter schemaPath: options.schemaPath, output: path.join(outDir, 'delegate.prisma'), overrideClientGenerationPath: prismaClientOutDir, mode: 'logical', }); + // make a bunch of typing fixes to the generated prisma client await processClientTypes(model, path.join(outDir, prismaClientOutDir)); + return prismaClientOutDir; } async function processClientTypes(model: Model, 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')); - const delegateModels: [DataModel, DataModel[]][] = []; + // build a map of delegate models and their sub models + const delegateInfo: DelegateInfo = []; model.declarations .filter((d): d is DataModel => isDelegateModel(d)) .forEach((dm) => { - delegateModels.push([ + delegateInfo.push([ dm, model.declarations.filter( (d): d is DataModel => isDataModel(d) && d.superTypes.some((s) => s.ref === dm) @@ -88,154 +101,231 @@ async function processClientTypes(model: Model, prismaClientDir: string) { ]); }); - const toRemove: (PropertySignature | MethodSignature)[] = []; - const toReplaceText: [TypeAliasDeclaration, string][] = []; - - sf.forEachDescendant((desc, traversal) => { - removeAuxRelationFields(desc, toRemove, traversal); - fixDelegateUnionType(desc, delegateModels, toReplaceText, traversal); - removeCreateFromDelegateInputTypes(desc, delegateModels, toRemove, traversal); - removeDelegateToplevelCreates(desc, delegateModels, toRemove, traversal); - removeDiscriminatorFromConcreteInputTypes(desc, delegateModels, toRemove); + const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { + overwrite: true, }); - - toRemove.forEach((n) => n.remove()); - toReplaceText.forEach(([node, text]) => node.replaceWithText(text)); + transform(sf, sfNew, delegateInfo); + sfNew.formatText(); await project.save(); } -function removeAuxRelationFields( - desc: Node, - toRemove: (PropertySignature | MethodSignature)[], - traversal: ForEachDescendantTraversalControl -) { - if (desc.isKind(SyntaxKind.PropertySignature) || desc.isKind(SyntaxKind.MethodSignature)) { - // remove aux fields - const name = desc.getName(); +function transform(sf: SourceFile, sfNew: SourceFile, delegateModels: DelegateInfo) { + // copy toplevel imports + sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); - if (name.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { - toRemove.push(desc); - traversal.skip(); - } - } + // 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 }); + transformPrismaModule(prismaModule, newPrismaModule, delegateModels); } -function fixDelegateUnionType( - desc: Node, - delegateModels: [DataModel, DataModel[]][], - toReplaceText: [TypeAliasDeclaration, string][], - traversal: ForEachDescendantTraversalControl +function transformPrismaModule( + prismaModule: ModuleDeclaration, + newPrismaModule: ModuleDeclaration, + delegateInfo: DelegateInfo ) { - if (!desc.isKind(SyntaxKind.TypeAliasDeclaration)) { - return; - } + // module block is the direct container of declarations inside a namespace + const moduleBlock = prismaModule.getFirstChildByKindOrThrow(SyntaxKind.ModuleBlock); - const name = desc.getName(); - delegateModels.forEach(([delegate, concreteModels]) => { - if (name === `$${delegate.name}Payload`) { - const discriminator = getDiscriminatorField(delegate); - if (discriminator) { - toReplaceText.push([ - desc, - `export type ${name} = - ${concreteModels - .map((m) => `($${m.name}Payload & { scalars: { ${discriminator.name}: '${m.name}' } })`) - .join(' | ')};`, - ]); - traversal.skip(); - } - } - }); + // 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) => transformVariableStatement(variable)); + newPrismaModule.addVariableStatements(newVariables); + + // transform interfaces + const newInterfaces = moduleBlock.getInterfaces().map((iface) => transformInterface(iface, delegateInfo)); + newPrismaModule.addInterfaces(newInterfaces); + + // transform type aliases + const newTypeAliases = moduleBlock.getTypeAliases().map((typeAlias) => transformTypeAlias(typeAlias, delegateInfo)); + newPrismaModule.addTypeAliases(newTypeAliases); } -function removeCreateFromDelegateInputTypes( - desc: Node, - delegateModels: [DataModel, DataModel[]][], - toRemove: (PropertySignature | MethodSignature)[], - traversal: ForEachDescendantTraversalControl -) { - if (!desc.isKind(SyntaxKind.TypeAliasDeclaration)) { - return; - } +function transformVariableStatement(variable: VariableStatement) { + const structure = variable.getStructure(); - const name = desc.getName(); - delegateModels.forEach(([delegate]) => { - // remove create related sub-payload from delegate's input types since they cannot be created directly - const regex = new RegExp(`\\${delegate.name}(Unchecked)?(Create|Update).*Input`); - if (regex.test(name)) { - desc.forEachDescendant((d, innerTraversal) => { - if ( - d.isKind(SyntaxKind.PropertySignature) && - ['create', 'upsert', 'connectOrCreate'].includes(d.getName()) - ) { - toRemove.push(d); - innerTraversal.skip(); - } + // remove `delegate_aux_*` fields from the variable's typing + const auxFields = findAuxDecls(variable); + if (auxFields.length > 0) { + structure.declarations.forEach((variable) => { + let source = variable.type?.toString(); + auxFields.forEach((f) => { + source = source?.replace(f.getText(), ''); }); - traversal.skip(); - } - }); + variable.type = source; + }); + } + + return structure; } -function removeDiscriminatorFromConcreteInputTypes( - desc: Node, - delegateModels: [DataModel, DataModel[]][], - toRemove: (PropertySignature | MethodSignature)[] -) { - if (!desc.isKind(SyntaxKind.TypeAliasDeclaration)) { - return; +function 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)); } - const name = desc.getName(); - delegateModels.forEach(([delegate, concretes]) => { - const discriminator = getDiscriminatorField(delegate); - if (!discriminator) { - return; + return structure; +} + +function transformTypeAlias(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo) { + const structure = typeAlias.getStructure(); + let source = structure.type as string; + + // remove aux fields + source = removeAuxFieldsFromTypeAlias(typeAlias, source); + + // remove discriminator field from concrete input types + source = removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); + + // remove create/connectOrCreate/upsert fields from delegate's input types + source = removeCreateFromDelegateInput(typeAlias, delegateInfo, source); + + // fix delegate payload union type + source = fixDelegatePayloadType(typeAlias, delegateInfo, source); + + structure.type = source; + return structure; +} + +function 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 = getDiscriminatorField(payloadRecord[0]); + if (discriminatorDecl) { + source = `${payloadRecord[1] + .map( + (concrete) => + `($${concrete.name}Payload & { scalars: { ${discriminatorDecl.name}: '${concrete.name}' } })` + ) + .join(' | ')}`; } + } + return source; +} - concretes.forEach((concrete) => { - // remove discriminator field from the create/update input of concrete models - const regex = new RegExp(`\\${concrete.name}(Unchecked)?(Create|Update).*Input`); - if (regex.test(name)) { - desc.forEachDescendant((d, innerTraversal) => { - if (d.isKind(SyntaxKind.PropertySignature)) { - if (d.getName() === discriminator.name) { - toRemove.push(d); - } - innerTraversal.skip(); - } - }); - } +function 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; } -function removeDelegateToplevelCreates( - desc: Node, - delegateModels: [DataModel, DataModel[]][], - toRemove: (PropertySignature | MethodSignature)[], - traversal: ForEachDescendantTraversalControl +function removeDiscriminatorFromConcreteInput( + typeAlias: TypeAliasDeclaration, + delegateInfo: DelegateInfo, + source: string ) { - if (desc.isKind(SyntaxKind.InterfaceDeclaration)) { - // remove create and upsert methods from delegate interfaces since they cannot be created directly - const name = desc.getName(); - if (delegateModels.map(([dm]) => `${dm.name}Delegate`).includes(name)) { - const createMethod = desc.getMethod('create'); - if (createMethod) { - toRemove.push(createMethod); - } - const createManyMethod = desc.getMethod('createMany'); - if (createManyMethod) { - toRemove.push(createManyMethod); - } - const upsertMethod = desc.getMethod('upsert'); - if (upsertMethod) { - toRemove.push(upsertMethod); - } - traversal.skip(); + // 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 = getDiscriminatorFieldsRecursively(delegateOfConcrete); + discriminators.forEach((discriminatorDecl) => { + const discriminatorNode = findNamedProperty(typeAlias, discriminatorDecl.name); + if (discriminatorNode) { + source = source.replace(discriminatorNode.getText(), ''); + } + }); } } + return source; +} + +function removeAuxFieldsFromTypeAlias(typeAlias: TypeAliasDeclaration, source: string) { + // remove `delegate_aux_*` fields from the type alias + const auxDecls = findAuxDecls(typeAlias); + if (auxDecls.length > 0) { + auxDecls.forEach((d) => { + source = source.replace(d.getText(), ''); + }); + } + return source; +} + +function findNamedProperty(typeAlias: TypeAliasDeclaration, name: string) { + return typeAlias.getFirstDescendant((d) => d.isKind(SyntaxKind.PropertySignature) && d.getName() === name); +} + +function findAuxDecls(node: Node) { + return node + .getDescendantsOfKind(SyntaxKind.PropertySignature) + .filter((n) => n.getName().startsWith(DELEGATE_AUX_RELATION_PREFIX)); } function getDiscriminatorField(delegate: DataModel) { @@ -246,3 +336,19 @@ function getDiscriminatorField(delegate: DataModel) { const arg = delegateAttr.args[0]?.value; return isReferenceExpr(arg) ? (arg.target.ref as DataModelField) : undefined; } + +function getDiscriminatorFieldsRecursively(delegate: DataModel, result: DataModelField[] = []) { + if (isDelegateModel(delegate)) { + const discriminator = getDiscriminatorField(delegate); + if (discriminator) { + result.push(discriminator); + } + + for (const superType of delegate.superTypes) { + if (superType.ref) { + result.push(...getDiscriminatorFieldsRecursively(superType.ref, result)); + } + } + } + return result; +} diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index b27624cd7..5aa64c145 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -5,7 +5,7 @@ export const name = 'Prisma'; export const description = 'Generating Prisma schema'; const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { - return new PrismaSchemaGenerator().generate(model, options); + return new PrismaSchemaGenerator(model).generate(options); }; export default run; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 9a8ccb0eb..4ac78c6e3 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -17,6 +17,7 @@ import { InvocationExpr, isArrayExpr, isDataModel, + isDataSource, isInvocationExpr, isLiteralExpr, isNullExpr, @@ -79,6 +80,7 @@ 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 @@ -95,7 +97,9 @@ export class PrismaSchemaGenerator { private mode: 'logical' | 'physical' = 'physical'; - async generate(model: Model, options: PluginOptions) { + constructor(private readonly zmodel: Model) {} + + async generate(options: PluginOptions) { const warnings: string[] = []; if (options.mode) { this.mode = options.mode as 'logical' | 'physical'; @@ -110,7 +114,7 @@ export 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); @@ -151,7 +155,7 @@ export class PrismaSchemaGenerator { const generateClient = options.generateClient !== false; if (generateClient) { - let generateCmd = `prisma generate --schema "${outFile}"`; + let generateCmd = `prisma generate --schema "${outFile}"${this.mode === 'logical' ? ' --no-engine' : ''}`; if (typeof options.generateArgs === 'string') { generateCmd += ` ${options.generateArgs}`; } @@ -452,17 +456,23 @@ export class PrismaSchemaGenerator { new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) ) ); - relationField.attributes.push( - new PrismaFieldAttribute('@relation', [ - new PrismaAttributeArg('fields', args), - new PrismaAttributeArg('references', args), + + const addedRel = new PrismaFieldAttribute('@relation', [ + new PrismaAttributeArg('fields', args), + new PrismaAttributeArg('references', args), + ]); + + 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)); } @@ -471,6 +481,21 @@ export class PrismaSchemaGenerator { }); } + 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) { if (!attr.decl.ref) { return false; diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 67ba27f99..35d68fb28 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -49,7 +49,7 @@ describe('Prisma generator test', () => { } `); - await new PrismaSchemaGenerator().generate(model, { + await new PrismaSchemaGenerator(model).generate({ name: 'Prisma', provider: '@core/prisma', schemaPath: 'schema.zmodel', @@ -90,7 +90,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', @@ -128,7 +128,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', @@ -162,7 +162,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', @@ -194,7 +194,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', @@ -230,7 +230,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', @@ -270,7 +270,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', @@ -321,7 +321,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', @@ -357,7 +357,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', @@ -380,7 +380,7 @@ describe('Prisma generator test', () => { 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', @@ -430,7 +430,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', @@ -461,7 +461,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', @@ -496,7 +496,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/tests/integration/tests/enhancements/with-delegate/regressions.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts similarity index 94% rename from tests/integration/tests/enhancements/with-delegate/regressions.test.ts rename to tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts index 77166e275..cd566c71f 100644 --- a/tests/integration/tests/enhancements/with-delegate/regressions.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/issue-1058.test.ts @@ -1,7 +1,7 @@ import { loadSchema } from '@zenstackhq/testtools'; -describe('Regression tests', () => { - it('FK Constraint Ambiguity', async () => { +describe('Regression for issue 1058', () => { + it('test', async () => { const schema = ` model User { id String @id @default(cuid()) 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/polymorphism.test.ts b/tests/integration/tests/enhancements/with-delegate/polymorphism.test.ts index 0d0b24ca2..31976fbce 100644 --- a/tests/integration/tests/enhancements/with-delegate/polymorphism.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/polymorphism.test.ts @@ -1,5 +1,7 @@ -import { loadSchema } from '@zenstackhq/testtools'; import { PrismaErrorCode } from '@zenstackhq/runtime'; +import { loadSchema, run } from '@zenstackhq/testtools'; +import fs from 'fs'; +import path from 'path'; describe('Polymorphism Test', () => { const schema = ` @@ -1012,4 +1014,77 @@ model Gallery { 'groupBy with fields from base type is not supported yet' ); }); + + it('typescript compilation', async () => { + const { projectDir } = await loadSchema(schema, { enhancements: ['delegate'] }); + const src = ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + const prisma = new PrismaClient(); + + async function main() { + await prisma.user.deleteMany(); + 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() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); + `; + + fs.writeFileSync(path.join(projectDir, 'script.ts'), src); + fs.writeFileSync( + path.join(projectDir, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + outDir: 'dist', + strict: true, + lib: ['esnext'], + esModuleInterop: true, + }, + }) + ); + + run('npm i -D @types/node', undefined, projectDir); + run('npx tsc --noEmit --skipLibCheck script.ts', undefined, projectDir); + }); }); From 430ab627158707f82f033dfa5d62f891df89d047 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 3 Mar 2024 20:52:41 -0800 Subject: [PATCH 043/127] chore: temporarily disable release-please (#1074) --- .github/workflows/management-changelog.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/management-changelog.yml b/.github/workflows/management-changelog.yml index d7460a53c..9b6ba1f12 100644 --- a/.github/workflows/management-changelog.yml +++ b/.github/workflows/management-changelog.yml @@ -1,10 +1,11 @@ on: push: - branches: - - main # Your main branch - - dev # Your development branch - - release/* # Your releases branch - - v2 # Temp V2 integration branch + branches: [] + # branches: + # - main # Your main branch + # - dev # Your development branch + # - release/* # Your releases branch + # - v2 # Temp V2 integration branch permissions: contents: read From 6e7993afa8dde03ae12c44f198bcca04724dbc92 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 5 Mar 2024 22:36:41 -0800 Subject: [PATCH 044/127] fix: clean up generation of logical prisma client (#1082) --- .../plugins/openapi/src/generator-base.ts | 4 +- packages/plugins/openapi/src/index.ts | 14 +- .../plugins/openapi/src/rest-generator.ts | 2 +- packages/plugins/openapi/src/rpc-generator.ts | 2 +- packages/plugins/swr/src/generator.ts | 9 +- packages/plugins/swr/src/index.ts | 13 +- .../plugins/tanstack-query/src/generator.ts | 9 +- packages/plugins/tanstack-query/src/index.ts | 13 +- packages/plugins/trpc/src/generator.ts | 18 +- packages/plugins/trpc/src/helpers.ts | 7 +- packages/plugins/trpc/src/index.ts | 13 +- packages/runtime/package.json | 3 + packages/runtime/res/prisma.d.ts | 1 + packages/runtime/src/prisma.d.ts | 2 + packages/schema/src/cli/plugin-runner.ts | 127 +-- .../src/plugins/enhancer/delegate/index.ts | 16 - .../src/plugins/enhancer/enhance/index.ts | 85 +- packages/schema/src/plugins/enhancer/index.ts | 57 +- .../src/plugins/enhancer/model-meta/index.ts | 4 + .../enhancer/policy/policy-guard-generator.ts | 10 +- packages/schema/src/plugins/plugin-utils.ts | 3 + packages/schema/src/plugins/prisma/index.ts | 94 +- .../src/plugins/prisma/schema-generator.ts | 65 +- packages/schema/src/plugins/zod/generator.ts | 815 +++++++++--------- packages/schema/src/plugins/zod/index.ts | 5 +- .../schema/src/plugins/zod/transformer.ts | 50 +- packages/schema/src/plugins/zod/types.ts | 2 - packages/schema/src/telemetry.ts | 6 +- packages/sdk/src/model-meta-generator.ts | 20 + packages/sdk/src/prisma.ts | 61 +- packages/sdk/src/types.ts | 45 +- packages/sdk/src/utils.ts | 2 +- packages/testtools/src/schema.ts | 20 +- ...rphism.test.ts => enhanced-client.test.ts} | 49 +- .../with-delegate/plugin-interaction.test.ts | 25 + ...icy.test.ts => policy-interaction.test.ts} | 0 .../tests/enhancements/with-delegate/utils.ts | 47 + 37 files changed, 961 insertions(+), 757 deletions(-) create mode 100644 packages/runtime/res/prisma.d.ts create mode 100644 packages/runtime/src/prisma.d.ts delete mode 100644 packages/schema/src/plugins/enhancer/delegate/index.ts rename tests/integration/tests/enhancements/with-delegate/{polymorphism.test.ts => enhanced-client.test.ts} (97%) create mode 100644 tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts rename tests/integration/tests/enhancements/with-delegate/{policy.test.ts => policy-interaction.test.ts} (100%) create mode 100644 tests/integration/tests/enhancements/with-delegate/utils.ts diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index 1a46fa528..e033a5206 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,5 +1,5 @@ import type { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; +import { PluginError, PluginOptions, PluginResult, getDataModels, hasAttribute } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import semver from 'semver'; @@ -12,7 +12,7 @@ export abstract class OpenAPIGeneratorBase { 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')); 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..0bb76251e 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -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 { diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index c551a8aef..8aa9189d0 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -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 { diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index 3a47a1c87..ca84c101c 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -49,11 +49,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, legacyMutations, options); }); await saveProject(project); - return warnings; + return { warnings }; } function generateModelHooks( @@ -61,14 +61,15 @@ function generateModelHooks( outDir: string, model: DataModel, mapping: DMMF.ModelMapping, - legacyMutations: boolean + 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, 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/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index a6cd75a5c..4e3079db8 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -55,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( @@ -286,7 +286,8 @@ function generateModelHooks( project: Project, outDir: string, model: DataModel, - mapping: DMMF.ModelMapping + mapping: DMMF.ModelMapping, + options: PluginOptions ) { const modelNameCap = upperCaseFirst(model.name); const prismaVersion = getPrismaVersion(); @@ -295,7 +296,7 @@ function generateModelHooks( sf.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(model.$container, outDir); + const prismaImport = getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma', model.name], isTypeOnly: true, 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/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 0d252cabc..3487386a6 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -72,7 +72,8 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. generateModelActions, generateClientHelpers, model, - zodSchemasImport + zodSchemasImport, + options ); createHelper(outDir); @@ -86,7 +87,8 @@ function createAppRouter( generateModelActions: string[] | undefined, generateClientHelpers: string[] | undefined, zmodel: Model, - zodSchemasImport: string + zodSchemasImport: string, + options: PluginOptions ) { const indexFile = path.resolve(outDir, 'routers', `index.ts`); const appRouter = project.createSourceFile(indexFile, undefined, { @@ -95,7 +97,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 +171,8 @@ function createAppRouter( outDir, generateModelActions, generateClientHelpers, - zmodel, - zodSchemasImport + zodSchemasImport, + options ); appRouter.addImportDeclaration({ @@ -239,8 +241,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 +260,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 54aec3ecb..62e2efafd 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, getPrismaClientImportSpec, type PluginOptions } from '@zenstackhq/sdk'; 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/runtime/package.json b/packages/runtime/package.json index 8292bcb7c..34f6ea0ed 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -46,6 +46,9 @@ "import": "./cross/index.mjs", "require": "./cross/index.js", "default": "./cross/index.js" + }, + "./prisma": { + "types": "./prisma.d.ts" } }, "publishConfig": { diff --git a/packages/runtime/res/prisma.d.ts b/packages/runtime/res/prisma.d.ts new file mode 100644 index 000000000..0068ce7ae --- /dev/null +++ b/packages/runtime/res/prisma.d.ts @@ -0,0 +1 @@ +export type * from '.zenstack/prisma'; diff --git a/packages/runtime/src/prisma.d.ts b/packages/runtime/src/prisma.d.ts new file mode 100644 index 000000000..c01cbe743 --- /dev/null +++ b/packages/runtime/src/prisma.d.ts @@ -0,0 +1,2 @@ +// @ts-expect-error stub for re-exporting PrismaClient +export type * from '.zenstack/prisma'; diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 3e73932a1..b07a592f3 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -3,23 +3,25 @@ import type { DMMF } from '@prisma/generator-helper'; import { isPlugin, Model, Plugin } from '@zenstackhq/language/ast'; import { + createProject, + emitProject, getDataModels, - getDMMF, getLiteral, getLiteralArray, hasValidationAttributes, - OptionValue, - PluginDeclaredOptions, PluginError, - PluginFunction, resolvePath, + saveProject, + type OptionValue, + type PluginDeclaredOptions, + type PluginFunction, + type PluginResult, } from '@zenstackhq/sdk'; import colors from 'colors'; -import fs from 'fs'; import ora from 'ora'; import path from 'path'; +import type { Project } from 'ts-morph'; import { CorePlugins, ensureDefaultOutputFolder } from '../plugins/plugin-utils'; -import { getDefaultPrismaOutputFile } from '../plugins/prisma/schema-generator'; import telemetry from '../telemetry'; import { getVersion } from '../utils/version-utils'; @@ -57,8 +59,6 @@ export class PluginRunner { const plugins: PluginInfo[] = []; const pluginDecls = runnerOptions.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - let prismaOutput = getDefaultPrismaOutputFile(runnerOptions.schemaPath); - for (const pluginDecl of pluginDecls) { const pluginProvider = this.getPluginProvider(pluginDecl); if (!pluginProvider) { @@ -103,15 +103,11 @@ export class PluginRunner { run: pluginModule.default as PluginFunction, module: pluginModule, }); - - if (pluginProvider === '@core/prisma' && typeof pluginOptions.output === 'string') { - // record custom prisma output path - prismaOutput = resolvePath(pluginOptions.output, { schemaPath: runnerOptions.schemaPath }); - } } // calculate all plugins (including core plugins implicitly enabled) - const allPlugins = this.calculateAllPlugins(runnerOptions, plugins); + const { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, plugins); + const allPlugins = [...corePlugins, ...userPlugins]; // check dependencies for (const plugin of allPlugins) { @@ -133,22 +129,38 @@ export class PluginRunner { const warnings: string[] = []; + // run core plugins first let dmmf: DMMF.Document | undefined = undefined; - for (const { name, description, provider, run, options: pluginOptions } of allPlugins) { - // const start = Date.now(); - await this.runPlugin(name, description, run, runnerOptions, 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.`); } @@ -168,7 +180,22 @@ export class PluginRunner { const hasValidation = this.hasValidation(options.schema); - // 2. @core/zod + // 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, + }) + ); + } + } + + // 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 @@ -178,7 +205,7 @@ export class PluginRunner { if ( !corePlugins.some((p) => p.provider === CorePlugins.Zod) && - (options.defaultPlugins || plugins.some((p) => p.provider === CorePlugins.Enhancer)) && + (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 @@ -186,21 +213,6 @@ export class PluginRunner { corePlugins.push(this.makeCorePlugin(CorePlugins.Zod, options.schemaPath, { modelOnly: true })); } - // 3. @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, - }) - ); - } - } - // collect core plugins introduced by dependencies plugins.forEach((plugin) => { // TODO: generalize this @@ -245,7 +257,7 @@ export class PluginRunner { } }); - return [...corePlugins, ...plugins]; + return { corePlugins, userPlugins: plugins }; } private makeCorePlugin( @@ -296,12 +308,12 @@ export class PluginRunner { runnerOptions: PluginRunnerOptions, options: PluginDeclaredOptions, dmmf: DMMF.Document | undefined, - warnings: string[] + project: Project ) { 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', @@ -310,19 +322,20 @@ export class PluginRunner { options, }, async () => { - let result = run(runnerOptions.schema, { ...options, schemaPath: runnerOptions.schemaPath }, 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; @@ -350,3 +363,13 @@ export class PluginRunner { 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/plugins/enhancer/delegate/index.ts b/packages/schema/src/plugins/enhancer/delegate/index.ts deleted file mode 100644 index d3f85576d..000000000 --- a/packages/schema/src/plugins/enhancer/delegate/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type PluginOptions } from '@zenstackhq/sdk'; -import type { Model } from '@zenstackhq/sdk/ast'; -import type { Project } from 'ts-morph'; -import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; -import path from 'path'; - -export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { - const prismaGenerator = new PrismaSchemaGenerator(model); - await prismaGenerator.generate({ - provider: '@internal', - schemaPath: options.schemaPath, - output: path.join(outDir, 'delegate.prisma'), - overrideClientGenerationPath: path.join(outDir, '.delegate'), - mode: 'logical', - }); -} diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 06caf6950..9488b24f7 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -1,12 +1,16 @@ +import type { DMMF } from '@prisma/generator-helper'; import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { getAttribute, getDataModels, + getDMMF, getPrismaClientImportSpec, isDelegateModel, + PluginError, type PluginOptions, } from '@zenstackhq/sdk'; import { DataModel, DataModelField, isDataModel, isReferenceExpr, type Model } from '@zenstackhq/sdk/ast'; +import fs from 'fs'; import path from 'path'; import { FunctionDeclarationStructure, @@ -19,31 +23,50 @@ import { TypeAliasDeclaration, VariableStatement, } from 'ts-morph'; +import { name } from '..'; +import { execPackage } from '../../../utils/exec-utils'; +import { trackPrismaSchemaError } from '../../prisma'; import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; // information of delegate models and their sub models type DelegateInfo = [DataModel, DataModel[]][]; export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { - const outFile = path.join(outDir, 'enhance.ts'); let logicalPrismaClientDir: string | undefined; + let dmmf: DMMF.Document | undefined; if (hasDelegateModel(model)) { - logicalPrismaClientDir = await generateLogicalPrisma(model, options, outDir); + // schema contains delegate models, need to generate a logical prisma schema + const result = await generateLogicalPrisma(model, options, outDir); + + logicalPrismaClientDir = './.logical-prisma-client'; + dmmf = result.dmmf; + + // create a reexport of the logical prisma client + const prismaDts = project.createSourceFile( + path.join(outDir, 'prisma.d.ts'), + `export type * from '${logicalPrismaClientDir}/index-fixed';`, + { overwrite: true } + ); + await saveSourceFile(prismaDts, options); + } else { + // just reexport the prisma client + const prismaDts = project.createSourceFile( + path.join(outDir, 'prisma.d.ts'), + `export type * from '${getPrismaClientImportSpec(outDir, options)}';`, + { overwrite: true } + ); + await saveSourceFile(prismaDts, options); } - project.createSourceFile( - outFile, + const enhanceTs = project.createSourceFile( + path.join(outDir, 'enhance.ts'), `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; import modelMeta from './model-meta'; import policy from './policy'; ${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} -import { Prisma } from '${getPrismaClientImportSpec(model, outDir)}'; -${ - logicalPrismaClientDir - ? `import type { PrismaClient as EnhancedPrismaClient } from '${logicalPrismaClientDir}/index-fixed';` - : '' -} +import { Prisma } from '${getPrismaClientImportSpec(outDir, options)}'; +${logicalPrismaClientDir ? `import { type PrismaClient } from '${logicalPrismaClientDir}/index-fixed';` : ``} export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions) { return createEnhancement(prisma, { @@ -52,11 +75,15 @@ export function enhance(prisma: DbClient, context?: Enh zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), prismaModule: Prisma, ...options - }, context)${logicalPrismaClientDir ? ' as EnhancedPrismaClient' : ''}; + }, context)${logicalPrismaClientDir ? ' as PrismaClient' : ''}; } `, { overwrite: true } ); + + await saveSourceFile(enhanceTs, options); + + return { dmmf }; } function hasDelegateModel(model: Model) { @@ -68,19 +95,40 @@ function hasDelegateModel(model: Model) { async function generateLogicalPrisma(model: Model, options: PluginOptions, outDir: string) { const prismaGenerator = new PrismaSchemaGenerator(model); - const prismaClientOutDir = './.delegate'; + const prismaClientOutDir = './.logical-prisma-client'; + const logicalPrismaFile = path.join(outDir, 'logical.prisma'); await prismaGenerator.generate({ provider: '@internal', // doesn't matter schemaPath: options.schemaPath, - output: path.join(outDir, 'delegate.prisma'), + 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"`); + } + // make a bunch of typing fixes to the generated prisma client await processClientTypes(model, path.join(outDir, prismaClientOutDir)); - return prismaClientOutDir; + return { + prismaSchema: logicalPrismaFile, + // load the dmmf of the logical prisma schema + dmmf: await getDMMF({ datamodel: fs.readFileSync(logicalPrismaFile, { encoding: 'utf-8' }) }), + }; } async function processClientTypes(model: Model, prismaClientDir: string) { @@ -106,8 +154,7 @@ async function processClientTypes(model: Model, prismaClientDir: string) { }); transform(sf, sfNew, delegateInfo); sfNew.formatText(); - - await project.save(); + await sfNew.save(); } function transform(sf: SourceFile, sfNew: SourceFile, delegateModels: DelegateInfo) { @@ -352,3 +399,9 @@ function getDiscriminatorFieldsRecursively(delegate: DataModel, result: DataMode } return result; } + +async function saveSourceFile(sf: SourceFile, options: PluginOptions) { + if (options.preserveTsFiles) { + await sf.save(); + } +} diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index 86e3ecf39..64e2ad1a4 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -1,11 +1,5 @@ -import { - PluginError, - createProject, - emitProject, - resolvePath, - saveProject, - type PluginFunction, -} from '@zenstackhq/sdk'; +import { PluginError, createProject, resolvePath, type PluginFunction, RUNTIME_PACKAGE } from '@zenstackhq/sdk'; +import path from 'path'; import { getDefaultOutputFolder } from '../plugin-utils'; import { generate as generateEnhancer } from './enhance'; import { generate as generateModelMeta } from './model-meta'; @@ -15,34 +9,33 @@ export const name = 'Prisma Enhancer'; export const description = 'Generating PrismaClient enhancer'; const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { - let ourDir = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); - if (!ourDir) { + let outDir = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); + if (!outDir) { throw new PluginError(name, `Unable to determine output path, not running plugin`); } - ourDir = resolvePath(ourDir, options); - - const project = createProject(); - - await generateModelMeta(model, options, project, ourDir); - await generatePolicy(model, options, project, ourDir); - await generateEnhancer(model, options, project, ourDir); - - let shouldCompile = true; - if (typeof options.compile === 'boolean') { - // explicit override - shouldCompile = options.compile; - } else if (globalOptions) { - // from CLI or config file - shouldCompile = globalOptions.compile; + outDir = resolvePath(outDir, options); + + const project = globalOptions?.tsProject ?? createProject(); + + await generateModelMeta(model, options, project, outDir); + await generatePolicy(model, options, project, outDir); + const { dmmf } = await generateEnhancer(model, options, project, outDir); + + let prismaClientPath: string | undefined; + if (dmmf) { + // a logical client is generated + if (typeof options.output === 'string') { + // get the absolute path of the logical prisma client + const prismaClientPathAbs = path.resolve(options.output, 'prisma'); + + // resolve it relative to the schema path + prismaClientPath = path.relative(path.dirname(options.schemaPath), prismaClientPathAbs); + } else { + prismaClientPath = `${RUNTIME_PACKAGE}/prisma`; + } } - if (!shouldCompile || options.preserveTsFiles === true) { - await saveProject(project); - } - - if (shouldCompile) { - await emitProject(project); - } + 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 index 541106e24..9939ae346 100644 --- a/packages/schema/src/plugins/enhancer/model-meta/index.ts +++ b/packages/schema/src/plugins/enhancer/model-meta/index.ts @@ -6,8 +6,12 @@ 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/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index 2032f2b99..fa1eb831a 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -60,7 +60,7 @@ import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; * Generates source file that contains Prisma query guard objects used for injecting database queries */ export class PolicyGenerator { - async generate(project: Project, model: Model, _options: PluginOptions, output: string) { + 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 */'); @@ -75,7 +75,7 @@ export 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 }], @@ -140,6 +140,12 @@ export class PolicyGenerator { }); sf.addStatements('export default policy'); + + // save ts files if requested explicitly or the user provided + const preserveTsFiles = options.preserveTsFiles === true || !!options.output; + if (preserveTsFiles) { + await sf.save(); + } } // Generates a { select: ... } object to select `auth()` fields used in policy rules diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 00b806e7e..d6cc12403 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -55,6 +55,9 @@ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { types: './zod/objects/index.d.ts', default: './zod/objects/index.js', }, + './prisma': { + types: './prisma.d.ts', + }, }, }; fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkgJson, undefined, 4)); diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 5aa64c145..478b6a54b 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,11 +1,101 @@ -import { PluginFunction } from '@zenstackhq/sdk'; +import { PluginError, PluginFunction, getDMMF, getLiteral, resolvePath } from '@zenstackhq/sdk'; +import { GeneratorDecl, isGeneratorDecl } from '@zenstackhq/sdk/ast'; +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(model).generate(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/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 4ac78c6e3..2519c3cd3 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -34,7 +34,6 @@ import { getIdFields } from '../../utils/ast-utils'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { getAttribute, - getDMMF, getLiteral, getPrismaVersion, isAuthInvocation, @@ -43,7 +42,6 @@ import { PluginError, PluginOptions, resolved, - resolvePath, ZModelCodeGenerator, } from '@zenstackhq/sdk'; import fs from 'fs'; @@ -52,13 +50,10 @@ import { streamAst } from 'langium'; 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 { AttributeArgValue, ModelFieldType, @@ -100,6 +95,11 @@ export class PrismaSchemaGenerator { 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'; @@ -134,10 +134,6 @@ export class PrismaSchemaGenerator { } } - 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 }); } @@ -152,42 +148,9 @@ export class PrismaSchemaGenerator { } } - const generateClient = options.generateClient !== false; - - if (generateClient) { - let generateCmd = `prisma generate --schema "${outFile}"${this.mode === 'logical' ? ' --no-engine' : ''}`; - if (typeof options.generateArgs === 'string') { - generateCmd += ` ${options.generateArgs}`; - } - try { - // run 'prisma generate' - await execPackage(generateCmd, { stdio: '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, @@ -693,21 +656,3 @@ export class PrismaSchemaGenerator { function isDescendantOf(model: DataModel, superModel: DataModel): boolean { return model.superTypes.some((s) => s.ref === superModel || isDescendantOf(s.ref!, superModel)); } - -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 }); -} diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index a09c4ad73..f09e93951 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -2,8 +2,6 @@ import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { PluginGlobalOptions, PluginOptions, - createProject, - emitProject, getDataModels, getLiteral, getPrismaClientImportSpec, @@ -13,14 +11,13 @@ import { isFromStdlib, parseOptionAsStrings, resolvePath, - saveProject, } from '@zenstackhq/sdk'; import { DataModel, DataSource, EnumField, Model, isDataModel, isDataSource, isEnum } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; import { promises as fs } from 'fs'; 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'; @@ -28,480 +25,490 @@ import Transformer from './transformer'; import removeDir from './utils/removeDir'; import { getFieldSchemaDefault, makeFieldSchema, makeValidationRefinements } 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'; +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); + 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); + await this.handleGeneratorOutputValue(output); - const prismaClientDmmf = dmmf; + // calculate the models to be excluded + const excludeModels = this.getExcludedModels(); - const modelOperations = prismaClientDmmf.mappings.modelOperations.filter( - (o) => !excludeModels.find((e) => e === o.model) - ); + const prismaClientDmmf = this.dmmf; - // 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())) - ); + const modelOperations = prismaClientDmmf.mappings.modelOperations.filter( + (o) => !excludeModels.find((e) => e === o.model) + ); - const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter( - (m) => !excludeModels.find((e) => e === m.name) - ); + // 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())) + ); - // whether Prisma's Unchecked* series of input types should be generated - const generateUnchecked = options.noUncheckedInput !== true; + const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter( + (m) => !excludeModels.find((e) => e === m.name) + ); - const project = createProject(); + // common schemas + await this.generateCommonSchemas(output); - // common schemas - await generateCommonSchemas(project, output); + // enums + await this.generateEnumSchemas( + prismaClientDmmf.schema.enumTypes.prisma, + prismaClientDmmf.schema.enumTypes.model ?? [] + ); - // enums - await generateEnumSchemas( - prismaClientDmmf.schema.enumTypes.prisma, - prismaClientDmmf.schema.enumTypes.model ?? [], - project, - model - ); + const dataSource = this.model.declarations.find((d): d is DataSource => isDataSource(d)); - const dataSource = model.declarations.find((d): d is DataSource => isDataSource(d)); + const dataSourceProvider = getLiteral( + dataSource?.fields.find((f) => f.name === 'provider')?.value + ) as ConnectorType; - 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 !== true) { + // detailed object schemas referenced from input schemas + Transformer.provider = dataSourceProvider; + addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models); + const aggregateOperationSupport = resolveAggregateOperationSupport(inputObjectTypes); + await this.generateObjectSchemas(inputObjectTypes, output); - 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); + // input schemas + const transformer = new Transformer({ + models, + modelOperations, + aggregateOperationSupport, + project: this.project, + inputObjectTypes, + }); + await transformer.generateInputSchemas(this.options); + this.sourceFiles.push(...transformer.sourceFiles); + } - // input schemas - const transformer = new Transformer({ - models, - modelOperations, - aggregateOperationSupport, - project, - zmodel: model, - inputObjectTypes, - }); - await transformer.generateInputSchemas(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 }) + ); - // 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; + 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(); + }) + ); + } } - if (!shouldCompile || options.preserveTsFiles === true) { - // save ts files - await saveProject(project); + private get project() { + return this.globalOptions.tsProject; } - if (shouldCompile) { - await emitProject(project); - } -} -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); - } - }); - } + 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); + } + }); + } - // finally find the models to be excluded - return getDataModels(model) - .filter((dm) => !included.has(dm)) - .map((m) => m.name); + // finally find the models to be excluded + return getDataModels(this.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 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); -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 } - ); -} + Transformer.setOutputPath(output); + } -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 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 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 transformer = new Transformer({ name, fields, project, zmodel, inputObjectTypes }); - const moduleName = transformer.generateObjectSchema(generateUnchecked); - moduleNames.push(moduleName); + 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); } - 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)); + 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; + + 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); } + + 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) + ); - writer.writeLine('/* eslint-disable */'); - writer.writeLine(`import { z } from 'zod';`); + const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref)); + const fkFields = model.fields.filter((field) => isForeignKeyField(field)); - // 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); + 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); + } } } - } - 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 + writer.write(`const baseSchema = z.object(`); writer.inlineBlock(() => { - [...relations, ...fkFields].forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); + scalarFields.forEach((field) => { + writer.writeLine(`${field.name}: ${makeFieldSchema(field, true)},`); }); }); 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 || fkFields.length > 0) { + relationSchema = 'relationSchema'; + writer.write(`const ${relationSchema} = z.object(`); + writer.inlineBlock(() => { + [...relations, ...fkFields].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( + `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 + //////////////////////////////////////////////// + const fieldsWithoutDefault = scalarFields.filter((f) => !getFieldSchemaDefault(f)); + // mark fields without default value as optional + let modelSchema = this.makePartial( + 'baseSchema', + fieldsWithoutDefault.length < scalarFields.length ? fieldsWithoutDefault.map((f) => f.name) : undefined ); - } - if (relationSchema) { - // export schema with only scalar fields - const modelScalarSchema = `${upperCaseFirst(model.name)}ScalarSchema`; - writer.writeLine(`export const ${modelScalarSchema} = ${modelSchema};`); - modelSchema = modelScalarSchema; + // omit fields + const fieldsToOmit = scalarFields.filter((field) => hasAttribute(field, '@omit')); + if (fieldsToOmit.length > 0) { + modelSchema = this.makeOmit( + modelSchema, + fieldsToOmit.map((f) => f.name) + ); + } - // merge relations - modelSchema = makeMerge(modelSchema, makePartial(relationSchema)); - } + if (relationSchema) { + // export schema with only scalar fields + const modelScalarSchema = `${upperCaseFirst(model.name)}ScalarSchema`; + writer.writeLine(`export const ${modelScalarSchema} = ${modelSchema};`); + modelSchema = modelScalarSchema; - // 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 relations + modelSchema = this.makeMerge(modelSchema, this.makePartial(relationSchema)); + } - //////////////////////////////////////////////// - // 2. Prisma create & update - //////////////////////////////////////////////// + // 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};`); - // 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) + //////////////////////////////////////////////// + // 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(`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(`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 = this.makePartial( + createSchema, + fieldsWithDefault.map((f) => f.name) + ); + } - if (fkSchema) { - // export schema with only scalar fields - const createScalarSchema = `${upperCaseFirst(model.name)}CreateScalarSchema`; - writer.writeLine(`export const ${createScalarSchema} = ${createSchema};`); + if (fkSchema) { + // export schema with only scalar fields + const createScalarSchema = `${upperCaseFirst(model.name)}CreateScalarSchema`; + writer.writeLine(`export const ${createScalarSchema} = ${createSchema};`); - // merge fk fields - createSchema = makeMerge(createScalarSchema, fkSchema); - } + // merge fk fields + createSchema = this.makeMerge(createScalarSchema, fkSchema); + } - 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};`); + 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};`); - //////////////////////////////////////////////// - // 3. Update schema - //////////////////////////////////////////////// - let updateSchema = makePartial('baseSchema'); + //////////////////////////////////////////////// + // 3. Update schema + //////////////////////////////////////////////// + let updateSchema = this.makePartial('baseSchema'); - if (fkSchema) { - // export schema with only scalar fields - const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; - writer.writeLine(`export const ${updateScalarSchema} = ${updateSchema};`); - updateSchema = updateScalarSchema; + if (fkSchema) { + // export schema with only scalar fields + const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; + writer.writeLine(`export const ${updateScalarSchema} = ${updateSchema};`); + updateSchema = updateScalarSchema; - // merge fk fields - updateSchema = makeMerge(updateSchema, makePartial(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 + const noRefineSchema = `${upperCaseFirst(model.name)}UpdateWithoutRefineSchema`; + writer.writeLine(`export const ${noRefineSchema} = ${updateSchema};`); + updateSchema = `${refineFuncName}(${noRefineSchema})`; + } + writer.writeLine(`export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema};`); + }); - return schemaName; -} + 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 53a30b4e3..ffe198378 100644 --- a/packages/schema/src/plugins/zod/index.ts +++ b/packages/schema/src/plugins/zod/index.ts @@ -1,13 +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 878eff82b..0471dec3f 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,12 +1,11 @@ /* 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 { getPrismaClientImportSpec, getPrismaVersion, type PluginOptions } from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; import { indentString } from '@zenstackhq/sdk/utils'; 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'; @@ -27,8 +26,8 @@ export default class Transformer { private hasJson = false; private hasDecimal = false; private project: Project; - private zmodel: Model; private inputObjectTypes: DMMF.InputType[]; + public sourceFiles: SourceFile[] = []; constructor(params: TransformerParams) { this.originalName = params.name ?? ''; @@ -39,7 +38,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 +57,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 +79,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`; } @@ -254,12 +257,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 +288,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 +387,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 +427,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; let imports = [ `import { z } from 'zod'`, - this.generateImportPrismaStatement(), + this.generateImportPrismaStatement(options), selectImport, includeImport, ]; @@ -666,7 +672,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 +680,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..e71b3b03a 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 { Project } from 'ts-morph'; export type TransformerParams = { @@ -12,7 +11,6 @@ export type TransformerParams = { isDefaultPrismaClientOutput?: boolean; prismaClientOutputPath?: string; project: Project; - zmodel: Model; inputObjectTypes: DMMF.InputType[]; }; diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index 9cd8ba386..45983886d 100644 --- a/packages/schema/src/telemetry.ts +++ b/packages/schema/src/telemetry.ts @@ -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/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index cd516f5ec..8adf42c4c 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -34,9 +34,24 @@ import { TypeScriptExpressionTransformer, } from '.'; +/** + * Options for generating model metadata + */ export type ModelMetaGeneratorOptions = { + /** + * Output directory + */ output: string; + + /** + * 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) { @@ -49,6 +64,11 @@ export async function generate(project: Project, models: DataModel[], options: M ], }); sf.addStatements('export default metadata;'); + + if (options.preserveTsFiles) { + await sf.save(); + } + return sf; } diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index 77db556b4..19b836cc2 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -3,62 +3,37 @@ import type { DMMF } from '@prisma/generator-helper'; import path from 'path'; import * as semver from 'semver'; -import { GeneratorDecl, Model, Plugin, isGeneratorDecl, isPlugin } from './ast'; -import { getLiteral } from './utils'; +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)) { + 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) { diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 9fbbd5553..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 @@ -19,7 +20,17 @@ export type PluginDeclaredOptions = { /** * Plugin configuration options for execution */ -export type PluginOptions = { schemaPath: string } & PluginDeclaredOptions; +export type PluginOptions = { + /** + * ZModel schema absolute path + */ + schemaPath: string; + + /** + * PrismaClient import path, either relative to `schemaPath` or absolute + */ + prismaClientPath?: string; +} & PluginDeclaredOptions; /** * Global options that apply to all plugins @@ -34,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; }; /** @@ -42,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/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 01d5d274d..f73d2e12c 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -285,7 +285,7 @@ export function resolvePath(_path: string, options: Pick { - const 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[] -} -`; + const schema = POLYMORPHIC_SCHEMA; async function setup() { const { enhance } = await loadSchema(schema, { logPrismaQuery: true, 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.test.ts b/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts similarity index 100% rename from tests/integration/tests/enhancements/with-delegate/policy.test.ts rename to tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts 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..0de8a7e8b --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/utils.ts @@ -0,0 +1,47 @@ +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[] +} +`; From 2e81a089a1b57ebf61d25fc49300fa22f0cda06b Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 6 Mar 2024 15:44:17 -0800 Subject: [PATCH 045/127] fix(polymorphism): support `orderBy` with base fields (#1086) --- packages/runtime/src/enhancements/delegate.ts | 34 +++++++++++------- tests/integration/tests/cli/plugins.test.ts | 1 + .../with-delegate/enhanced-client.test.ts | 35 +++++++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index 7032a965a..06a96b0c6 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -77,6 +77,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { 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)}`); } @@ -126,19 +131,19 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { }); } - private buildWhereHierarchy(where: any) { + 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, this.model, field); + const fieldInfo = resolveField(this.options.modelMeta, model, field); if (!fieldInfo?.inheritedFrom) { return; } - let base = this.getBaseModel(this.model); + let base = this.getBaseModel(model); let target = where; while (base) { @@ -173,12 +178,17 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { for (const kind of ['select', 'include'] as const) { if (args[kind] && typeof args[kind] === 'object') { - for (const [field, value] of Object.entries(args[kind])) { - if (value !== undefined) { + 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 { - const fieldInfo = resolveField(this.options.modelMeta, model, field); if (fieldInfo && this.isDelegateOrDescendantOfDelegate(fieldInfo.type)) { let nextValue = value; if (nextValue === true) { @@ -847,15 +857,15 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { args = deepcopy(args); if (args.cursor) { - args.cursor = this.buildWhereHierarchy(args.cursor); + args.cursor = this.buildWhereHierarchy(this.model, args.cursor); } if (args.orderBy) { - args.orderBy = this.buildWhereHierarchy(args.orderBy); + args.orderBy = this.buildWhereHierarchy(this.model, args.orderBy); } if (args.where) { - args.where = this.buildWhereHierarchy(args.where); + args.where = this.buildWhereHierarchy(this.model, args.where); } if (this.options.logPrismaQuery) { @@ -875,11 +885,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { args = deepcopy(args); if (args?.cursor) { - args.cursor = this.buildWhereHierarchy(args.cursor); + args.cursor = this.buildWhereHierarchy(this.model, args.cursor); } if (args?.where) { - args.where = this.buildWhereHierarchy(args.where); + args.where = this.buildWhereHierarchy(this.model, args.where); } if (this.options.logPrismaQuery) { @@ -915,7 +925,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { args = deepcopy(args); if (args.where) { - args.where = this.buildWhereHierarchy(args.where); + args.where = this.buildWhereHierarchy(this.model, args.where); } if (this.options.logPrismaQuery) { diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 36d6dd1a9..4d28c8e2a 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -116,6 +116,7 @@ describe('CLI Plugins Tests', () => { strict: true, lib: ['esnext', 'dom'], esModuleInterop: true, + skipLibCheck: true, }, }) ); diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index b4f273be5..5a171aa8b 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -235,6 +235,41 @@ describe('Polymorphism Test', () => { expect(imgAsset.owner).toMatchObject(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(); From 36e515e485c580657b9edbfc52014f3542abfb96 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 6 Mar 2024 21:09:41 -0800 Subject: [PATCH 046/127] fix: several issues with using `auth()` in `@default` (#1088) --- packages/runtime/src/cross/model-meta.ts | 9 +- .../runtime/src/enhancements/default-auth.ts | 46 +++++++++- .../src/enhancements/policy/handler.ts | 21 +---- packages/runtime/src/enhancements/utils.ts | 19 +++++ .../src/plugins/enhancer/enhance/index.ts | 26 +++++- .../src/plugins/enhancer/enhancer-utils.ts | 20 +++++ .../src/plugins/prisma/schema-generator.ts | 38 +++++---- .../validation/attribute-validation.test.ts | 26 +----- packages/sdk/src/model-meta-generator.ts | 7 +- packages/sdk/src/utils.ts | 39 +++++++++ packages/testtools/src/schema.ts | 6 ++ .../enhancements/with-policy/auth.test.ts | 83 +++++++++++++++++++ 12 files changed, 271 insertions(+), 69 deletions(-) create mode 100644 packages/schema/src/plugins/enhancer/enhancer-utils.ts diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts index 9f767af0e..efa4d1a03 100644 --- a/packages/runtime/src/cross/model-meta.ts +++ b/packages/runtime/src/cross/model-meta.ts @@ -75,7 +75,14 @@ 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; diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts index bbbd35861..78294f28b 100644 --- a/packages/runtime/src/enhancements/default-auth.ts +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -2,10 +2,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import deepcopy from 'deepcopy'; -import { FieldInfo, NestedWriteVisitor, PrismaWriteActionType, enumerate, getFields } from '../cross'; +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. @@ -68,7 +69,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { const authDefaultValue = this.getDefaultValueFromAuth(fieldInfo); if (authDefaultValue !== undefined) { // set field value extracted from `auth()` - data[fieldInfo.name] = authDefaultValue; + this.setAuthDefaultValue(fieldInfo, model, data, authDefaultValue); } } }; @@ -90,6 +91,47 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { return newArgs; } + private setAuthDefaultValue(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) { + 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`); diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index f31e145f9..f2bc4ad07 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -21,7 +21,7 @@ import type { EnhancementContext, InternalEnhancementOptions } from '../create-e import { Logger } from '../logger'; import { PrismaProxyHandler } from '../proxy'; import { QueryUtils } from '../query-utils'; -import { formatObject, prismaClientValidationError } from '../utils'; +import { formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils'; import { PolicyUtil } from './policy-utils'; import { createDeferredPromise } from './promise'; @@ -691,7 +691,7 @@ 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.policyUtils.buildReversedQuery(context, true, unsafe); @@ -1083,23 +1083,6 @@ 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 (field && (this.isAutoIncrementIdField(field) || field.isForeignKey)) { - return true; - } - } - return false; - } - - private isAutoIncrementIdField(field: FieldInfo) { - return field.isId && field.isAutoIncrement; - } - async updateMany(args: any) { if (!args) { throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index ba2f9a2d8..9bc7ce0bc 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -1,4 +1,5 @@ import * as util from 'util'; +import { FieldInfo, ModelMeta, resolveField } from '..'; import type { DbClientContract } from '../types'; /** @@ -22,3 +23,21 @@ export function prismaClientKnownRequestError(prisma: DbClientContract, prismaMo export function prismaClientUnknownRequestError(prismaModule: any, ...args: unknown[]): Error { throw new prismaModule.PrismaClientUnknownRequestError(...args); } + +// 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; + } + } + return false; +} + +export function isAutoIncrementIdField(field: FieldInfo) { + return field.isId && field.isAutoIncrement; +} diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 9488b24f7..63845ba1c 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -27,6 +27,7 @@ import { name } from '..'; import { execPackage } from '../../../utils/exec-utils'; import { trackPrismaSchemaError } from '../../prisma'; import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; +import { isDefaultWithAuth } from '../enhancer-utils'; // information of delegate models and their sub models type DelegateInfo = [DataModel, DataModel[]][]; @@ -35,7 +36,7 @@ export async function generate(model: Model, options: PluginOptions, project: Pr let logicalPrismaClientDir: string | undefined; let dmmf: DMMF.Document | undefined; - if (hasDelegateModel(model)) { + if (needsLogicalClient(model)) { // schema contains delegate models, need to generate a logical prisma schema const result = await generateLogicalPrisma(model, options, outDir); @@ -86,6 +87,10 @@ export function enhance(prisma: DbClient, context?: Enh return { dmmf }; } +function needsLogicalClient(model: Model) { + return hasDelegateModel(model) || hasAuthInDefault(model); +} + function hasDelegateModel(model: Model) { const dataModels = getDataModels(model); return dataModels.some( @@ -93,6 +98,12 @@ function hasDelegateModel(model: Model) { ); } +function hasAuthInDefault(model: Model) { + return getDataModels(model).some((dm) => + dm.fields.some((f) => f.attributes.some((attr) => isDefaultWithAuth(attr))) + ); +} + async function generateLogicalPrisma(model: Model, options: PluginOptions, outDir: string) { const prismaGenerator = new PrismaSchemaGenerator(model); const prismaClientOutDir = './.logical-prisma-client'; @@ -152,12 +163,19 @@ async function processClientTypes(model: Model, prismaClientDir: string) { const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { overwrite: true, }); - transform(sf, sfNew, delegateInfo); - sfNew.formatText(); + + if (delegateInfo.length > 0) { + // transform types for delegated models + transformDelegate(sf, sfNew, delegateInfo); + sfNew.formatText(); + } else { + // just copy + sfNew.replaceWithText(sf.getFullText()); + } await sfNew.save(); } -function transform(sf: SourceFile, sfNew: SourceFile, delegateModels: DelegateInfo) { +function transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateModels: DelegateInfo) { // copy toplevel imports sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); 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/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 2519c3cd3..bc63d535a 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -34,11 +34,12 @@ import { getIdFields } from '../../utils/ast-utils'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { getAttribute, + getForeignKeyFields, getLiteral, getPrismaVersion, - isAuthInvocation, isDelegateModel, isIdField, + isRelationshipField, PluginError, PluginOptions, resolved, @@ -46,7 +47,6 @@ import { } from '@zenstackhq/sdk'; import fs from 'fs'; import { writeFile } from 'fs/promises'; -import { streamAst } from 'langium'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import semver from 'semver'; @@ -54,6 +54,7 @@ import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; import { execPackage } from '../../utils/exec-utils'; +import { isDefaultWithAuth } from '../enhancer/enhancer-utils'; import { AttributeArgValue, ModelFieldType, @@ -494,10 +495,27 @@ export class PrismaSchemaGenerator { const type = new ModelFieldType(fieldType, field.type.array, field.type.optional); + if (this.mode === 'logical') { + if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { + // field has `@default` with `auth()`, it should be set optional, and the + // default value setting is handled outside Prisma + type.optional = true; + } + + if (isRelationshipField(field)) { + // if foreign key field has `@default` with `auth()`, the relation + // field should be set optional + const foreignKeyFields = getForeignKeyFields(field); + if (foreignKeyFields.some((fkField) => fkField.attributes.some((attr) => isDefaultWithAuth(attr)))) { + type.optional = true; + } + } + } + const attributes = field.attributes .filter((attr) => this.isPrismaAttribute(attr)) // `@default` with `auth()` is handled outside Prisma - .filter((attr) => !this.isDefaultWithAuth(attr)) + .filter((attr) => !isDefaultWithAuth(attr)) .filter( (attr) => // when building physical schema, exclude `@default` for id fields inherited from delegate base @@ -524,20 +542,6 @@ export class PrismaSchemaGenerator { return field.$inheritedFrom && isDelegateModel(field.$inheritedFrom); } - private 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); - } - private makeFieldAttribute(attr: DataModelFieldAttribute) { const attrName = resolved(attr.decl).name; if (attrName === FIELD_PASSTHROUGH_ATTR) { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 611f8dc60..c6d0db13b 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -227,7 +227,7 @@ describe('Attribute tests', () => { `); await loadModel(` - ${ prelude } + ${prelude} model A { id String @id x String @@ -1051,21 +1051,6 @@ describe('Attribute tests', () => { } `); - // expect( - // await loadModelWithError(` - // ${prelude} - - // model User { - // id String @id - // name String - // } - // model B { - // id String @id - // userData String @default(auth()) - // } - // `) - // ).toContain("Value is not assignable to parameter"); - expect( await loadModelWithError(` ${prelude} @@ -1185,15 +1170,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/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 8adf42c4c..3dc0f3f1e 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -32,6 +32,7 @@ import { isIdField, resolved, TypeScriptExpressionTransformer, + getRelationField, } from '.'; /** @@ -247,6 +248,11 @@ function writeFields( 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) { @@ -408,7 +414,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) { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index f73d2e12c..641446a02 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -281,6 +281,45 @@ 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; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index df72c107c..392b4af4f 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -121,6 +121,7 @@ export type SchemaLoadOptions = { getPrismaOnly?: boolean; enhancements?: EnhancementKind[]; enhanceOptions?: Partial; + extraSourceFiles?: { name: string; content: string }[]; }; const defaultOptions: SchemaLoadOptions = { @@ -246,6 +247,11 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { if (opt.compile) { console.log('Compiling...'); + + opt.extraSourceFiles?.forEach(({ name, content }) => { + fs.writeFileSync(path.join(projectRoot, name), content); + }); + run('npx tsc --init'); // add generated '.zenstack/zod' folder to typescript's search path, diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index e1fff4f73..e2655b36a 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -534,4 +534,87 @@ describe('With Policy: auth() test', () => { ); 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(); + }); }); From d3629bef459afc11c16461fb18621d2f77ac35cc Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 6 Mar 2024 22:12:06 -0800 Subject: [PATCH 047/127] fix: prisma.d.ts is not properly saved (#1090) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- .../trpc/tests/projects/t3-trpc-v10/.gitignore | 3 ++- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- .../schema/src/plugins/enhancer/enhance/index.ts | 16 ++++++++++------ packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 16 files changed, 26 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index febc93264..54769b34b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 4c8825117..23c8d0b7b 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-alpha.1" +version = "2.0.0-alpha.4" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 47dfec1b8..db2daea8d 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 0b7ea3673..315e65049 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index cb75056e7..9886151b4 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": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 3ea7a4397..5d191e2ff 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": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 7af3405d2..12652665d 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": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 1416132c4..3da2d8bf8 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": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 4b56afd1c..c21bccd72 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": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { 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/runtime/package.json b/packages/runtime/package.json index 34f6ea0ed..4dcb94daa 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 762e20089..d2113958a 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": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 63845ba1c..df14e0826 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -36,7 +36,9 @@ export async function generate(model: Model, options: PluginOptions, project: Pr let logicalPrismaClientDir: string | undefined; let dmmf: DMMF.Document | undefined; - if (needsLogicalClient(model)) { + const withLogicalClient = needsLogicalClient(model); + + if (withLogicalClient) { // schema contains delegate models, need to generate a logical prisma schema const result = await generateLogicalPrisma(model, options, outDir); @@ -49,7 +51,7 @@ export async function generate(model: Model, options: PluginOptions, project: Pr `export type * from '${logicalPrismaClientDir}/index-fixed';`, { overwrite: true } ); - await saveSourceFile(prismaDts, options); + await prismaDts.save(); } else { // just reexport the prisma client const prismaDts = project.createSourceFile( @@ -57,7 +59,7 @@ export async function generate(model: Model, options: PluginOptions, project: Pr `export type * from '${getPrismaClientImportSpec(outDir, options)}';`, { overwrite: true } ); - await saveSourceFile(prismaDts, options); + await prismaDts.save(); } const enhanceTs = project.createSourceFile( @@ -67,16 +69,18 @@ import modelMeta from './model-meta'; import policy from './policy'; ${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} import { Prisma } from '${getPrismaClientImportSpec(outDir, options)}'; -${logicalPrismaClientDir ? `import { type PrismaClient } from '${logicalPrismaClientDir}/index-fixed';` : ``} +${withLogicalClient ? `import { type PrismaClient } from '${logicalPrismaClientDir}/index-fixed';` : ``} -export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions) { +export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions)${ + withLogicalClient ? ': PrismaClient' : '' + } { return createEnhancement(prisma, { modelMeta, policy, zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), prismaModule: Prisma, ...options - }, context)${logicalPrismaClientDir ? ' as PrismaClient' : ''}; + }, context)${withLogicalClient ? ' as PrismaClient' : ''}; } `, { overwrite: true } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 671f87155..d29ecefe1 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index f988b0e93..40b660ef5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 9b9379a8d..35b5078cc 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From d11d4bade318d5a17d1a5e3860292352e25cc813 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 7 Mar 2024 08:18:14 -0800 Subject: [PATCH 048/127] fix: more robust calculation of default location for code generation (#1095) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/schema/src/plugins/plugin-utils.ts | 32 ++++++++++----- packages/schema/src/utils/pkg-utils.ts | 43 +++++++++++--------- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/server/src/shared.ts | 9 ++-- packages/testtools/package.json | 2 +- packages/testtools/src/schema.ts | 10 ++++- 18 files changed, 72 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 54769b34b..6c73b5f25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 23c8d0b7b..d74358e5f 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-alpha.4" +version = "2.0.0-alpha.5" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index db2daea8d..b42a1047b 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 315e65049..5ea7cdfa8 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 9886151b4..2f19b2305 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": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 5d191e2ff..314c71f1e 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": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 12652665d..fa22863fa 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": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 3da2d8bf8..e4f2e548a 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": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index c21bccd72..a0ad55223 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": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 4dcb94daa..93bd0592e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index d2113958a..4fe78d2dd 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": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index d6cc12403..f405604d0 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -3,6 +3,7 @@ import { PluginGlobalOptions } from '@zenstackhq/sdk'; import fs from 'fs'; import path from 'path'; import { PluginRunnerOptions } from '../cli/plugin-runner'; +import { getPackageManager } from '../utils/pkg-utils'; export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; @@ -76,23 +77,32 @@ 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') { - // handle 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 !== '/') { + const { projectRoot } = getPackageManager(__dirname); + if (fs.existsSync(path.join(projectRoot, 'node_modules'))) { + // use the located node_modules folder + return path.join(projectRoot, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); + } else { + // unable to locate a node_modules folder, fallback to where the runtime + // package resides + + // find the real runtime module path, it might be a symlink in pnpm + let runtimeModulePath = require.resolve('@zenstackhq/runtime'); + + 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, '..'); + } runtimeModulePath = path.join(runtimeModulePath, '..'); } - runtimeModulePath = path.join(runtimeModulePath, '..'); + const modulesFolder = getNodeModulesFolder(runtimeModulePath); + return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined; } - const modulesFolder = getNodeModulesFolder(runtimeModulePath); - return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined; } /** diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index ce41dac34..69c42e1ae 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -1,22 +1,23 @@ 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'; /** - * A type named FindUp that takes a type parameter e which extends boolean. - * If e extends true, it returns a union type of string[] or undefined. + * A type named FindUp that takes a type parameter e which extends boolean. + * If e extends true, it returns a union type of string[] or undefined. * If e does not extend true, it returns a union type of string or undefined. * * @export * @template e A type parameter that extends boolean */ -export type FindUp = e extends true ? string[] | undefined : string | undefined +export type FindUp = e extends true ? string[] | undefined : string | undefined; /** - * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. - * Optionally return a single path or multiple paths. - * If multiple allowed, return all paths found. + * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. + * Optionally return a single path or multiple paths. + * If multiple allowed, return all paths found. * If no paths are found, return undefined. * * @export @@ -27,7 +28,12 @@ export type FindUp = e extends true ? string[] | undefined : * @param [result=[]] An array of strings representing the accumulated results used in multiple results * @returns Path(s) to a specific file or folder within the directory or parent directories */ -export function findUp(names: string[], cwd: string = process.cwd(), multiple: e = false as e, result: string[] = []): FindUp { +export function findUp( + names: string[], + cwd: string = process.cwd(), + multiple: e = false as e, + result: string[] = [] +): FindUp { if (!names.some((name) => !!name)) return undefined; const target = names.find((name) => fs.existsSync(path.join(cwd, name))); if (multiple == false && target) return path.join(cwd, target) as FindUp; @@ -37,23 +43,22 @@ export function findUp(names: string[], cwd: string = return findUp(names, up, multiple, result); } -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, @@ -106,7 +111,7 @@ export function ensurePackage( } /** - * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. + * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. * It iterates through the directory structure going one level up at a time until it finds a package.json file. If no package.json file is found, it returns undefined. * @deprecated Use findUp instead @see findUp */ diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d29ecefe1..11f17f3d6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 40b660ef5..748b68e52 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/server/src/shared.ts b/packages/server/src/shared.ts index 1a9c62119..7538411c5 100644 --- a/packages/server/src/shared.ts +++ b/packages/server/src/shared.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import type { ModelMeta, PolicyDef, ZodSchemas } from '@zenstackhq/runtime'; +import { DEFAULT_RUNTIME_LOAD_PATH, type ModelMeta, type PolicyDef, type ZodSchemas } from '@zenstackhq/runtime'; import path from 'path'; import { AdapterBaseOptions } from './types'; @@ -39,7 +39,8 @@ export function getDefaultModelMeta(loadPath: string | undefined): ModelMeta { 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; + return require(path.join(process.cwd(), 'node_modules', DEFAULT_RUNTIME_LOAD_PATH, 'model-meta')) + .default; } catch { throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.'); } @@ -66,7 +67,7 @@ export function getDefaultPolicy(loadPath: string | undefined): PolicyDef { 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; + return require(path.join(process.cwd(), 'node_modules', DEFAULT_RUNTIME_LOAD_PATH, 'policy')).default; } catch { throw new Error( 'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.' @@ -97,7 +98,7 @@ export function getDefaultZodSchemas(loadPath: string | undefined): ZodSchemas | 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')); + return require(path.join(process.cwd(), 'node_modules', DEFAULT_RUNTIME_LOAD_PATH, 'zod')); } catch { return undefined; } diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 35b5078cc..b6ce4df2e 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "ZenStack Test Tools", "main": "index.js", "private": true, diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 392b4af4f..ecdad8336 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -2,7 +2,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { DMMF } from '@prisma/generator-helper'; import type { Model } from '@zenstackhq/language/ast'; -import type { AuthUser, CrudContract, EnhancementKind, EnhancementOptions } from '@zenstackhq/runtime'; +import { + DEFAULT_RUNTIME_LOAD_PATH, + type AuthUser, + type CrudContract, + type EnhancementKind, + type EnhancementOptions, +} from '@zenstackhq/runtime'; import { getDMMF } from '@zenstackhq/sdk'; import { execSync } from 'child_process'; import * as fs from 'fs'; @@ -284,7 +290,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { ? path.isAbsolute(opt.output) ? opt.output : path.join(projectRoot, opt.output) - : path.join(projectRoot, 'node_modules', '.zenstack'); + : path.join(projectRoot, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); const policy = require(path.join(outputPath, 'policy')).default; const modelMeta = require(path.join(outputPath, 'model-meta')).default; From 9f9d27704c2eecbbbd69e841ece6b1d4d22040f6 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 8 Mar 2024 21:11:26 -0800 Subject: [PATCH 049/127] fix(polymorphism): relation name disambiguation (#1107) --- .../validator/datamodel-validator.ts | 2 +- .../src/plugins/enhancer/enhance/index.ts | 2 +- .../src/plugins/prisma/schema-generator.ts | 55 +++++++++++++++ packages/schema/src/utils/ast-utils.ts | 11 +-- .../with-delegate/issue-1100.test.ts | 69 +++++++++++++++++++ 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 1d442f12b..4f9cd0039 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -241,7 +241,7 @@ export default class DataModelValidator implements AstValidator { const oppositeModel = field.type.reference!.ref! as DataModel; // Use name because the current document might be updated - let oppositeFields = getModelFieldsWithBases(oppositeModel).filter( + let oppositeFields = getModelFieldsWithBases(oppositeModel, false).filter( (f) => f.type.reference?.ref?.name === contextModel.name ); oppositeFields = oppositeFields.filter((f) => { diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index df14e0826..a379e5ad0 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -133,7 +133,7 @@ async function generateLogicalPrisma(model: Model, options: PluginOptions, outDi } catch { // noop } - throw new PluginError(name, `Failed to run "prisma generate"`); + throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`); } // make a bunch of typing fixes to the generated prisma client diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index bc63d535a..bfcecc9ef 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -34,6 +34,7 @@ import { getIdFields } from '../../utils/ast-utils'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { getAttribute, + getAttributeArg, getForeignKeyFields, getLiteral, getPrismaVersion, @@ -299,6 +300,9 @@ export class PrismaSchemaGenerator { // 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) { @@ -422,6 +426,8 @@ export class PrismaSchemaGenerator { ); const addedRel = new PrismaFieldAttribute('@relation', [ + // use field name as relation name for disambiguation + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)), new PrismaAttributeArg('fields', args), new PrismaAttributeArg('references', args), ]); @@ -440,11 +446,60 @@ export class PrismaSchemaGenerator { } 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; + } + + const relAttr = getAttribute(f, '@relation'); + const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(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 get supportNamedConstraints() { const ds = this.zmodel.declarations.find(isDataSource); if (!ds) { diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 2688987a2..8dfe75b4b 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -16,7 +16,7 @@ import { ModelImport, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { isFromStdlib } from '@zenstackhq/sdk'; +import { isDelegateModel, isFromStdlib } from '@zenstackhq/sdk'; import { AstNode, copyAstNode, @@ -207,19 +207,22 @@ export function getContainingDataModel(node: Expression): DataModel | undefined return undefined; } -export function getModelFieldsWithBases(model: DataModel) { +export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) { if (model.$baseMerged) { return model.fields; } else { - return [...model.fields, ...getRecursiveBases(model).flatMap((base) => base.fields)]; + return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)]; } } -export function getRecursiveBases(dataModel: DataModel): DataModel[] { +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)); } 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(); + }); +}); From df078308699a7388afeb7f5f842f37ac0ae8b5c0 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 8 Mar 2024 22:11:54 -0800 Subject: [PATCH 050/127] merge from dev (#1110) Co-authored-by: ErikMCM <70036542+ErikMCM@users.noreply.github.com> Co-authored-by: Jason Kleinberg Co-authored-by: Jonathan S Co-authored-by: Jiasheng --- .../runtime/src/cross/nested-write-visitor.ts | 34 +- .../src/enhancements/policy/handler.ts | 53 ++- .../attribute-application-validator.ts | 15 +- .../validator/expression-validator.ts | 19 +- .../src/plugins/zod/utils/schema-gen.ts | 10 +- packages/schema/src/utils/ast-utils.ts | 14 + packages/schema/src/utils/pkg-utils.ts | 21 +- .../tests/generator/expression-writer.test.ts | 18 +- .../validation/attribute-validation.test.ts | 39 +-- .../validation/datamodel-validation.test.ts | 80 ++--- .../src/typescript-expression-transformer.ts | 157 +++++++-- .../with-policy/field-validation.test.ts | 331 +++++++++++++++++- .../tests/regression/issue-1078.test.ts | 55 +++ .../tests/regression/issue-1080.test.ts | 133 +++++++ 14 files changed, 829 insertions(+), 150 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1078.test.ts create mode 100644 tests/integration/tests/regression/issue-1080.test.ts diff --git a/packages/runtime/src/cross/nested-write-visitor.ts b/packages/runtime/src/cross/nested-write-visitor.ts index db2455d7e..4ce4e0ae7 100644 --- a/packages/runtime/src/cross/nested-write-visitor.ts +++ b/packages/runtime/src/cross/nested-write-visitor.ts @@ -4,7 +4,7 @@ import type { FieldInfo, ModelMeta } from './model-meta'; import { resolveField } from './model-meta'; import { MaybePromise, PrismaWriteActionType, PrismaWriteActions } from './types'; -import { enumerate, getModelFields } from './utils'; +import { getModelFields } from './utils'; type NestingPathItem = { field?: FieldInfo; model: string; where: any; unique: boolean }; @@ -155,7 +155,7 @@ export class NestedWriteVisitor { // visit payload switch (action) { case 'create': - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, {}); let callbackResult: any; if (this.callback.create) { @@ -183,7 +183,7 @@ export class NestedWriteVisitor { break; case 'connectOrCreate': - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item.where); let callbackResult: any; if (this.callback.connectOrCreate) { @@ -198,7 +198,7 @@ export class NestedWriteVisitor { case 'connect': if (this.callback.connect) { - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item, true); await this.callback.connect(model, item, newContext); } @@ -210,7 +210,7 @@ export class NestedWriteVisitor { // if relation is to-many, the payload is a unique filter object // if relation is to-one, the payload can only be boolean `true` if (this.callback.disconnect) { - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item, typeof item === 'object'); await this.callback.disconnect(model, item, newContext); } @@ -219,7 +219,7 @@ export class NestedWriteVisitor { case 'set': if (this.callback.set) { - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item, true); await this.callback.set(model, item, newContext); } @@ -227,7 +227,7 @@ export class NestedWriteVisitor { break; case 'update': - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item.where); let callbackResult: any; if (this.callback.update) { @@ -246,7 +246,7 @@ export class NestedWriteVisitor { break; case 'updateMany': - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item.where); let callbackResult: any; if (this.callback.updateMany) { @@ -260,7 +260,7 @@ export class NestedWriteVisitor { break; case 'upsert': { - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, item.where); let callbackResult: any; if (this.callback.upsert) { @@ -280,7 +280,7 @@ export class NestedWriteVisitor { case 'delete': { if (this.callback.delete) { - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, toplevel ? item.where : item); await this.callback.delete(model, item, newContext); } @@ -290,7 +290,7 @@ export class NestedWriteVisitor { case 'deleteMany': if (this.callback.deleteMany) { - for (const item of enumerate(data)) { + for (const item of this.enumerateReverse(data)) { const newContext = pushNewContext(field, model, toplevel ? item.where : item); await this.callback.deleteMany(model, item, newContext); } @@ -338,4 +338,16 @@ export class NestedWriteVisitor { } } } + + // enumerate a (possible) array in reverse order, so that the enumeration + // callback can safely delete the current item + private *enumerateReverse(data: any) { + if (Array.isArray(data)) { + for (let i = data.length - 1; i >= 0; i--) { + yield data[i]; + } + } else { + yield data; + } + } } diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index f2bc4ad07..6c8f6d205 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -357,17 +357,7 @@ export class PolicyProxyHandler implements Pr } } - if (context.parent.connect) { - // if the payload parent already has a "connect" clause, merge it - if (Array.isArray(context.parent.connect)) { - context.parent.connect.push(args.where); - } else { - context.parent.connect = [context.parent.connect, args.where]; - } - } else { - // otherwise, create a new "connect" clause - context.parent.connect = args.where; - } + this.mergeToParent(context.parent, 'connect', args.where); // record the key of connected entities so we can avoid validating them later connectedEntities.add(getEntityKey(model, existing)); } else { @@ -375,11 +365,11 @@ export class PolicyProxyHandler implements Pr pushIdFields(model, context); // create a new "create" clause at the parent level - context.parent.create = args.create; + this.mergeToParent(context.parent, 'create', args.create); } // remove the connectOrCreate clause - delete context.parent['connectOrCreate']; + this.removeFromParent(context.parent, 'connectOrCreate', args); // return false to prevent visiting the nested payload return false; @@ -917,7 +907,7 @@ export class PolicyProxyHandler implements Pr await _create(model, args, context); // remove it from the update payload - delete context.parent.create; + this.removeFromParent(context.parent, 'create', args); // don't visit payload return false; @@ -950,14 +940,15 @@ export class PolicyProxyHandler implements Pr await _registerPostUpdateCheck(model, uniqueFilter); // convert upsert to update - context.parent.update = { + const convertedUpdate = { where: args.where, data: this.validateUpdateInputSchema(model, args.update), }; - delete context.parent.upsert; + this.mergeToParent(context.parent, 'update', convertedUpdate); + this.removeFromParent(context.parent, 'upsert', args); // continue visiting the new payload - return context.parent.update; + return convertedUpdate; } else { // create case @@ -965,7 +956,7 @@ export class PolicyProxyHandler implements Pr await _create(model, args.create, context); // remove it from the update payload - delete context.parent.upsert; + this.removeFromParent(context.parent, 'upsert', args); // don't visit payload return false; @@ -1388,5 +1379,31 @@ export class PolicyProxyHandler implements Pr return requireField(this.modelMeta, fieldInfo.type, fieldInfo.backLink); } + private mergeToParent(parent: any, key: string, value: any) { + if (parent[key]) { + if (Array.isArray(parent[key])) { + parent[key].push(value); + } else { + parent[key] = [parent[key], value]; + } + } else { + parent[key] = value; + } + } + + private removeFromParent(parent: any, key: string, data: any) { + if (parent[key] === data) { + delete parent[key]; + } else if (Array.isArray(parent[key])) { + const idx = parent[key].indexOf(data); + if (idx >= 0) { + parent[key].splice(idx, 1); + if (parent[key].length === 0) { + delete parent[key]; + } + } + } + } + //#endregion } diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index f81f5c166..92c086005 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -15,7 +15,7 @@ import { isEnum, isReferenceExpr, } from '@zenstackhq/language/ast'; -import { isFutureExpr, isRelationshipField, resolved } from '@zenstackhq/sdk'; +import { isDataModelFieldReference, isFutureExpr, isRelationshipField, resolved } from '@zenstackhq/sdk'; import { ValidationAcceptor, streamAst } from 'langium'; import pluralize from 'pluralize'; import { AstValidator } from '../types'; @@ -151,6 +151,19 @@ export default class AttributeApplicationValidator implements AstValidator isDataModelFieldReference(node) && isDataModel(node.$resolvedType?.decl) + ) + ) { + accept('error', `\`@@validate\` condition cannot use relation fields`, { node: condition }); + } + } + private validatePolicyKinds( kind: string, candidates: string[], diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index cfc8a39af..8a87ddc14 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -1,8 +1,10 @@ import { + AstNode, BinaryExpr, Expression, ExpressionType, isDataModel, + isDataModelAttribute, isDataModelField, isEnum, isLiteralExpr, @@ -12,7 +14,7 @@ import { } from '@zenstackhq/language/ast'; import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; import { ValidationAcceptor } from 'langium'; -import { getContainingDataModel, isCollectionPredicate } from '../../utils/ast-utils'; +import { findUpAst, getContainingDataModel, isCollectionPredicate } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { typeAssignable } from './utils'; @@ -123,6 +125,17 @@ export default class ExpressionValidator implements AstValidator { case '==': case '!=': { + if (this.isInValidationContext(expr)) { + // in validation context, all fields are optional, so we should allow + // comparing any field against null + if ( + (isDataModelFieldReference(expr.left) && isNullExpr(expr.right)) || + (isDataModelFieldReference(expr.right) && isNullExpr(expr.left)) + ) { + return; + } + } + if (!!expr.left.$resolvedType?.array !== !!expr.right.$resolvedType?.array) { accept('error', 'incompatible operand types', { node: expr }); break; @@ -211,6 +224,10 @@ export default class ExpressionValidator implements AstValidator { } } + private isInValidationContext(node: AstNode) { + return findUpAst(node, (n) => isDataModelAttribute(n) && n.decl.$refText === '@@validate'); + } + private isNotModelFieldExpr(expr: Expression) { return ( isLiteralExpr(expr) || isEnumFieldReference(expr) || isNullExpr(expr) || this.isAuthOrAuthMemberAccess(expr) diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index ec181e8d4..b800a0869 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -6,6 +6,7 @@ import { getAttributeArg, getAttributeArgLiteral, getLiteral, + isDataModelFieldReference, isFromStdlib, } from '@zenstackhq/sdk'; import { @@ -203,10 +204,17 @@ export function makeValidationRefinements(model: DataModel) { const message = messageArg ? `, { message: ${JSON.stringify(messageArg)} }` : ''; try { - const expr = new TypeScriptExpressionTransformer({ + let expr = new TypeScriptExpressionTransformer({ context: ExpressionContext.ValidationRule, fieldReferenceContext: 'value', }).transform(valueArg); + + if (isDataModelFieldReference(valueArg)) { + // if the expression is a simple field reference, treat undefined + // as true since the all fields are optional in validation context + expr = `${expr} ?? true`; + } + return `.refine((value: any) => ${expr}${message})`; } catch (err) { if (err instanceof TypeScriptExpressionTransformerError) { diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 8dfe75b4b..0f8e5567a 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -229,3 +229,17 @@ export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): }); return result; } + +/** + * Walk upward from the current AST node to find the first node that satisfies the predicate. + */ +export function findUpAst(node: AstNode, predicate: (node: AstNode) => boolean): AstNode | undefined { + let curr: AstNode | undefined = node; + while (curr) { + if (predicate(curr)) { + return curr; + } + curr = curr.$container; + } + return undefined; +} diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index 69c42e1ae..0ac2c5379 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -6,18 +6,18 @@ import { match } from 'ts-pattern'; export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; /** - * A type named FindUp that takes a type parameter e which extends boolean. - * If e extends true, it returns a union type of string[] or undefined. + * A type named FindUp that takes a type parameter e which extends boolean. + * If e extends true, it returns a union type of string[] or undefined. * If e does not extend true, it returns a union type of string or undefined. * * @export * @template e A type parameter that extends boolean */ -export type FindUp = e extends true ? string[] | undefined : string | undefined; +export type FindUp = e extends true ? string[] | undefined : string | undefined /** - * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. - * Optionally return a single path or multiple paths. - * If multiple allowed, return all paths found. + * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. + * Optionally return a single path or multiple paths. + * If multiple allowed, return all paths found. * If no paths are found, return undefined. * * @export @@ -28,12 +28,7 @@ export type FindUp = e extends true ? string[] | undefined : * @param [result=[]] An array of strings representing the accumulated results used in multiple results * @returns Path(s) to a specific file or folder within the directory or parent directories */ -export function findUp( - names: string[], - cwd: string = process.cwd(), - multiple: e = false as e, - result: string[] = [] -): FindUp { +export function findUp(names: string[], cwd: string = process.cwd(), multiple: e = false as e, result: string[] = []): FindUp { if (!names.some((name) => !!name)) return undefined; const target = names.find((name) => fs.existsSync(path.join(cwd, name))); if (multiple == false && target) return path.join(cwd, target) as FindUp; @@ -111,7 +106,7 @@ export function ensurePackage( } /** - * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. + * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. * It iterates through the directory structure going one level up at a time until it finds a package.json file. If no package.json file is found, it returns undefined. * @deprecated Use findUp instead @see findUp */ diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index a4cc6ae5f..463567d0b 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -1178,7 +1178,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `(user?.roles?.includes(Role.ADMIN)??false)?{AND:[]}:{OR:[]}`, + `((user?.roles?.includes(Role.ADMIN))??false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1205,7 +1205,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `(user?.email?.includes('test')??false)?{AND:[]}:{OR:[]}`, + `((user?.email?.includes('test'))??false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1218,7 +1218,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `(user?.email?.toLowerCase().includes('test'?.toLowerCase())??false)?{AND:[]}:{OR:[]}`, + `((user?.email?.toLowerCase().includes('test'?.toLowerCase()))??false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1231,7 +1231,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `(user?.email?.startsWith('test')??false)?{AND:[]}:{OR:[]}`, + `((user?.email?.startsWith('test'))??false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1244,7 +1244,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `(user?.email?.endsWith('test')??false)?{AND:[]}:{OR:[]}`, + `((user?.email?.endsWith('test'))??false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1257,7 +1257,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `(user?.roles?.includes(Role.ADMIN)??false)?{AND:[]}:{OR:[]}`, + `((user?.roles?.includes(Role.ADMIN))??false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1270,7 +1270,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `([Role.ADMIN,Role.USER]?.every((item)=>user?.roles?.includes(item))??false)?{AND:[]}:{OR:[]}`, + `((user?.roles)!==undefined?([Role.ADMIN,Role.USER]?.every((item)=>user?.roles?.includes(item))):false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1283,7 +1283,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `([Role.USER,Role.ADMIN]?.some((item)=>user?.roles?.includes(item))??false)?{AND:[]}:{OR:[]}`, + `((user?.roles)!==undefined?([Role.USER,Role.ADMIN]?.some((item)=>user?.roles?.includes(item))):false)?{AND:[]}:{OR:[]}`, userInit ); @@ -1296,7 +1296,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `((!user?.roles||user?.roles?.length===0)??false)?{AND:[]}:{OR:[]}`, + `(!user?.roles||user?.roles?.length===0)?{AND:[]}:{OR:[]}`, userInit ); }); diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index c6d0db13b..e3c1c597e 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -927,17 +927,6 @@ describe('Attribute tests', () => { @@validate(hasSome(es, [E1])) @@validate(hasEvery(es, [E1])) @@validate(isEmpty(es)) - - @@validate(n.e in [E1, E2]) - @@validate(n.i in [1, 2]) - @@validate(contains(n.s, 'a')) - @@validate(contains(n.s, 'a', true)) - @@validate(startsWith(n.s, 'a')) - @@validate(endsWith(n.s, 'a')) - @@validate(has(n.es, E1)) - @@validate(hasSome(n.es, [E1])) - @@validate(hasEvery(n.es, [E1])) - @@validate(isEmpty(n.es)) } `); @@ -1000,26 +989,21 @@ describe('Attribute tests', () => { expect( await loadModelWithError(` ${prelude} - model N { - id String @id - m M @relation(fields: [mId], references: [id]) - mId String - } model M { id String @id - n N? - @@validate(n in [1]) + x Int + @@validate(has(x, 1)) } `) - ).toContain('left operand of "in" must be of scalar type'); + ).toContain('argument is not assignable to parameter'); expect( await loadModelWithError(` ${prelude} model M { id String @id - x Int - @@validate(has(x, 1)) + x Int[] + @@validate(hasSome(x, 1)) } `) ).toContain('argument is not assignable to parameter'); @@ -1029,11 +1013,18 @@ describe('Attribute tests', () => { ${prelude} model M { id String @id - x Int[] - @@validate(hasSome(x, 1)) + n N? + @@validate(n.value > 0) + } + + model N { + id String @id + value Int + m M @relation(fields: [mId], references: [id]) + mId String @unique } `) - ).toContain('argument is not assignable to parameter'); + ).toContain('`@@validate` condition cannot use relation fields'); }); it('auth function check', async () => { diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 736a202cb..955f315c3 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -10,7 +10,7 @@ describe('Data Model Validation Tests', () => { it('duplicated fields', async () => { const result = await safelyLoadModel(` - ${ prelude } + ${prelude} model M { id String @id x Int @@ -128,7 +128,7 @@ 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) @@ -199,100 +199,102 @@ describe('Data Model Validation Tests', () => { 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]) diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts index 20585118c..b033dab96 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -5,9 +5,6 @@ import { DataModel, Expression, InvocationExpr, - isDataModel, - isEnumField, - isThisExpr, LiteralExpr, MemberAccessExpr, NullExpr, @@ -16,10 +13,16 @@ import { StringLiteral, ThisExpr, UnaryExpr, + isArrayExpr, + isDataModel, + isEnumField, + isLiteralExpr, + isNullExpr, + isThisExpr, } from '@zenstackhq/language/ast'; -import { match, P } from 'ts-pattern'; +import { P, match } from 'ts-pattern'; import { ExpressionContext } from './constants'; -import { getIdFields, getLiteral, isFromStdlib, isFutureExpr } from './utils'; +import { getIdFields, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from './utils'; export class TypeScriptExpressionTransformerError extends Error { constructor(message: string) { @@ -168,13 +171,17 @@ export class TypeScriptExpressionTransformer { const max = getLiteral(args[2]); let result: string; if (min === undefined) { - result = `(${field}?.length > 0)`; + result = this.ensureBooleanTernary(args[0], field, `${field}?.length > 0`); } else if (max === undefined) { - result = `(${field}?.length >= ${min})`; + result = this.ensureBooleanTernary(args[0], field, `${field}?.length >= ${min}`); } else { - result = `(${field}?.length >= ${min} && ${field}?.length <= ${max})`; + result = this.ensureBooleanTernary( + args[0], + field, + `${field}?.length >= ${min} && ${field}?.length <= ${max}` + ); } - return this.ensureBoolean(result); + return result; } @func('contains') @@ -208,25 +215,29 @@ export class TypeScriptExpressionTransformer { private _regex(args: Expression[]) { const field = this.transform(args[0], false); const pattern = getLiteral(args[1]); - return `new RegExp(${JSON.stringify(pattern)}).test(${field})`; + return this.ensureBooleanTernary(args[0], field, `new RegExp(${JSON.stringify(pattern)}).test(${field})`); } @func('email') private _email(args: Expression[]) { const field = this.transform(args[0], false); - return `z.string().email().safeParse(${field}).success`; + return this.ensureBooleanTernary(args[0], field, `z.string().email().safeParse(${field}).success`); } @func('datetime') private _datetime(args: Expression[]) { const field = this.transform(args[0], false); - return `z.string().datetime({ offset: true }).safeParse(${field}).success`; + return this.ensureBooleanTernary( + args[0], + field, + `z.string().datetime({ offset: true }).safeParse(${field}).success` + ); } @func('url') private _url(args: Expression[]) { const field = this.transform(args[0], false); - return `z.string().url().safeParse(${field}).success`; + return this.ensureBooleanTernary(args[0], field, `z.string().url().safeParse(${field}).success`); } @func('has') @@ -239,26 +250,52 @@ export class TypeScriptExpressionTransformer { @func('hasEvery') private _hasEvery(args: Expression[], normalizeUndefined: boolean) { const field = this.transform(args[0], false); - const result = `${this.transform(args[1], normalizeUndefined)}?.every((item) => ${field}?.includes(item))`; - return this.ensureBoolean(result); + return this.ensureBooleanTernary( + args[0], + field, + `${this.transform(args[1], normalizeUndefined)}?.every((item) => ${field}?.includes(item))` + ); } @func('hasSome') private _hasSome(args: Expression[], normalizeUndefined: boolean) { const field = this.transform(args[0], false); - const result = `${this.transform(args[1], normalizeUndefined)}?.some((item) => ${field}?.includes(item))`; - return this.ensureBoolean(result); + return this.ensureBooleanTernary( + args[0], + field, + `${this.transform(args[1], normalizeUndefined)}?.some((item) => ${field}?.includes(item))` + ); } @func('isEmpty') private _isEmpty(args: Expression[]) { const field = this.transform(args[0], false); - const result = `(!${field} || ${field}?.length === 0)`; - return this.ensureBoolean(result); + return `(!${field} || ${field}?.length === 0)`; } private ensureBoolean(expr: string) { - return `(${expr} ?? false)`; + if (this.options.context === ExpressionContext.ValidationRule) { + // all fields are optional in a validation context, so we treat undefined + // as boolean true + return `(${expr} ?? true)`; + } else { + return `((${expr}) ?? false)`; + } + } + + private ensureBooleanTernary(predicate: Expression, transformedPredicate: string, value: string) { + if (isLiteralExpr(predicate) || isArrayExpr(predicate)) { + // these are never undefined + return value; + } + + if (this.options.context === ExpressionContext.ValidationRule) { + // all fields are optional in a validation context, so we treat undefined + // as boolean true + return `((${transformedPredicate}) !== undefined ? (${value}): true)`; + } else { + return `((${transformedPredicate}) !== undefined ? (${value}): false)`; + } } // #endregion @@ -300,8 +337,18 @@ export class TypeScriptExpressionTransformer { } } - private unary(expr: UnaryExpr, normalizeUndefined: boolean): string { - return `(${expr.operator} ${this.transform(expr.operand, normalizeUndefined)})`; + private unary(expr: UnaryExpr, normalizeUndefined: boolean) { + const operand = this.transform(expr.operand, normalizeUndefined); + let result = `(${expr.operator} ${operand})`; + if ( + expr.operator === '!' && + this.options.context === ExpressionContext.ValidationRule && + isDataModelFieldReference(expr.operand) + ) { + // in a validation context, we treat unary involving undefined as boolean true + result = this.ensureBooleanTernary(expr.operand, operand, result); + } + return result; } private isModelType(expr: Expression) { @@ -316,17 +363,49 @@ export class TypeScriptExpressionTransformer { left = `(${left}?.id ?? null)`; right = `(${right}?.id ?? null)`; } - const _default = `(${left} ${expr.operator} ${right})`; + + let _default = `(${left} ${expr.operator} ${right})`; + + if (this.options.context === ExpressionContext.ValidationRule) { + const nullComparison = this.extractNullComparison(expr); + if (nullComparison) { + // null comparison covers both null and undefined + const { fieldRef } = nullComparison; + const field = this.transform(fieldRef, normalizeUndefined); + if (expr.operator === '==') { + _default = `(${field} === null || ${field} === undefined)`; + } else if (expr.operator === '!=') { + _default = `(${field} !== null && ${field} !== undefined)`; + } + } else { + // for other comparisons, in a validation context, + // we treat binary involving undefined as boolean true + if (isDataModelFieldReference(expr.left)) { + _default = this.ensureBooleanTernary(expr.left, left, _default); + } + if (isDataModelFieldReference(expr.right)) { + _default = this.ensureBooleanTernary(expr.right, right, _default); + } + } + } return match(expr.operator) - .with( - 'in', - () => - `(${this.transform(expr.right, false)}?.includes(${this.transform( + .with('in', () => { + const left = `${this.transform(expr.left, normalizeUndefined)}`; + const right = `${this.transform(expr.right, false)}`; + let result = `${right}?.includes(${left})`; + if (this.options.context === ExpressionContext.ValidationRule) { + // in a validation context, we treat binary involving undefined as boolean true + result = this.ensureBooleanTernary( expr.left, - normalizeUndefined - )}) ?? false)` - ) + left, + this.ensureBooleanTernary(expr.right, right, result) + ); + } else { + result = this.ensureBoolean(result); + } + return result; + }) .with(P.union('==', '!='), () => { if (isThisExpr(expr.left) || isThisExpr(expr.right)) { // map equality comparison with `this` to id comparison @@ -352,6 +431,20 @@ export class TypeScriptExpressionTransformer { .otherwise(() => _default); } + private extractNullComparison(expr: BinaryExpr) { + if (expr.operator !== '==' && expr.operator !== '!=') { + return undefined; + } + + if (isDataModelFieldReference(expr.left) && isNullExpr(expr.right)) { + return { fieldRef: expr.left, nullExpr: expr.right }; + } else if (isDataModelFieldReference(expr.right) && isNullExpr(expr.left)) { + return { fieldRef: expr.right, nullExpr: expr.left }; + } else { + return undefined; + } + } + private collectionPredicate(expr: BinaryExpr, operator: '?' | '!' | '^', normalizeUndefined: boolean) { const operand = this.transform(expr.left, normalizeUndefined); const innerTransformer = new TypeScriptExpressionTransformer({ @@ -363,8 +456,8 @@ export class TypeScriptExpressionTransformer { const predicate = innerTransformer.transform(expr.right, normalizeUndefined); return match(operator) - .with('?', () => `!!((${operand})?.some((_item: any) => ${predicate}))`) - .with('!', () => `!!((${operand})?.every((_item: any) => ${predicate}))`) + .with('?', () => this.ensureBoolean(`(${operand})?.some((_item: any) => ${predicate})`)) + .with('!', () => this.ensureBoolean(`(${operand})?.every((_item: any) => ${predicate})`)) .with('^', () => `!((${operand})?.some((_item: any) => ${predicate}))`) .exhaustive(); } 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 84a3496d1..7508333b6 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -1,5 +1,5 @@ import { CrudFailureReason, isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; -import { FullDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; +import { FullDbClientContract, createPostgresDb, dropPostgresDb, loadSchema, run } from '@zenstackhq/testtools'; describe('With Policy: field validation', () => { let db: FullDbClientContract; @@ -609,3 +609,332 @@ describe('With Policy: field validation', () => { expect(u.tasks[0]).toMatchObject({ slug: 'slug2' }); }); }); + +describe('With Policy: model-level validation', () => { + it('create', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int + y Int + + @@validate(x > 0) + @@validate(x >= y) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: 0, y: 0 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); + }); + + it('update', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int + y Int + + @@validate(x >= y) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); + await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); + }); + + it('int optionality', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int? + + @@validate(x > 0) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: 0 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 1 } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + }); + + it('boolean optionality', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Boolean? + + @@validate(x) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: false } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: true } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + }); + + it('optionality with binary', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int? + y Int? + + @@validate(x > y) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 1 } })).toResolveTruthy(); + await expect(db.model.create({ data: { y: 1 } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + }); + + it('optionality with in operator lhs', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x String? + + @@validate(x in ['foo', 'bar']) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: 'hello' } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 'foo' } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + }); + + it('optionality with in operator rhs', async () => { + let prisma; + try { + const dbUrl = await createPostgresDb('field-validation-in-operator'); + const r = await loadSchema( + ` + model Model { + id Int @id @default(autoincrement()) + x String[] + + @@validate('foo' in x) + @@allow('all', true) + } + `, + { + provider: 'postgresql', + dbUrl, + } + ); + + const db = r.enhance(); + prisma = r.prisma; + + await expect(db.model.create({ data: { x: ['hello'] } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: ['foo', 'bar'] } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + } finally { + await prisma.$disconnect(); + await dropPostgresDb('field-validation-in-operator'); + } + }); + + it('optionality with complex expression', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int? + y Int? + + @@validate(y > 1 && x > y) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { y: 1 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { y: 2 } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 3, y: 2 } })).toResolveTruthy(); + }); + + it('optionality with negation', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Boolean? + + @@validate(!x) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { x: true } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: false } })).toResolveTruthy(); + await expect(db.model.create({ data: {} })).toResolveTruthy(); + }); + + it('update implied optionality', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int + y Int + + @@validate(x > y) + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { id: 1, x: 2, y: 1 } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { y: 1 } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); + }); + + it('optionality with scalar functions', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + s String + e String + u String + d String + + @@validate( + length(s, 1, 5) && + contains(s, 'b') && + startsWith(s, 'a') && + endsWith(s, 'c') && + regex(s, '^[0-9a-zA-Z]*$'), + 'invalid s') + @@validate(email(e), 'invalid e') + @@validate(url(u), 'invalid u') + @@validate(datetime(d), 'invalid d') + + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect( + db.model.create({ + data: { + id: 1, + s: 'a1b2c', + e: 'a@bcd.com', + u: 'https://www.zenstack.dev', + d: '2024-01-01T00:00:00.000Z', + }, + }) + ).toResolveTruthy(); + + await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); + + await expect(db.model.update({ where: { id: 1 }, data: { s: 'a2b3c' } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { s: 'c2b3c' } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { s: 'a1b2c3' } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { s: 'aaccc' } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { s: 'a1b2d' } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { s: 'a1-3c' } })).toBeRejectedByPolicy(); + + await expect(db.model.update({ where: { id: 1 }, data: { e: 'b@def.com' } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { e: 'xyz' } })).toBeRejectedByPolicy(); + + await expect(db.model.update({ where: { id: 1 }, data: { u: 'https://zenstack.dev/docs' } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { u: 'xyz' } })).toBeRejectedByPolicy(); + + await expect(db.model.update({ where: { id: 1 }, data: { d: '2025-01-01T00:00:00.000Z' } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { d: 'xyz' } })).toBeRejectedByPolicy(); + }); + + it('optionality with array functions', async () => { + let prisma; + try { + const dbUrl = await createPostgresDb('field-validation-array-funcs'); + const r = await loadSchema( + ` + model Model { + id Int @id @default(autoincrement()) + x String[] + y Int[] + + @@validate( + has(x, 'a') && + hasEvery(x, ['a', 'b']) && + hasSome(x, ['x', 'y']) && + (y == null || !isEmpty(y)) + ) + + @@allow('all', true) + } + `, + { + provider: 'postgresql', + dbUrl, + } + ); + + const db = r.enhance(); + prisma = r.prisma; + + await expect(db.model.create({ data: { id: 1, x: ['a', 'b', 'x'], y: [1] } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { x: ['b', 'x'] } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { x: ['a', 'b'] } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { y: [] } })).toBeRejectedByPolicy(); + } finally { + await prisma.$disconnect(); + await dropPostgresDb('field-validation-array-funcs'); + } + }); + + it('null comparison', async () => { + const { enhance } = await loadSchema(` + model Model { + id Int @id @default(autoincrement()) + x Int + y Int + + @@validate(x == null || !(x <= 0)) + @@validate(y != null && !(y > 1)) + + @@allow('all', true) + } + `); + + const db = enhance(); + + await expect(db.model.create({ data: { id: 1, x: 1 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { id: 1, x: 1, y: 2 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { id: 1, x: 0, y: 0 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { id: 1, x: 1, y: 0 } })).toResolveTruthy(); + + await expect(db.model.update({ where: { id: 1 }, data: {} })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { y: 2 } })).toBeRejectedByPolicy(); + await expect(db.model.update({ where: { id: 1 }, data: { y: 1 } })).toResolveTruthy(); + await expect(db.model.update({ where: { id: 1 }, data: { x: 2, y: 1 } })).toResolveTruthy(); + }); +}); diff --git a/tests/integration/tests/regression/issue-1078.test.ts b/tests/integration/tests/regression/issue-1078.test.ts new file mode 100644 index 000000000..3c0fc7024 --- /dev/null +++ b/tests/integration/tests/regression/issue-1078.test.ts @@ -0,0 +1,55 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1078', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Counter { + id String @id + + name String + value Int + + @@validate(value >= 0) + @@allow('all', true) + } + ` + ); + + const db = enhance(); + + await expect( + db.counter.create({ + data: { id: '1', name: 'It should create', value: 1 }, + }) + ).toResolveTruthy(); + + //! This query fails validation + await expect( + db.counter.update({ + where: { id: '1' }, + data: { name: 'It should update' }, + }) + ).toResolveTruthy(); + }); + + it('read', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Post { + id Int @id() @default(autoincrement()) + title String @allow('read', true, true) + content String + } + ` + ); + + const db = enhance(); + + const post = await prisma.post.create({ data: { title: 'Post1', content: 'Content' } }); + await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); + await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ + title: 'Post1', + }); + }); +}); diff --git a/tests/integration/tests/regression/issue-1080.test.ts b/tests/integration/tests/regression/issue-1080.test.ts new file mode 100644 index 000000000..17ce998c2 --- /dev/null +++ b/tests/integration/tests/regression/issue-1080.test.ts @@ -0,0 +1,133 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1080', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Project { + id String @id @unique @default(uuid()) + Fields Field[] + + @@allow('all', true) + } + + model Field { + id String @id @unique @default(uuid()) + name String + Project Project @relation(fields: [projectId], references: [id]) + projectId String + + @@allow('all', true) + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const project = await db.project.create({ + include: { Fields: true }, + data: { + Fields: { + create: [{ name: 'first' }, { name: 'second' }], + }, + }, + }); + + let updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: [ + { + where: { id: project.Fields[0].id }, + create: { name: 'first1' }, + update: { name: 'first1' }, + }, + { + where: { id: project.Fields[1].id }, + create: { name: 'second1' }, + update: { name: 'second1' }, + }, + ], + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first1' }), + expect.objectContaining({ name: 'second1' }), + ]), + }); + + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: project.Fields[0].id }, + create: { name: 'first2' }, + update: { name: 'first2' }, + }, + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first2' }), + expect.objectContaining({ name: 'second1' }), + ]), + }); + + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: project.Fields[0].id }, + create: { name: 'first3' }, + update: { name: 'first3' }, + }, + update: { + where: { id: project.Fields[1].id }, + data: { name: 'second3' }, + }, + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first3' }), + expect.objectContaining({ name: 'second3' }), + ]), + }); + + updated = await db.project.update({ + where: { id: project.id }, + include: { Fields: true }, + data: { + Fields: { + upsert: { + where: { id: 'non-exist' }, + create: { name: 'third1' }, + update: { name: 'third1' }, + }, + update: { + where: { id: project.Fields[1].id }, + data: { name: 'second4' }, + }, + }, + }, + }); + expect(updated).toMatchObject({ + Fields: expect.arrayContaining([ + expect.objectContaining({ name: 'first3' }), + expect.objectContaining({ name: 'second4' }), + expect.objectContaining({ name: 'third1' }), + ]), + }); + }); +}); From 557163f789527aa64627b16fb718da3068dd0052 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 9 Mar 2024 00:45:54 -0800 Subject: [PATCH 051/127] fix(polymorphism): include submodel relations when reading a concrete model (#1112) --- packages/runtime/src/enhancements/delegate.ts | 144 +++++++++++------- .../with-delegate/enhanced-client.test.ts | 26 ++-- 2 files changed, 97 insertions(+), 73 deletions(-) diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index 06a96b0c6..dce180bd6 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import deepcopy from 'deepcopy'; -import deepmerge from 'deepmerge'; +import deepmerge, { type ArrayMergeOptions } from 'deepmerge'; import { lowerCaseFirst } from 'lower-case-first'; import { DELEGATE_AUX_RELATION_PREFIX } from '../constants'; import { @@ -11,7 +11,6 @@ import { getIdFields, getModelInfo, isDelegateModel, - requireField, resolveField, } from '../cross'; import type { CrudContract, DbClientContract } from '../types'; @@ -204,7 +203,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { } if (!args.select) { + // include base models upwards this.injectBaseIncludeRecursively(model, args); + + // include sub models downwards + this.injectConcreteIncludeRecursively(model, args); } } @@ -232,6 +235,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { if (!selectInclude.select) { this.injectBaseIncludeRecursively(model, selectInclude); + this.injectConcreteIncludeRecursively(model, selectInclude); } return selectInclude; } @@ -302,6 +306,30 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { 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 @@ -1038,6 +1066,31 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { 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); @@ -1046,7 +1099,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { const baseRelationName = this.makeAuxRelationName(base); const baseData = entity[baseRelationName]; if (baseData && typeof baseData === 'object') { - const baseAssembled = this.assembleHierarchy(base.name, baseData); + const baseAssembled = this.assembleUp(base.name, baseData); Object.assign(result, baseAssembled); } } @@ -1063,9 +1116,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { const fieldValue = entity[field.name]; if (field.isDataModel) { if (Array.isArray(fieldValue)) { - result[field.name] = fieldValue.map((item) => this.assembleHierarchy(field.type, item)); + result[field.name] = fieldValue.map((item) => this.assembleUp(field.type, item)); } else { - result[field.name] = this.assembleHierarchy(field.type, fieldValue); + result[field.name] = this.assembleUp(field.type, fieldValue); } } else { result[field.name] = fieldValue; @@ -1076,66 +1129,39 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { return result; } - // #endregion - - // #region backup - - private transformWhereHierarchy(where: any, contextModel: ModelInfo, forModel: ModelInfo) { - if (!where || typeof where !== 'object') { - return where; - } - - let curr: ModelInfo | undefined = contextModel; - const inheritStack: ModelInfo[] = []; - while (curr) { - inheritStack.unshift(curr); - curr = this.getBaseModel(curr.name); - } - - let result: any = {}; - for (const [key, value] of Object.entries(where)) { - const fieldInfo = requireField(this.options.modelMeta, contextModel.name, key); - const fieldHierarchy = this.transformFieldHierarchy(fieldInfo, value, contextModel, forModel, inheritStack); - result = deepmerge(result, fieldHierarchy); - } - - return result; - } - - private transformFieldHierarchy( - fieldInfo: FieldInfo, - value: unknown, - contextModel: ModelInfo, - forModel: ModelInfo, - inheritStack: ModelInfo[] - ): any { - const fieldModel = fieldInfo.inheritedFrom ? this.getModelInfo(fieldInfo.inheritedFrom) : contextModel; - if (fieldModel === forModel) { - return { [fieldInfo.name]: value }; - } - - const fieldModelPos = inheritStack.findIndex((m) => m === fieldModel); - const forModelPos = inheritStack.findIndex((m) => m === forModel); + private assembleDown(model: string, entity: any) { const result: any = {}; - let curr = result; + const modelInfo = getModelInfo(this.options.modelMeta, model, true); - if (fieldModelPos > forModelPos) { - // walk down hierarchy - for (let i = forModelPos + 1; i <= fieldModelPos; i++) { - const rel = this.makeAuxRelationName(inheritStack[i]); - curr[rel] = {}; - curr = curr[rel]; + 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); + } } - } else { - // walk up hierarchy - for (let i = forModelPos - 1; i >= fieldModelPos; i--) { - const rel = this.makeAuxRelationName(inheritStack[i]); - curr[rel] = {}; - curr = curr[rel]; + } + + for (const field of Object.values(modelInfo.fields)) { + if (field.name in entity) { + const fieldValue = entity[field.name]; + if (field.isDataModel) { + if (Array.isArray(fieldValue)) { + result[field.name] = fieldValue.map((item) => this.assembleDown(field.type, item)); + } else { + result[field.name] = this.assembleDown(field.type, fieldValue); + } + } else { + result[field.name] = fieldValue; + } } } - curr[fieldInfo.name] = value; return result; } diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index 5a171aa8b..f4b4e63f8 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -201,24 +201,22 @@ describe('Polymorphism Test', () => { let video = await db.video.findFirst({ where: { duration: r.duration }, include: { owner: true } }); expect(video).toMatchObject({ - id: video.id, - createdAt: r.createdAt, - viewCount: r.viewCount, - url: r.url, - duration: r.duration, + ...r, assetType: 'Video', videoType: 'RatedVideo', }); - expect(video.rating).toBeUndefined(); expect(video.owner).toMatchObject(user); const asset = await db.asset.findFirst({ where: { viewCount: r.viewCount }, include: { owner: true } }); - expect(asset).toMatchObject({ id: r.id, createdAt: r.createdAt, assetType: 'Video', viewCount: r.viewCount }); - expect(asset.url).toBeUndefined(); - expect(asset.duration).toBeUndefined(); - expect(asset.rating).toBeUndefined(); - expect(asset.videoType).toBeUndefined(); - expect(asset.owner).toMatchObject(user); + 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' }, @@ -230,9 +228,9 @@ describe('Polymorphism Test', () => { createdAt: image.createdAt, assetType: 'Image', viewCount: image.viewCount, + format: 'png', + owner: expect.objectContaining(user), }); - expect(imgAsset.format).toBeUndefined(); - expect(imgAsset.owner).toMatchObject(user); }); it('order by base fields', async () => { From c0a9a027b28834f2c41ae1f0f6b0b3a8ba475301 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 9 Mar 2024 08:14:12 -0800 Subject: [PATCH 052/127] chore: bump version (#1113) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 6c73b5f25..471452d21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index d74358e5f..36158c630 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-alpha.5" +version = "2.0.0-alpha.6" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index b42a1047b..71e33c4b0 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 5ea7cdfa8..e159f1198 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 2f19b2305..65d61ed90 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": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 314c71f1e..9be65d9be 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": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index fa22863fa..2fc322554 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": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index e4f2e548a..c78807897 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": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index a0ad55223..44e81392c 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": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 93bd0592e..301cafb00 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 4fe78d2dd..1781b0ed6 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": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 11f17f3d6..10a643b9f 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 748b68e52..ff3f08152 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index b6ce4df2e..d16d9f84a 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From e34c530a93b67a3eda9f5b53a087c7edd4a44999 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 11 Mar 2024 14:57:05 -0700 Subject: [PATCH 053/127] fix(hooks): include delegate base models when calculating models affected by a mutation (#1121) --- packages/runtime/src/cross/model-meta.ts | 2 +- packages/runtime/src/cross/query-analyzer.ts | 16 ++++++++++++++++ .../src/enhancements/create-enhancement.ts | 4 ---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts index efa4d1a03..a90b20685 100644 --- a/packages/runtime/src/cross/model-meta.ts +++ b/packages/runtime/src/cross/model-meta.ts @@ -118,7 +118,7 @@ export type ModelInfo = { name: string; /** - * Base types + * Base types (not including abstract base models). */ baseTypes?: string[]; diff --git a/packages/runtime/src/cross/query-analyzer.ts b/packages/runtime/src/cross/query-analyzer.ts index bf501f020..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]; } @@ -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/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index 1b9796970..be2fc4579 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -5,7 +5,6 @@ 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'; @@ -130,9 +129,6 @@ export function createEnhancement( ); } - const logger = new Logger(prisma); - logger.info(`Enabled ZenStack enhancements: ${options.kinds?.join(', ')}`); - let result = prisma; if ( From dc6901a2d90faf57741ab9b560f8c1eb7088079f Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 11 Mar 2024 15:28:58 -0700 Subject: [PATCH 054/127] fix(polymorphism): wrong query result when selecting "_count" (#1124) --- packages/runtime/src/enhancements/delegate.ts | 56 ++++++++++++------- .../with-delegate/issue-1123.test.ts | 47 ++++++++++++++++ 2 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1123.test.ts diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index dce180bd6..12c249912 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -1106,23 +1106,31 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { const modelInfo = getModelInfo(this.options.modelMeta, model, true); - for (const field of Object.values(modelInfo.fields)) { + 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.name in entity) { - const fieldValue = entity[field.name]; - if (field.isDataModel) { - if (Array.isArray(fieldValue)) { - result[field.name] = fieldValue.map((item) => this.assembleUp(field.type, item)); - } else { - result[field.name] = this.assembleUp(field.type, fieldValue); - } + if (field.isDataModel) { + if (Array.isArray(value)) { + result[field.name] = value.map((item) => this.assembleUp(field.type, item)); } else { - result[field.name] = fieldValue; + result[field.name] = this.assembleUp(field.type, value); } + } else { + result[field.name] = value; } } @@ -1147,18 +1155,26 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { } } - for (const field of Object.values(modelInfo.fields)) { - if (field.name in entity) { - const fieldValue = entity[field.name]; - if (field.isDataModel) { - if (Array.isArray(fieldValue)) { - result[field.name] = fieldValue.map((item) => this.assembleDown(field.type, item)); - } else { - result[field.name] = this.assembleDown(field.type, fieldValue); - } + 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] = fieldValue; + result[field.name] = this.assembleDown(field.type, value); } + } else { + result[field.name] = value; } } 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 }, + } + ); + }); +}); From b63589aa314de29a26a3e24902a63750f4a4c054 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 11 Mar 2024 15:29:19 -0700 Subject: [PATCH 055/127] chore: bump version (#1127) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 471452d21..ac2d35cbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 36158c630..15a60f0a4 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-alpha.6" +version = "2.0.0-alpha.7" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 71e33c4b0..19b81ebce 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index e159f1198..1b98f4062 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 65d61ed90..c3df79e42 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": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 9be65d9be..fe5034310 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": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 2fc322554..7dc6fd5e8 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": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index c78807897..4fd444426 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": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 44e81392c..d7427347b 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": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 301cafb00..b3a108019 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 1781b0ed6..fc04065bb 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": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 10a643b9f..02bd6c681 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index ff3f08152..a6dd78931 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index d16d9f84a..13c883dcc 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-alpha.6", + "version": "2.0.0-alpha.7", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From a6621079bd69b0cbd798c68098b609140cfdab76 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 13 Mar 2024 09:08:22 -0700 Subject: [PATCH 056/127] fix: disallow extending from multiple delegate models (#1117) --- .../validator/datamodel-validator.ts | 10 ++++++++ .../validation/datamodel-validation.test.ts | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 4f9cd0039..0baf5ace3 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -22,6 +22,7 @@ import { validateDuplicatedDeclarations } from './utils'; export default class DataModelValidator implements AstValidator { validate(dm: DataModel, accept: ValidationAcceptor): void { this.validateBaseAbstractModel(dm, accept); + this.validateBaseDelegateModel(dm, accept); validateDuplicatedDeclarations(dm, getModelFieldsWithBases(dm), accept); this.validateAttributes(dm, accept); this.validateFields(dm, accept); @@ -396,6 +397,15 @@ export default class DataModelValidator implements AstValidator { ); }); } + + 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/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 955f315c3..4f44b109e 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -714,4 +714,28 @@ describe('Data Model Validation Tests', () => { 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`)); + }); }); From ea4442b7d8a55744f84c7d247ffe45d1849121a7 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 13 Mar 2024 09:27:47 -0700 Subject: [PATCH 057/127] merge from dev (#1132) Co-authored-by: ErikMCM <70036542+ErikMCM@users.noreply.github.com> Co-authored-by: Jason Kleinberg Co-authored-by: Jonathan S Co-authored-by: Jiasheng --- README.md | 10 +-- packages/schema/README.md | 4 +- packages/sdk/src/utils.ts | 9 +- .../tests/regression/issue-1129.test.ts | 87 +++++++++++++++++++ 4 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1129.test.ts diff --git a/README.md b/README.md index ed37238f8..d874cfefb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - + @@ -142,7 +142,7 @@ The following diagram gives a high-level architecture overview of ZenStack. - [Home](https://zenstack.dev) - [Documentation](https://zenstack.dev/docs) -- [Community chat](https://go.zenstack.dev/chat) +- [Community chat](https://discord.gg/Ykhr738dUe) - [Twitter](https://twitter.com/zenstackhq) - [Blog](https://zenstack.dev/blog) @@ -162,7 +162,7 @@ The following diagram gives a high-level architecture overview of ZenStack. - [SWR](https://github.com/vercel/swr) and [TanStack Query](https://github.com/TanStack/query) hooks generator - OpenAPI specification generator - [tRPC](https://trpc.io) router generator -- 🙋🏻 [Request for a plugin](https://go.zenstack.dev/chat) +- 🙋🏻 [Request for a plugin](https://discord.gg/Ykhr738dUe) ### Framework adapters @@ -171,7 +171,7 @@ The following diagram gives a high-level architecture overview of ZenStack. - [SvelteKit](https://zenstack.dev/docs/reference/server-adapters/sveltekit) - [Fastify](https://zenstack.dev/docs/reference/server-adapters/fastify) - [ExpressJS](https://zenstack.dev/docs/reference/server-adapters/express) -- 🙋🏻 [Request for an adapter](https://go.zenstack.dev/chat) +- 🙋🏻 [Request for an adapter](https://discord.gg/Ykhr738dUe) ### Prisma schema extensions @@ -179,7 +179,7 @@ The following diagram gives a high-level architecture overview of ZenStack. - [Multi-file schema and model inheritance](https://zenstack.dev/docs/guides/multiple-schema) - Strong-typed JSON field (coming soon) - Polymorphism (future) -- 🙋🏻 [Request for an extension](https://go.zenstack.dev/chat) +- 🙋🏻 [Request for an extension](https://discord.gg/Ykhr738dUe) ## Examples diff --git a/packages/schema/README.md b/packages/schema/README.md index ece352081..2c24ab102 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -17,13 +17,13 @@ This VS Code extension provides code editing helpers for authoring ZenStack's sc - [Home](https://zenstack.dev) - [Documentation](https://zenstack.dev/docs) -- [Community chat](https://go.zenstack.dev/chat) +- [Community chat](https://discord.gg/Ykhr738dUe) - [Twitter](https://twitter.com/zenstackhq) - [Blog](https://dev.to/zenstack) ## Community -Join our [discord server](https://go.zenstack.dev/chat) for chat and updates! +Join our [discord server](https://discord.gg/Ykhr738dUe) for chat and updates! ## License diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 641446a02..4c79d4c9c 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -219,11 +219,14 @@ export function isIdField(field: DataModelField) { return true; } + // NOTE: we have to use name to match fields because the fields + // may be inherited from an abstract base and have cloned identities + const model = field.$container as DataModel; // model-level @@id attribute with a list of fields const modelLevelIds = getModelIdFields(model); - if (modelLevelIds.includes(field)) { + if (modelLevelIds.map((f) => f.name).includes(field.name)) { return true; } @@ -235,12 +238,12 @@ export function isIdField(field: DataModelField) { // then, the first field with @unique can be used as id const firstUniqueField = model.fields.find((f) => hasAttribute(f, '@unique')); if (firstUniqueField) { - return firstUniqueField === field; + return firstUniqueField.name === field.name; } // last, the first model level @@unique can be used as id const modelLevelUnique = getModelUniqueFields(model); - if (modelLevelUnique.includes(field)) { + if (modelLevelUnique.map((f) => f.name).includes(field.name)) { return true; } diff --git a/tests/integration/tests/regression/issue-1129.test.ts b/tests/integration/tests/regression/issue-1129.test.ts new file mode 100644 index 000000000..49198a5cb --- /dev/null +++ b/tests/integration/tests/regression/issue-1129.test.ts @@ -0,0 +1,87 @@ +import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1129', () => { + it('regression', async () => { + let prisma; + const dbUrl = await createPostgresDb('regression-issue-1129'); + + try { + const r = await loadSchema( + ` + model Relation1 { + id String @id @default(cuid()) + field1 String + concrete Concrete[] + @@allow('all', true) + } + + model Relation2 { + id String @id @default(cuid()) + field2 String + concrete Concrete[] + @@allow('all', true) + } + + abstract model WithRelation1 { + relation1Id String + relation1 Relation1 @relation(fields: [relation1Id], references: [id]) + } + abstract model WithRelation2 { + relation2Id String + relation2 Relation2 @relation(fields: [relation2Id], references: [id]) + } + + model Concrete extends WithRelation1, WithRelation2 { + concreteField String + @@id([relation1Id, relation2Id]) + @@allow('all', true) + } + `, + { provider: 'postgresql', dbUrl } + ); + + prisma = r.prisma; + const db = r.enhance(); + + await db.$transaction(async (tx: any) => { + await tx.relation2.createMany({ + data: [ + { + id: 'relation2Id1', + field2: 'field2Value1', + }, + { + id: 'relation2Id2', + field2: 'field2Value2', + }, + ], + }); + + await tx.relation1.create({ + data: { + field1: 'field1Value', + concrete: { + createMany: { + data: [ + { + concreteField: 'concreteFieldValue1', + relation2Id: 'relation2Id1', + }, + { + concreteField: 'concreteFieldValue2', + relation2Id: 'relation2Id2', + }, + ], + }, + }, + }, + }); + }); + } finally { + if (prisma) { + await prisma.$disconnect(); + } + await dropPostgresDb('regression-issue-1129'); + } + }); +}); From 6db11e2cfcd57939e75b3ec9baedd9ab1fb1d4c8 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 13 Mar 2024 12:15:24 -0700 Subject: [PATCH 058/127] refactor: remove legacy swr mutation generation (#1134) --- packages/plugins/swr/src/generator.ts | 65 ++++----------------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index ca84c101c..b6fb4edb1 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -12,10 +12,9 @@ import { } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; 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 '.'; @@ -26,14 +25,6 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. 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, { @@ -49,7 +40,7 @@ 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, options); + generateModelHooks(project, outDir, dataModel, mapping, options); }); await saveProject(project); @@ -61,7 +52,6 @@ function generateModelHooks( outDir: string, model: DataModel, mapping: DMMF.ModelMapping, - legacyMutations: boolean, options: PluginOptions ) { const fileName = paramCase(model.name); @@ -84,31 +74,19 @@ function generateModelHooks( 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[] = []; // create is somehow named "createOne" in the DMMF // 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 @@ -147,13 +125,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 @@ -161,7 +139,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 @@ -169,13 +147,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 @@ -267,8 +245,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) { @@ -329,7 +305,6 @@ function generateQueryHook( function generateMutation( sf: SourceFile, - useMutateModelFunc: FunctionDeclaration | undefined, model: DataModel, method: 'POST' | 'PUT' | 'PATCH' | 'DELETE', operation: string, @@ -342,30 +317,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}`, From 0d6674d9d14ce8c73e3761e61a6c24a6fea74317 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 13 Mar 2024 19:25:24 -0700 Subject: [PATCH 059/127] refactor(tanstack): merge zenstack query/mutation options into tanstack options (#1136) --- packages/plugins/swr/src/generator.ts | 2 +- packages/plugins/tanstack-query/package.json | 1 + .../plugins/tanstack-query/src/generator.ts | 78 ++++++++--------- .../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 | 36 +++++++- .../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 | 26 +++--- .../tanstack-query/tests/plugin.test.ts | 6 +- .../tests/react-hooks-v5.test.tsx | 84 +++++++------------ .../tanstack-query/tests/react-hooks.test.tsx | 46 ++++------ pnpm-lock.yaml | 3 + tests/integration/tests/cli/plugins.test.ts | 2 +- 16 files changed, 215 insertions(+), 199 deletions(-) diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index b6fb4edb1..4a36f545f 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -66,7 +66,7 @@ function generateModelHooks( 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';`, ]); diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 4fd444426..64d5c3f63 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -90,6 +90,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": { diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 4e3079db8..c4be4e3c5 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -17,6 +17,7 @@ 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 '.'; @@ -37,7 +38,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. 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(name, `Unsupported version "${version}": use "v4" or "v5"`); } @@ -130,15 +131,6 @@ function generateQueryHook( name: 'options?', type: optionsType, }, - ...(optimistic - ? [ - { - name: 'optimisticUpdate', - type: 'boolean', - initializer: 'true', - }, - ] - : []), ], isExported: true, }); @@ -152,7 +144,7 @@ function generateQueryHook( makeGetContext(target), `return use${generateMode}ModelQuery('${model}', \`\${endpoint}/${lowerCaseFirst( model - )}/${operation}\`, args, options, fetch${optimistic ? ', optimisticUpdate' : ''});`, + )}/${operation}\`, args, options, fetch);`, ]); } } @@ -189,16 +181,6 @@ function generateMutationHook( name: 'options?', type: nonGenericOptionsType, }, - { - name: 'invalidateQueries', - type: 'boolean', - initializer: 'true', - }, - { - name: 'optimisticUpdate', - type: 'boolean', - initializer: 'false', - }, ], }); @@ -215,7 +197,7 @@ function generateMutationHook( overrideReturnType ?? model }, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst( model - )}/${operation}\`, metadata, options, fetch, invalidateQueries, ${checkReadBack}, optimisticUpdate) + )}/${operation}\`, metadata, options, fetch, ${checkReadBack}) `, }, ], @@ -561,7 +543,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;`, ]; @@ -612,41 +594,53 @@ 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': - return `Omit, 'queryKey'>`; - case 'svelte': - return infinite + : `Omit, 'queryKey'>` + ) + .with( + 'vue', + () => `Omit, 'queryKey'>` + ) + .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': - return `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`; - case 'svelte': - return `MutationOptions<${returnType}, DefaultError, ${argsType}>`; - default: + let result = match(target) + .with('react', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`) + .with('vue', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`) + .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/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..95479da75 100644 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ b/packages/plugins/tanstack-query/src/runtime/common.ts @@ -40,6 +40,31 @@ export type QueryError = Error & { status?: number; }; +/** + * 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; +}; + +/** + * 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 +135,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 }]; } 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 049b66907..24edc225a 100644 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime/vue.ts @@ -20,6 +20,8 @@ import { marshal, setupInvalidation, setupOptimisticUpdate, + type ExtraMutationOptions, + type ExtraQueryOptions, type FetchFn, } from './common'; @@ -54,20 +56,21 @@ 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( 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, }); @@ -91,7 +94,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); }, @@ -108,9 +111,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< @@ -124,11 +125,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) => { @@ -148,6 +147,9 @@ export function useModelMutation< // TODO: figure out the typing problem const finalOptions: any = { ...options, mutationFn }; const operation = url.split('/').pop(); + const invalidateQueries = options?.invalidateQueries !== false; + const optimisticUpdate = !!options?.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 c87e2a38f..3916cc21e 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -53,6 +53,7 @@ plugin tanstack { provider = '${path.resolve(__dirname, '../dist')}' output = '$projectRoot/hooks' target = 'react' + version = 'v4' } ${sharedModel} @@ -74,7 +75,6 @@ plugin tanstack { provider = '${path.resolve(__dirname, '../dist')}' output = '$projectRoot/hooks' target = 'react' - version = 'v5' } ${sharedModel} @@ -96,6 +96,7 @@ plugin tanstack { provider = '${path.resolve(__dirname, '../dist')}' output = '$projectRoot/hooks' target = 'vue' + version = 'v4' } ${sharedModel} @@ -117,7 +118,6 @@ plugin tanstack { provider = '${path.resolve(__dirname, '../dist')}' output = '$projectRoot/hooks' target = 'vue' - version = 'v5' } ${sharedModel} @@ -139,6 +139,7 @@ plugin tanstack { provider = '${path.resolve(__dirname, '../dist')}' output = '$projectRoot/hooks' target = 'svelte' + version = 'v4' } ${sharedModel} @@ -160,7 +161,6 @@ plugin tanstack { provider = '${path.resolve(__dirname, '../dist')}' output = '$projectRoot/hooks' target = 'svelte' - version = 'v5' } ${sharedModel} 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..ed6af9f2e 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); }); }); 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/pnpm-lock.yaml b/pnpm-lock.yaml index d78c2eea2..43aa526d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -282,6 +282,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 diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 4d28c8e2a..ee32ce9e9 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -71,7 +71,7 @@ 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', `${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`, From 7229316a73d881c05e241f12b40b3c77ed1607e5 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 13 Mar 2024 19:43:02 -0700 Subject: [PATCH 060/127] refactor(server): remove deprecated useSuperJSON flag (#1137) --- packages/server/src/express/middleware.ts | 5 ----- packages/server/src/fastify/plugin.ts | 5 ----- packages/server/src/next/app-route-handler.ts | 5 ----- packages/server/src/next/pages-route-handler.ts | 5 ----- packages/server/src/sveltekit/handler.ts | 5 ----- packages/server/src/types.ts | 7 ------- 6 files changed, 32 deletions(-) diff --git a/packages/server/src/express/middleware.ts b/packages/server/src/express/middleware.ts index cdf5a3c6e..200c57bd6 100644 --- a/packages/server/src/express/middleware.ts +++ b/packages/server/src/express/middleware.ts @@ -33,11 +33,6 @@ 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.' - ); - } return async (request, response, next) => { const prisma = (await options.getPrisma(request, response)) as DbClientContract; diff --git a/packages/server/src/fastify/plugin.ts b/packages/server/src/fastify/plugin.ts index 480c4ba8d..8651e0bb0 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; diff --git a/packages/server/src/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts index 538f4ceb5..71121d151 100644 --- a/packages/server/src/next/app-route-handler.ts +++ b/packages/server/src/next/app-route-handler.ts @@ -20,11 +20,6 @@ export default function factory( 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 (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..0752775da 100644 --- a/packages/server/src/next/pages-route-handler.ts +++ b/packages/server/src/next/pages-route-handler.ts @@ -18,11 +18,6 @@ export default function factory( 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 (req: NextApiRequest, res: NextApiResponse) => { const prisma = (await options.getPrisma(req, res)) as DbClientContract; diff --git a/packages/server/src/sveltekit/handler.ts b/packages/server/src/sveltekit/handler.ts index be1d831d8..3d346f762 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)) { diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index dc72fea25..81e2f5fd7 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -58,11 +58,4 @@ export interface AdapterBaseOptions { * 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; } From bbea0a9bd0aea9f2dff4ada8b8322511ea0cc5e4 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 13 Mar 2024 20:05:04 -0700 Subject: [PATCH 061/127] refactor(swr): remove deprecated initialData option (#1138) --- packages/plugins/swr/src/runtime/index.ts | 24 ++--------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts index e8ce6a255..0deeefdcc 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'; @@ -189,10 +175,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 +222,7 @@ export function useInfiniteModelQuery( throw new Error('Invalid query key: ' + key); } }, - { - ...options, - fallbackData: options?.initialData ?? options?.fallbackData, - } + options ); } From 8099793b89376e0c5289e74b3f60ef05c080951f Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 14 Mar 2024 15:30:21 -0700 Subject: [PATCH 062/127] refactor(zod): clean up optionality of generated schema fields (#1139) --- packages/schema/src/plugins/zod/generator.ts | 153 +++++++++++++----- tests/integration/tests/plugins/zod.test.ts | 54 ++++++- .../tests/regression/issue-886.test.ts | 4 +- 3 files changed, 171 insertions(+), 40 deletions(-) diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index f09e93951..542b77a64 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -23,7 +23,7 @@ import { name } from '.'; import { getDefaultOutputFolder } from '../plugin-utils'; import Transformer from './transformer'; import removeDir from './utils/removeDir'; -import { getFieldSchemaDefault, makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; +import { makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; export class ZodSchemaGenerator { private readonly sourceFiles: SourceFile[] = []; @@ -311,7 +311,7 @@ export class ZodSchemaGenerator { writer.writeLine(`import { Decimal } from 'decimal.js';`); } - // base schema + // base schema - including all scalar fields, with optionality following the schema writer.write(`const baseSchema = z.object(`); writer.inlineBlock(() => { scalarFields.forEach((field) => { @@ -325,11 +325,11 @@ export class ZodSchemaGenerator { let relationSchema: string | undefined; let fkSchema: string | undefined; - if (relations.length > 0 || fkFields.length > 0) { + if (relations.length > 0) { relationSchema = 'relationSchema'; writer.write(`const ${relationSchema} = z.object(`); writer.inlineBlock(() => { - [...relations, ...fkFields].forEach((field) => { + [...relations].forEach((field) => { writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); }); }); @@ -353,21 +353,23 @@ export class ZodSchemaGenerator { if (refinements.length > 0) { refineFuncName = `refine${upperCaseFirst(model.name)}`; writer.writeLine( - `export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join( + ` +/** + * Schema refinement function for applying \`@@validate\` rules. + */ +export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join( '\n' - )}; }` + )}; +} +` ); } //////////////////////////////////////////////// // 1. Model schema //////////////////////////////////////////////// - const fieldsWithoutDefault = scalarFields.filter((f) => !getFieldSchemaDefault(f)); - // mark fields without default value as optional - let modelSchema = this.makePartial( - 'baseSchema', - fieldsWithoutDefault.length < scalarFields.length ? fieldsWithoutDefault.map((f) => f.name) : undefined - ); + + let modelSchema = 'baseSchema'; // omit fields const fieldsToOmit = scalarFields.filter((field) => hasAttribute(field, '@omit')); @@ -378,23 +380,46 @@ export class ZodSchemaGenerator { ); } - if (relationSchema) { - // export schema with only scalar fields - const modelScalarSchema = `${upperCaseFirst(model.name)}ScalarSchema`; - writer.writeLine(`export const ${modelScalarSchema} = ${modelSchema};`); - modelSchema = modelScalarSchema; + // export schema with only scalar fields: `[Model]ScalarSchema` + const modelScalarSchema = `${upperCaseFirst(model.name)}ScalarSchema`; + writer.writeLine(` +/** + * \`${model.name}\` schema excluding foreign keys and relations. + */ +export const ${modelScalarSchema} = ${modelSchema}; +`); + modelSchema = modelScalarSchema; + + // merge fk fields + if (fkSchema) { + modelSchema = this.makeMerge(modelSchema, fkSchema); + } - // merge relations + // merge relation fields (all optional) + if (relationSchema) { modelSchema = this.makeMerge(modelSchema, this.makePartial(relationSchema)); } // refine if (refineFuncName) { + // export a schema without refinement for extensibility: `[Model]WithoutRefineSchema` const noRefineSchema = `${upperCaseFirst(model.name)}WithoutRefineSchema`; - writer.writeLine(`export const ${noRefineSchema} = ${modelSchema};`); + writer.writeLine(` +/** + * \`${model.name}\` schema prior to calling \`.refine()\` for extensibility. + */ +export const ${noRefineSchema} = ${modelSchema}; +`); modelSchema = `${refineFuncName}(${noRefineSchema})`; } - writer.writeLine(`export const ${upperCaseFirst(model.name)}Schema = ${modelSchema};`); + + // 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 @@ -405,7 +430,13 @@ export class ZodSchemaGenerator { if (refineFuncName) { prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`; } - writer.writeLine(`export const ${upperCaseFirst(model.name)}PrismaCreateSchema = ${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 @@ -424,15 +455,26 @@ export class ZodSchemaGenerator { if (refineFuncName) { prismaUpdateSchema = `${refineFuncName}(${prismaUpdateSchema})`; } - writer.writeLine(`export const ${upperCaseFirst(model.name)}PrismaUpdateSchema = ${prismaUpdateSchema};`); + writer.writeLine( + ` +/** + * Schema used for validating Prisma update input. For internal use only. + * @private + */ +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 ); + + // mark fields with default as optional if (fieldsWithDefault.length > 0) { createSchema = this.makePartial( createSchema, @@ -440,45 +482,82 @@ export class ZodSchemaGenerator { ); } - if (fkSchema) { - // export schema with only scalar fields - const createScalarSchema = `${upperCaseFirst(model.name)}CreateScalarSchema`; - writer.writeLine(`export const ${createScalarSchema} = ${createSchema};`); + // export schema with only scalar fields: `[Model]CreateScalarSchema` + const createScalarSchema = `${upperCaseFirst(model.name)}CreateScalarSchema`; + 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); } if (refineFuncName) { - // export a schema without refinement for extensibility + // export a schema without refinement for extensibility: `[Model]CreateWithoutRefineSchema` const noRefineSchema = `${upperCaseFirst(model.name)}CreateWithoutRefineSchema`; - writer.writeLine(`export const ${noRefineSchema} = ${createSchema};`); + writer.writeLine(` +/** + * \`${model.name}\` schema for create operations prior to calling \`.refine()\` for extensibility. + */ +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 //////////////////////////////////////////////// + + // for update all fields are optional let updateSchema = this.makePartial('baseSchema'); - if (fkSchema) { - // export schema with only scalar fields - const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; - writer.writeLine(`export const ${updateScalarSchema} = ${updateSchema};`); - updateSchema = updateScalarSchema; + // export schema with only scalar fields: `[Model]UpdateScalarSchema` + const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; + writer.writeLine(` +/** + * \`${model.name}\` schema for update operations excluding foreign keys and relations. + */ +export const ${updateScalarSchema} = ${updateSchema}; +`); + + updateSchema = updateScalarSchema; + if (fkSchema) { // merge fk fields updateSchema = this.makeMerge(updateSchema, this.makePartial(fkSchema)); } if (refineFuncName) { - // export a schema without refinement for extensibility + // export a schema without refinement for extensibility: `[Model]UpdateWithoutRefineSchema` const noRefineSchema = `${upperCaseFirst(model.name)}UpdateWithoutRefineSchema`; - writer.writeLine(`export const ${noRefineSchema} = ${updateSchema};`); + writer.writeLine(` +/** + * \`${model.name}\` schema for update operations prior to calling \`.refine()\` for extensibility. + */ +export const ${noRefineSchema} = ${updateSchema}; +`); updateSchema = `${refineFuncName}(${noRefineSchema})`; } - writer.writeLine(`export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema};`); + + // 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; diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index dd82f6786..23ff4190b 100644 --- a/tests/integration/tests/plugins/zod.test.ts +++ b/tests/integration/tests/plugins/zod.test.ts @@ -45,6 +45,9 @@ describe('Zod plugin tests', () => { password String @omit role Role @default(USER) posts Post[] + age Int? + + @@validate(length(password, 6, 20)) } model Post { @@ -61,8 +64,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(); @@ -75,6 +84,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(); @@ -90,6 +109,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 @@ -98,7 +119,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({ @@ -109,6 +148,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, diff --git a/tests/integration/tests/regression/issue-886.test.ts b/tests/integration/tests/regression/issue-886.test.ts index a749db61e..4f20d9817 100644 --- a/tests/integration/tests/regression/issue-886.test.ts +++ b/tests/integration/tests/regression/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); }); }); From bab51958246272a7ae171200797b525213c13d4d Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 14 Mar 2024 21:49:18 -0700 Subject: [PATCH 063/127] feat: generate strong typing for the `user` context of `enhance` API (#1141) --- .../src/enhancements/create-enhancement.ts | 4 +- .../runtime/src/enhancements/policy/index.ts | 4 +- .../enhancer/enhance/auth-type-generator.ts | 141 +++++++++++++++ .../src/plugins/enhancer/enhance/index.ts | 25 ++- packages/sdk/src/utils.ts | 2 +- packages/testtools/src/schema.ts | 12 +- .../enhancements/with-policy/auth.test.ts | 170 +++++++++++++++++- 7 files changed, 345 insertions(+), 13 deletions(-) create mode 100644 packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts diff --git a/packages/runtime/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index be2fc4579..9ab27448c 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -94,8 +94,8 @@ export type InternalEnhancementOptions = EnhancementOptions & { /** * Context for creating enhanced `PrismaClient` */ -export type EnhancementContext = { - user?: AuthUser; +export type EnhancementContext = { + user?: User; }; let hasPassword: boolean | undefined = undefined; diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index e197e18c1..c76812a51 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -4,6 +4,7 @@ import { getIdFields } from '../../cross'; import { DbClientContract } from '../../types'; import { hasAllFields } from '../../validation'; import type { EnhancementContext, InternalEnhancementOptions } from '../create-enhancement'; +import { Logger } from '../logger'; import { makeProxy } from '../proxy'; import { PolicyProxyHandler } from './handler'; @@ -44,7 +45,8 @@ export function withPolicy( 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`); } }); } 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..d8e53c173 --- /dev/null +++ b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts @@ -0,0 +1,141 @@ +import { getIdFields, 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 + 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 { + // field is a scalar + addPickField(fieldDecl.$container.name, node.target.$refText); + } + } + }); + }); + + // generate: + // ` + // namespace auth { + // export type User = WithRequired, 'id'> & { profile: Profile; }; + // export type Profile = WithRequired, 'age'>; + // } + // ` + + 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('|')}>`; + } + + 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; +} diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index a379e5ad0..afb1aa776 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -2,6 +2,7 @@ import type { DMMF } from '@prisma/generator-helper'; import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { getAttribute, + getAuthModel, getDataModels, getDMMF, getPrismaClientImportSpec, @@ -28,6 +29,7 @@ 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[]][]; @@ -62,16 +64,31 @@ export async function generate(model: Model, options: PluginOptions, project: Pr await prismaDts.save(); } + const authModel = getAuthModel(getDataModels(model)); + const authTypes = authModel ? generateAuthType(model, authModel) : ''; + const authTypeParam = authModel ? `auth.${authModel.name}` : 'AuthUser'; + const prismaImport = getPrismaClientImportSpec(outDir, options); + const enhanceTs = project.createSourceFile( path.join(outDir, 'enhance.ts'), - `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas } from '@zenstackhq/runtime'; + `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; import modelMeta from './model-meta'; import policy from './policy'; +import { Prisma } from '${prismaImport}'; +${ + withLogicalClient + ? `import type * as _P from '${logicalPrismaClientDir}/index-fixed'; +import type { PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; +` + : `import type * as _P from '${prismaImport}'; +import type { PrismaClient } from '${prismaImport}'; +` +} ${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} -import { Prisma } from '${getPrismaClientImportSpec(outDir, options)}'; -${withLogicalClient ? `import { type PrismaClient } from '${logicalPrismaClientDir}/index-fixed';` : ``} -export function enhance(prisma: DbClient, context?: EnhancementContext, options?: EnhancementOptions)${ +${authTypes} + +export function enhance(prisma: DbClient, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions)${ withLogicalClient ? ': PrismaClient' : '' } { return createEnhancement(prisma, { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 4c79d4c9c..b33f8a257 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -35,7 +35,7 @@ import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; 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)); diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index ecdad8336..b744aa578 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -251,13 +251,17 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey })); } + opt.extraSourceFiles?.forEach(({ name, content }) => { + fs.writeFileSync(path.join(projectRoot, 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...'); - opt.extraSourceFiles?.forEach(({ name, content }) => { - fs.writeFileSync(path.join(projectRoot, name), content); - }); - run('npx tsc --init'); // add generated '.zenstack/zod' folder to typescript's search path, diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index e2655b36a..9079da045 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 () => { @@ -618,3 +618,171 @@ describe('With Policy: auth() test', () => { ).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 } ] } ] } }); + `, + }, + ], + } + ); + }); +}); From 7ec9b8f00eb1494910a93a17bd7bba6a37505cee Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 15 Mar 2024 12:43:13 -0700 Subject: [PATCH 064/127] feat(tanstack-query): allow to pass in a custom callback for computing optimistic data (#1143) --- .../tanstack-query/src/runtime/common.ts | 82 +++++++++++++++- .../tests/react-hooks-v5.test.tsx | 96 +++++++++++++++++++ 2 files changed, 175 insertions(+), 3 deletions(-) diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts index 95479da75..40830b3ee 100644 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ b/packages/plugins/tanstack-query/src/runtime/common.ts @@ -40,6 +40,46 @@ 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. */ @@ -53,6 +93,11 @@ export type ExtraMutationOptions = { * 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; }; /** @@ -274,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, @@ -294,6 +342,7 @@ export function setupOptimisticUpdate( model, operation as PrismaWriteActionType, variables, + options, modelMeta, queryCache, setCache, @@ -324,6 +373,7 @@ async function optimisticUpdate( mutationModel: string, mutationOp: string, mutationArgs: any, + options: MutationOptions & ExtraMutationOptions, modelMeta: ModelMeta, queryCache: QueryCache, setCache: SetCacheFunc, @@ -342,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`); @@ -350,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/tests/react-hooks-v5.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx index ed6af9f2e..404b3e57b 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx @@ -618,4 +618,100 @@ 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 }; + }); + + 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' } })); + + // check 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'); + }); + }); }); From 95b8495667a6e518442f0d281a39bc012502ee99 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 15 Mar 2024 14:36:23 -0700 Subject: [PATCH 065/127] feat(swr): allow to provide a custom optimistic update data provider (#1144) --- packages/plugins/swr/src/runtime/index.ts | 79 ++++++++++++++- .../plugins/swr/tests/react-hooks.test.tsx | 96 +++++++++++++++++++ .../tests/react-hooks-v5.test.tsx | 5 +- 3 files changed, 173 insertions(+), 7 deletions(-) diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts index 0deeefdcc..0ca4212cc 100644 --- a/packages/plugins/swr/src/runtime/index.ts +++ b/packages/plugins/swr/src/runtime/index.ts @@ -107,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. */ @@ -115,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'>; /** @@ -242,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); @@ -410,6 +455,7 @@ async function optimisticUpdate( mutationModel: string, mutationOp: string, mutationArgs: any, + options: MutationOptions | undefined, modelMeta: ModelMeta, cache: Cache, mutator: ScopedMutator, @@ -430,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/tanstack-query/tests/react-hooks-v5.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx index 404b3e57b..d5e23c374 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx @@ -647,7 +647,8 @@ describe('Tanstack Query React Hooks V5 Test', () => { .reply(200, () => { console.log('Not mutating data'); return { data: null }; - }); + }) + .persist(); const { result: mutationResult1 } = renderHook( () => @@ -669,7 +670,7 @@ describe('Tanstack Query React Hooks V5 Test', () => { act(() => mutationResult1.current.mutate({ data: { name: 'foo' } })); - // check should not update + // cache should not update await waitFor(() => { const cacheData: any = queryClient.getQueryData( getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) From 01e372e9e6454a4016eda161e964889c5639c537 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 15 Mar 2024 23:04:14 -0700 Subject: [PATCH 066/127] fix(polymorphism): incorrect typing generated for nested create/update input (#1145) --- .../src/plugins/enhancer/enhance/index.ts | 761 ++++++++++-------- packages/schema/src/plugins/enhancer/index.ts | 6 +- .../src/plugins/prisma/schema-generator.ts | 71 +- .../with-delegate/enhanced-client.test.ts | 38 +- .../with-delegate/issue-1135.test.ts | 85 ++ .../tests/enhancements/with-delegate/utils.ts | 28 + 6 files changed, 633 insertions(+), 356 deletions(-) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1135.test.ts diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index afb1aa776..35bc246b4 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -2,6 +2,7 @@ import type { DMMF } from '@prisma/generator-helper'; import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { getAttribute, + getAttributeArg, getAuthModel, getDataModels, getDMMF, @@ -10,7 +11,15 @@ import { PluginError, type PluginOptions, } from '@zenstackhq/sdk'; -import { DataModel, DataModelField, isDataModel, isReferenceExpr, type Model } from '@zenstackhq/sdk/ast'; +import { + DataModel, + DataModelField, + isArrayExpr, + isDataModel, + isReferenceExpr, + ReferenceExpr, + type Model, +} from '@zenstackhq/sdk/ast'; import fs from 'fs'; import path from 'path'; import { @@ -24,6 +33,7 @@ import { TypeAliasDeclaration, VariableStatement, } from 'ts-morph'; +import { upperCaseFirst } from 'upper-case-first'; import { name } from '..'; import { execPackage } from '../../../utils/exec-utils'; import { trackPrismaSchemaError } from '../../prisma'; @@ -34,413 +44,494 @@ import { generateAuthType } from './auth-type-generator'; // information of delegate models and their sub models type DelegateInfo = [DataModel, DataModel[]][]; -export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { - let logicalPrismaClientDir: string | undefined; - let dmmf: DMMF.Document | undefined; - - const withLogicalClient = needsLogicalClient(model); - - if (withLogicalClient) { - // schema contains delegate models, need to generate a logical prisma schema - const result = await generateLogicalPrisma(model, options, outDir); - - logicalPrismaClientDir = './.logical-prisma-client'; - dmmf = result.dmmf; +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 withLogicalClient = this.needsLogicalClient(); + const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); + + if (withLogicalClient) { + // 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, 'prisma.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, 'prisma.d.ts'), + `export type * from '${prismaImport}';`, + { overwrite: true } + ); + await prismaDts.save(); + } - // create a reexport of the logical prisma client - const prismaDts = project.createSourceFile( - path.join(outDir, 'prisma.d.ts'), - `export type * from '${logicalPrismaClientDir}/index-fixed';`, - { overwrite: true } - ); - await prismaDts.save(); - } else { - // just reexport the prisma client - const prismaDts = project.createSourceFile( - path.join(outDir, 'prisma.d.ts'), - `export type * from '${getPrismaClientImportSpec(outDir, options)}';`, + 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 { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; + import modelMeta from './model-meta'; + import policy from './policy'; + import { Prisma } from '${prismaImport}'; + ${ + withLogicalClient + ? `import type * as _P from '${logicalPrismaClientDir}/index-fixed'; + import type { PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; + ` + : `import type * as _P from '${prismaImport}'; + import type { PrismaClient } from '${prismaImport}'; + ` + } + ${this.options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} + + ${authTypes} + + export function enhance(prisma: DbClient, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions)${ + withLogicalClient ? ': PrismaClient' : '' + } { + return createEnhancement(prisma, { + modelMeta, + policy, + zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), + prismaModule: Prisma, + ...options + }, context)${withLogicalClient ? ' as PrismaClient' : ''}; + } + `, { overwrite: true } ); - await prismaDts.save(); - } - const authModel = getAuthModel(getDataModels(model)); - const authTypes = authModel ? generateAuthType(model, authModel) : ''; - const authTypeParam = authModel ? `auth.${authModel.name}` : 'AuthUser'; - const prismaImport = getPrismaClientImportSpec(outDir, options); - - const enhanceTs = project.createSourceFile( - path.join(outDir, 'enhance.ts'), - `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; -import modelMeta from './model-meta'; -import policy from './policy'; -import { Prisma } from '${prismaImport}'; -${ - withLogicalClient - ? `import type * as _P from '${logicalPrismaClientDir}/index-fixed'; -import type { PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; -` - : `import type * as _P from '${prismaImport}'; -import type { PrismaClient } from '${prismaImport}'; -` -} -${options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} - -${authTypes} - -export function enhance(prisma: DbClient, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions)${ - withLogicalClient ? ': PrismaClient' : '' - } { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: Prisma, - ...options - }, context)${withLogicalClient ? ' as PrismaClient' : ''}; -} -`, - { overwrite: true } - ); + await this.saveSourceFile(enhanceTs); - await saveSourceFile(enhanceTs, options); + return { dmmf }; + } - return { dmmf }; -} + private needsLogicalClient() { + return this.hasDelegateModel(this.model) || this.hasAuthInDefault(this.model); + } -function needsLogicalClient(model: Model) { - return hasDelegateModel(model) || hasAuthInDefault(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)) + ); + } -function 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))) + ); + } -function 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', + }); -async function generateLogicalPrisma(model: Model, options: PluginOptions, outDir: string) { - const prismaGenerator = new PrismaSchemaGenerator(model); - const prismaClientOutDir = './.logical-prisma-client'; - const logicalPrismaFile = path.join(outDir, 'logical.prisma'); - await prismaGenerator.generate({ - provider: '@internal', // doesn't matter - schemaPath: 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); + // generate the prisma client + const generateCmd = `prisma generate --schema "${logicalPrismaFile}" --no-engine`; try { - // run 'prisma generate' again with output to the console - await execPackage(generateCmd); + // run 'prisma generate' + await execPackage(generateCmd, { stdio: 'ignore' }); } catch { - // noop + 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}`); } - 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 processClientTypes(model, path.join(outDir, prismaClientOutDir)); + // 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' }) }), - }; -} + return { + prismaSchema: logicalPrismaFile, + // load the dmmf of the logical prisma schema + dmmf: await getDMMF({ datamodel: fs.readFileSync(logicalPrismaFile, { encoding: 'utf-8' }) }), + }; + } -async function processClientTypes(model: Model, 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 = []; - model.declarations - .filter((d): d is DataModel => isDelegateModel(d)) - .forEach((dm) => { - delegateInfo.push([ - dm, - model.declarations.filter( - (d): d is DataModel => isDataModel(d) && d.superTypes.some((s) => s.ref === dm) - ), - ]); + 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) + ), + ]); + }); + + const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { + overwrite: true, }); - const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { - overwrite: true, - }); - - if (delegateInfo.length > 0) { - // transform types for delegated models - transformDelegate(sf, sfNew, delegateInfo); - sfNew.formatText(); - } else { - // just copy - sfNew.replaceWithText(sf.getFullText()); + 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(); } - await sfNew.save(); -} - -function transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateModels: 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())); + private transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { + // copy toplevel imports + sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); - // copy toplevel exports - sfNew.addExportAssignments(sf.getExportAssignments().map((n) => n.getStructure())); + // copy toplevel import equals + sfNew.addStatements(sf.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText())); - // copy toplevel type aliases - sfNew.addTypeAliases(sf.getTypeAliases().map((n) => n.getStructure())); + // copy toplevel exports + sfNew.addExportAssignments(sf.getExportAssignments().map((n) => n.getStructure())); - // copy toplevel classes - sfNew.addClasses(sf.getClasses().map((n) => n.getStructure())); + // copy toplevel type aliases + sfNew.addTypeAliases(sf.getTypeAliases().map((n) => n.getStructure())); - // copy toplevel variables - sfNew.addVariableStatements(sf.getVariableStatements().map((n) => n.getStructure())); + // copy toplevel classes + sfNew.addClasses(sf.getClasses().map((n) => n.getStructure())); - // copy toplevel namespaces except for `Prisma` - sfNew.addModules( - sf - .getModules() - .filter((n) => n.getName() !== 'Prisma') - .map((n) => n.getStructure()) - ); + // copy toplevel variables + sfNew.addVariableStatements(sf.getVariableStatements().map((n) => n.getStructure())); - // transform the `Prisma` namespace - const prismaModule = sf.getModuleOrThrow('Prisma'); - const newPrismaModule = sfNew.addModule({ name: 'Prisma', isExported: true }); - transformPrismaModule(prismaModule, newPrismaModule, delegateModels); -} + // copy toplevel namespaces except for `Prisma` + sfNew.addModules( + sf + .getModules() + .filter((n) => n.getName() !== 'Prisma') + .map((n) => n.getStructure()) + ); -function transformPrismaModule( - prismaModule: ModuleDeclaration, - newPrismaModule: ModuleDeclaration, - delegateInfo: DelegateInfo -) { - // module block is the direct container of declarations inside a namespace - const moduleBlock = prismaModule.getFirstChildByKindOrThrow(SyntaxKind.ModuleBlock); + // transform the `Prisma` namespace + const prismaModule = sf.getModuleOrThrow('Prisma'); + const newPrismaModule = sfNew.addModule({ name: 'Prisma', isExported: true }); + this.transformPrismaModule(prismaModule, newPrismaModule, delegateInfo); + } - // most of the toplevel constructs should be copied over - // here we use ts-morph batch operations for optimal performance + 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); - // copy imports - newPrismaModule.addStatements( - moduleBlock.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText()) - ); + // most of the toplevel constructs should be copied over + // here we use ts-morph batch operations for optimal performance - // copy classes - newPrismaModule.addClasses(moduleBlock.getClasses().map((n) => n.getStructure())); + // copy imports + newPrismaModule.addStatements( + moduleBlock.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText()) + ); - // copy functions - newPrismaModule.addFunctions( - moduleBlock.getFunctions().map((n) => n.getStructure() as FunctionDeclarationStructure) - ); + // copy classes + newPrismaModule.addClasses(moduleBlock.getClasses().map((n) => n.getStructure())); - // copy nested namespaces - newPrismaModule.addModules(moduleBlock.getModules().map((n) => n.getStructure())); + // copy functions + newPrismaModule.addFunctions( + moduleBlock.getFunctions().map((n) => n.getStructure() as FunctionDeclarationStructure) + ); - // transform variables - const newVariables = moduleBlock.getVariableStatements().map((variable) => transformVariableStatement(variable)); - newPrismaModule.addVariableStatements(newVariables); + // copy nested namespaces + newPrismaModule.addModules(moduleBlock.getModules().map((n) => n.getStructure())); - // transform interfaces - const newInterfaces = moduleBlock.getInterfaces().map((iface) => transformInterface(iface, delegateInfo)); - newPrismaModule.addInterfaces(newInterfaces); + // transform variables + const newVariables = moduleBlock + .getVariableStatements() + .map((variable) => this.transformVariableStatement(variable)); + newPrismaModule.addVariableStatements(newVariables); - // transform type aliases - const newTypeAliases = moduleBlock.getTypeAliases().map((typeAlias) => transformTypeAlias(typeAlias, delegateInfo)); - newPrismaModule.addTypeAliases(newTypeAliases); -} + // transform interfaces + const newInterfaces = moduleBlock.getInterfaces().map((iface) => this.transformInterface(iface, delegateInfo)); + newPrismaModule.addInterfaces(newInterfaces); -function transformVariableStatement(variable: VariableStatement) { - const structure = variable.getStructure(); + // transform type aliases + const newTypeAliases = moduleBlock + .getTypeAliases() + .map((typeAlias) => this.transformTypeAlias(typeAlias, delegateInfo)); + newPrismaModule.addTypeAliases(newTypeAliases); + } - // remove `delegate_aux_*` fields from the variable's typing - const auxFields = findAuxDecls(variable); - if (auxFields.length > 0) { - structure.declarations.forEach((variable) => { - let source = variable.type?.toString(); - auxFields.forEach((f) => { - source = source?.replace(f.getText(), ''); + 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; }); - variable.type = source; - }); + } + + return structure; } - return structure; -} + private transformInterface(iface: InterfaceDeclaration, delegateInfo: DelegateInfo) { + const structure = iface.getStructure(); -function 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 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)); - // 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)); + } - 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; } - return structure; -} + private transformTypeAlias(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo) { + const structure = typeAlias.getStructure(); + let source = structure.type as string; -function transformTypeAlias(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo) { - const structure = typeAlias.getStructure(); - let source = structure.type as string; + // remove aux fields + source = this.removeAuxFieldsFromTypeAlias(typeAlias, source); - // remove aux fields - source = removeAuxFieldsFromTypeAlias(typeAlias, source); + // remove discriminator field from concrete input types + source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); - // remove discriminator field from concrete input types - source = removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); + // remove create/connectOrCreate/upsert fields from delegate's input types + source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); - // remove create/connectOrCreate/upsert fields from delegate's input types - source = removeCreateFromDelegateInput(typeAlias, delegateInfo, source); + // remove delegate fields from nested mutation input types + source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); - // fix delegate payload union type - source = fixDelegatePayloadType(typeAlias, delegateInfo, source); + // fix delegate payload union type + source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); - structure.type = source; - return structure; -} + 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; + } -function 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 = getDiscriminatorField(payloadRecord[0]); - if (discriminatorDecl) { - source = `${payloadRecord[1] - .map( - (concrete) => - `($${concrete.name}Payload & { scalars: { ${discriminatorDecl.name}: '${concrete.name}' } })` - ) - .join(' | ')}`; + 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; } - return source; -} -function 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(), ''); - }); + 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; } - return source; -} -function 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 = getDiscriminatorFieldsRecursively(delegateOfConcrete); - discriminators.forEach((discriminatorDecl) => { - const discriminatorNode = findNamedProperty(typeAlias, discriminatorDecl.name); - if (discriminatorNode) { - source = source.replace(discriminatorNode.getText(), ''); - } + 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; } - return source; -} -function removeAuxFieldsFromTypeAlias(typeAlias: TypeAliasDeclaration, source: string) { - // remove `delegate_aux_*` fields from the type alias - const auxDecls = findAuxDecls(typeAlias); - if (auxDecls.length > 0) { - auxDecls.forEach((d) => { - source = source.replace(d.getText(), ''); + 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; -} -function findNamedProperty(typeAlias: TypeAliasDeclaration, name: string) { - return typeAlias.getFirstDescendant((d) => d.isKind(SyntaxKind.PropertySignature) && d.getName() === name); -} + return source; + } -function findAuxDecls(node: Node) { - return node - .getDescendantsOfKind(SyntaxKind.PropertySignature) - .filter((n) => n.getName().startsWith(DELEGATE_AUX_RELATION_PREFIX)); -} + private findNamedProperty(typeAlias: TypeAliasDeclaration, name: string) { + return typeAlias.getFirstDescendant((d) => d.isKind(SyntaxKind.PropertySignature) && d.getName() === name); + } -function getDiscriminatorField(delegate: DataModel) { - const delegateAttr = getAttribute(delegate, '@@delegate'); - if (!delegateAttr) { - return undefined; + private findAuxDecls(node: Node) { + return node + .getDescendantsOfKind(SyntaxKind.PropertySignature) + .filter((n) => n.getName().startsWith(DELEGATE_AUX_RELATION_PREFIX)); } - const arg = delegateAttr.args[0]?.value; - return isReferenceExpr(arg) ? (arg.target.ref as DataModelField) : undefined; -} -function getDiscriminatorFieldsRecursively(delegate: DataModel, result: DataModelField[] = []) { - if (isDelegateModel(delegate)) { - const discriminator = getDiscriminatorField(delegate); - if (discriminator) { - result.push(discriminator); + 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(...getDiscriminatorFieldsRecursively(superType.ref, result)); + for (const superType of delegate.superTypes) { + if (superType.ref) { + result.push(...this.getDiscriminatorFieldsRecursively(superType.ref, result)); + } } } + return result; } - return result; -} -async function saveSourceFile(sf: SourceFile, options: PluginOptions) { - if (options.preserveTsFiles) { - await sf.save(); + private async saveSourceFile(sf: SourceFile) { + if (this.options.preserveTsFiles) { + await sf.save(); + } } } diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index 64e2ad1a4..df518a52e 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -1,7 +1,7 @@ -import { PluginError, createProject, resolvePath, type PluginFunction, RUNTIME_PACKAGE } from '@zenstackhq/sdk'; +import { PluginError, RUNTIME_PACKAGE, createProject, resolvePath, type PluginFunction } from '@zenstackhq/sdk'; import path from 'path'; import { getDefaultOutputFolder } from '../plugin-utils'; -import { generate as generateEnhancer } from './enhance'; +import { EnhancerGenerator } from './enhance'; import { generate as generateModelMeta } from './model-meta'; import { generate as generatePolicy } from './policy'; @@ -19,7 +19,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { await generateModelMeta(model, options, project, outDir); await generatePolicy(model, options, project, outDir); - const { dmmf } = await generateEnhancer(model, options, project, outDir); + const { dmmf } = await new EnhancerGenerator(model, options, project, outDir).generate(); let prismaClientPath: string | undefined; if (dmmf) { diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index bfcecc9ef..649362eff 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -35,6 +35,7 @@ import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackh import { getAttribute, getAttributeArg, + getAttributeArgLiteral, getForeignKeyFields, getLiteral, getPrismaVersion, @@ -385,8 +386,8 @@ export class PrismaSchemaGenerator { // the logical schema needs to expand relations to the delegate models to concrete ones - // for the given model, find all concrete models that have relation to it, - // and generate an auxiliary opposite relation field + // 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)) { @@ -398,9 +399,12 @@ export class PrismaSchemaGenerator { (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}_${lowerCaseFirst(concrete.name)}`, + `${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'); @@ -408,28 +412,31 @@ export class PrismaSchemaGenerator { const fieldsArg = relAttr.args.find((arg) => arg.name === 'fields'); if (fieldsArg) { const idFields = getIdFields(fieldType); - idFields.forEach((idField) => { - model.addField( - `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(concrete.name)}${upperCaseFirst( - idField.name - )}`, - idField.type.type! - ); - }); - const args = new AttributeArgValue( + // 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( - (idField) => - new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) + (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', args), - new PrismaAttributeArg('references', args), + new PrismaAttributeArg('fields', fieldsArg), + new PrismaAttributeArg('references', referencesArg), ]); if (this.supportNamedConstraints) { @@ -475,8 +482,23 @@ export class PrismaSchemaGenerator { 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}_${lowerCaseFirst(decl.name)}`; + const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${fieldType.name}_${oppositeRelationField.name}_${decl.name}`; if (relAttr) { const nameArg = getAttributeArg(relAttr, 'name'); @@ -500,6 +522,21 @@ export class PrismaSchemaGenerator { }); } + 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) { diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index f4b4e63f8..6ec03486f 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -2,7 +2,7 @@ import { PrismaErrorCode } from '@zenstackhq/runtime'; import { loadSchema, run } from '@zenstackhq/testtools'; import fs from 'fs'; import path from 'path'; -import { POLYMORPHIC_SCHEMA } from './utils'; +import { POLYMORPHIC_MANY_TO_MANY_SCHEMA, POLYMORPHIC_SCHEMA } from './utils'; describe('Polymorphism Test', () => { const schema = POLYMORPHIC_SCHEMA; @@ -1003,6 +1003,42 @@ describe('Polymorphism Test', () => { ); }); + 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', async () => { const { projectDir } = await loadSchema(schema, { enhancements: ['delegate'] }); const src = ` 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/utils.ts b/tests/integration/tests/enhancements/with-delegate/utils.ts index 0de8a7e8b..c04106ea2 100644 --- a/tests/integration/tests/enhancements/with-delegate/utils.ts +++ b/tests/integration/tests/enhancements/with-delegate/utils.ts @@ -45,3 +45,31 @@ model Gallery { 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 +} +`; From b3fa1edf43ff18a1ae90be9d1449e003101f27c4 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 17 Mar 2024 11:12:07 -0700 Subject: [PATCH 067/127] fix: preserve prisma client extensions's typing (#1148) --- .../src/plugins/enhancer/enhance/index.ts | 36 +++--- tests/integration/tests/cli/plugins.test.ts | 4 +- .../with-delegate/enhanced-client.test.ts | 105 +++++++++++++----- 3 files changed, 100 insertions(+), 45 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 35bc246b4..fbb8a442a 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -92,31 +92,39 @@ export class EnhancerGenerator { `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; import modelMeta from './model-meta'; import policy from './policy'; - import { Prisma } from '${prismaImport}'; + import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaImport}'; + import type { InternalArgs, TypeMapDef, TypeMapCbDef, DynamicClientExtensionThis } from '${prismaImport}/runtime/library'; ${ withLogicalClient ? `import type * as _P from '${logicalPrismaClientDir}/index-fixed'; - import type { PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; + import type { Prisma, PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; ` : `import type * as _P from '${prismaImport}'; - import type { PrismaClient } from '${prismaImport}'; + import type { Prisma, PrismaClient } from '${prismaImport}'; ` } ${this.options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} ${authTypes} +// overload for plain PrismaClient +export function enhance & InternalArgs>( + prisma: _PrismaClient, + context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): PrismaClient; - export function enhance(prisma: DbClient, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions)${ - withLogicalClient ? ': PrismaClient' : '' - } { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: Prisma, - ...options - }, context)${withLogicalClient ? ' as 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); +} `, { overwrite: true } ); diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index ee32ce9e9..b5dbf9fb8 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -73,7 +73,7 @@ describe('CLI Plugins Tests', () => { 'swr', '@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')}`, @@ -83,7 +83,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')}`, diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index 6ec03486f..076202553 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -1,7 +1,5 @@ import { PrismaErrorCode } from '@zenstackhq/runtime'; -import { loadSchema, run } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; +import { loadSchema } from '@zenstackhq/testtools'; import { POLYMORPHIC_MANY_TO_MANY_SCHEMA, POLYMORPHIC_SCHEMA } from './utils'; describe('Polymorphism Test', () => { @@ -1039,8 +1037,7 @@ describe('Polymorphism Test', () => { ); }); - it('typescript compilation', async () => { - const { projectDir } = await loadSchema(schema, { enhancements: ['delegate'] }); + it('typescript compilation plain prisma', async () => { const src = ` import { PrismaClient } from '@prisma/client'; import { enhance } from '.zenstack/enhance'; @@ -1048,7 +1045,6 @@ describe('Polymorphism Test', () => { const prisma = new PrismaClient(); async function main() { - await prisma.user.deleteMany(); const db = enhance(prisma); const user1 = await db.user.create({ data: { } }); @@ -1084,31 +1080,82 @@ describe('Polymorphism Test', () => { } } - main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); + main(); `; - - fs.writeFileSync(path.join(projectDir, 'script.ts'), src); - fs.writeFileSync( - path.join(projectDir, 'tsconfig.json'), - JSON.stringify({ - compilerOptions: { - outDir: 'dist', - strict: true, - lib: ['esnext'], - esModuleInterop: true, + await loadSchema(schema, { + compile: true, + enhancements: ['delegate'], + extraSourceFiles: [ + { + name: 'main.ts', + content: src, }, - }) - ); + ], + }); + }); - run('npm i -D @types/node', undefined, projectDir); - run('npx tsc --noEmit --skipLibCheck script.ts', undefined, projectDir); + 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, + }, + ], + }); }); }); From 2d822182717f7ae1aa313d90500387abd6507a8d Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 17 Mar 2024 11:32:17 -0700 Subject: [PATCH 068/127] chore: improve `enhance` API code generation (#1150) --- .../src/plugins/enhancer/enhance/index.ts | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index fbb8a442a..7b042f05a 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -56,10 +56,9 @@ export class EnhancerGenerator { let logicalPrismaClientDir: string | undefined; let dmmf: DMMF.Document | undefined; - const withLogicalClient = this.needsLogicalClient(); const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); - if (withLogicalClient) { + if (this.needsLogicalClient()) { // schema contains delegate models, need to generate a logical prisma schema const result = await this.generateLogicalPrisma(); @@ -90,22 +89,67 @@ export class EnhancerGenerator { const enhanceTs = this.project.createSourceFile( path.join(this.outDir, 'enhance.ts'), `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; - import modelMeta from './model-meta'; - import policy from './policy'; - import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaImport}'; - import type { InternalArgs, TypeMapDef, TypeMapCbDef, DynamicClientExtensionThis } from '${prismaImport}/runtime/library'; - ${ - withLogicalClient - ? `import type * as _P from '${logicalPrismaClientDir}/index-fixed'; - import type { Prisma, PrismaClient } from '${logicalPrismaClientDir}/index-fixed'; - ` - : `import type * as _P from '${prismaImport}'; - import type { Prisma, PrismaClient } from '${prismaImport}'; - ` +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 }; } - ${this.options.withZodSchemas ? "import * as zodSchemas from './zod';" : 'const zodSchemas = undefined;'} - - ${authTypes} + + 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, @@ -125,13 +169,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara ...options }, context); } - `, - { overwrite: true } - ); - - await this.saveSourceFile(enhanceTs); - - return { dmmf }; +`; } private needsLogicalClient() { From 1488bcf9d8dad9b83beb93e06c2d8a88671738cb Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 17 Mar 2024 17:35:28 -0700 Subject: [PATCH 069/127] fix(polymorphism): `createMany` is handled incorrectly (#1151) --- packages/runtime/src/enhancements/delegate.ts | 2 +- .../with-delegate/enhanced-client.test.ts | 37 ++++++ .../with-delegate/issue-1149.test.ts | 112 ++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1149.test.ts diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index 12c249912..061a65114 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -390,7 +390,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { 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, item); + return this.doCreate(tx, this.model, { data: item }); }) ); diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index 076202553..6a31540d7 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -121,6 +121,43 @@ describe('Polymorphism Test', () => { ).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(); 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 }), + ]) + ); + }); +}); From aa8b182cf8412ce76c9b668e686b2cb68eef82d7 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 17 Mar 2024 17:37:49 -0700 Subject: [PATCH 070/127] chore: bump version (#1152) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index ac2d35cbb..f36cd3a3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 15a60f0a4..95cada36f 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-alpha.7" +version = "2.0.0-beta.1" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 19b81ebce..62703919a 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 1b98f4062..2c97322bb 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index c3df79e42..7176d9459 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": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index fe5034310..57d6d601e 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": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 7dc6fd5e8..089f6f886 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": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 64d5c3f63..6597e1e7f 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": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index d7427347b..b27ac2737 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": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b3a108019..889ebe826 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index fc04065bb..80a8fbfa3 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": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 02bd6c681..8932b1bc0 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index a6dd78931..01fd97bb6 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 13c883dcc..43bf6dd88 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-alpha.7", + "version": "2.0.0-beta.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 13f8c73ac39ab2d9a1474383729eba453e83f0a2 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 18 Mar 2024 21:01:58 -0700 Subject: [PATCH 071/127] fix: issue with locating .zenstack package (#1157) --- packages/schema/src/plugins/plugin-utils.ts | 29 +++++++-------------- packages/server/src/shared.ts | 18 ++++++++++--- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index f405604d0..3632dedc3 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -3,7 +3,6 @@ import { PluginGlobalOptions } from '@zenstackhq/sdk'; import fs from 'fs'; import path from 'path'; import { PluginRunnerOptions } from '../cli/plugin-runner'; -import { getPackageManager } from '../utils/pkg-utils'; export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; @@ -82,27 +81,17 @@ export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { return path.join(process.cwd(), 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); } - const { projectRoot } = getPackageManager(__dirname); - if (fs.existsSync(path.join(projectRoot, 'node_modules'))) { - // use the located node_modules folder - return path.join(projectRoot, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); - } else { - // unable to locate a node_modules folder, fallback to where the runtime - // package resides - - // find the real runtime module path, it might be a symlink in pnpm - let runtimeModulePath = require.resolve('@zenstackhq/runtime'); + // find the real runtime module path, it might be a symlink in pnpm + let runtimeModulePath = require.resolve('@zenstackhq/runtime'); - 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, '..'); - } - runtimeModulePath = path.join(runtimeModulePath, '..'); - } - const modulesFolder = getNodeModulesFolder(runtimeModulePath); - return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined; + // 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; } /** diff --git a/packages/server/src/shared.ts b/packages/server/src/shared.ts index 7538411c5..eaeef2cb3 100644 --- a/packages/server/src/shared.ts +++ b/packages/server/src/shared.ts @@ -33,7 +33,11 @@ export function getDefaultModelMeta(loadPath: string | undefined): ModelMeta { const toLoad = path.resolve(loadPath, 'model-meta'); return require(toLoad).default; } else { - return require('.zenstack/model-meta').default; + // model-meta should be resolved relative to the runtime + const metaPath = require.resolve('.zenstack/model-meta', { + paths: [require.resolve('@zenstackhq/runtime')], + }); + return require(metaPath).default; } } catch { if (process.env.ZENSTACK_TEST === '1' && !loadPath) { @@ -61,7 +65,11 @@ export function getDefaultPolicy(loadPath: string | undefined): PolicyDef { const toLoad = path.resolve(loadPath, 'policy'); return require(toLoad).default; } else { - return require('.zenstack/policy').default; + // policy should be resolved relative to the runtime + const policyPath = require.resolve('.zenstack/policy', { + paths: [require.resolve('@zenstackhq/runtime')], + }); + return require(policyPath).default; } } catch { if (process.env.ZENSTACK_TEST === '1' && !loadPath) { @@ -92,7 +100,11 @@ export function getDefaultZodSchemas(loadPath: string | undefined): ZodSchemas | const toLoad = path.resolve(loadPath, 'zod'); return require(toLoad); } else { - return require('.zenstack/zod'); + // policy should be resolved relative to the runtime + const zodPath = require.resolve('.zenstack/zod', { + paths: [require.resolve('@zenstackhq/runtime')], + }); + return require(zodPath); } } catch { if (process.env.ZENSTACK_TEST === '1' && !loadPath) { From 16ccedac4fdc0a2780e3146b5e4d40c2669506ca Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 18 Mar 2024 21:03:33 -0700 Subject: [PATCH 072/127] chore: bump version (#1158) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index f36cd3a3f..39e293ea5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 95cada36f..bf5e883f5 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.1" +version = "2.0.0-beta.2" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 62703919a..dda806b6f 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 2c97322bb..a53e284f0 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 7176d9459..6bf460af2 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": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 57d6d601e..2ac43391b 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": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 089f6f886..ea1f47891 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": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 6597e1e7f..55d25e3f2 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": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index b27ac2737..ee9bbb25d 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": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 889ebe826..4a15422b5 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 80a8fbfa3..190f27aad 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": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8932b1bc0..f704f2ca5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 01fd97bb6..6dec15cab 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 43bf6dd88..7044af3f6 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From c13a1fd24df1865280b20010dde16413ea003cde Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 24 Mar 2024 15:37:56 -0700 Subject: [PATCH 073/127] fix(api-handler): return http 422 when the request is rejected due to validation errors (#1170) --- .../plugins/openapi/src/rest-generator.ts | 13 + packages/plugins/openapi/src/rpc-generator.ts | 8 + .../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 | 228 +++ .../tests/baseline/rpc-3.1.0.baseline.yaml | 228 +++ .../rpc-type-coverage-3.0.0.baseline.yaml | 78 + .../rpc-type-coverage-3.1.0.baseline.yaml | 78 + packages/server/src/api/rest/index.ts | 24 +- packages/server/src/api/rpc/index.ts | 11 +- packages/server/tests/api/rest.test.ts | 6 +- packages/server/tests/api/rpc.test.ts | 15 +- packages/server/tests/utils.ts | 4 +- 15 files changed, 1636 insertions(+), 833 deletions(-) diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 0bb76251e..b1ed77829 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -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 8aa9189d0..724e58839 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -516,6 +516,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', + }, }, }; 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 0f936771e..c583a19ed 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 @@ -3734,6 +3734,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: @@ -3773,6 +3779,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: @@ -3812,6 +3824,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 @@ -3861,6 +3879,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 @@ -3912,6 +3936,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 @@ -3961,6 +3991,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: @@ -4000,6 +4036,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: @@ -4039,6 +4081,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: @@ -4081,6 +4129,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: @@ -4120,6 +4174,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 @@ -4171,6 +4231,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 @@ -4220,6 +4286,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 @@ -4271,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 @@ -4320,6 +4398,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: @@ -4359,6 +4443,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: @@ -4398,6 +4488,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 @@ -4447,6 +4543,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 @@ -4498,6 +4600,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 @@ -4547,6 +4655,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: @@ -4586,6 +4700,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: @@ -4625,6 +4745,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 +4790,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 @@ -4713,6 +4845,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 @@ -4764,6 +4902,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 @@ -4813,6 +4957,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 @@ -4864,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 @@ -4913,6 +5069,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: @@ -4952,6 +5114,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: @@ -4991,6 +5159,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 @@ -5040,6 +5214,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 @@ -5089,6 +5269,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: @@ -5128,6 +5314,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: @@ -5167,6 +5359,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 +5404,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 @@ -5255,6 +5459,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 @@ -5306,6 +5516,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 @@ -5355,6 +5571,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 @@ -5406,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 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 d842234ab..4ea9b2547 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 @@ -3788,6 +3788,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: @@ -3827,6 +3833,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: @@ -3866,6 +3878,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 @@ -3915,6 +3933,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 @@ -3966,6 +3990,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 @@ -4015,6 +4045,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: @@ -4054,6 +4090,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: @@ -4093,6 +4135,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: @@ -4135,6 +4183,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: @@ -4174,6 +4228,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 @@ -4225,6 +4285,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 @@ -4274,6 +4340,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 @@ -4325,6 +4397,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 @@ -4374,6 +4452,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: @@ -4413,6 +4497,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: @@ -4452,6 +4542,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 @@ -4501,6 +4597,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 @@ -4552,6 +4654,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 @@ -4601,6 +4709,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: @@ -4640,6 +4754,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: @@ -4679,6 +4799,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: @@ -4718,6 +4844,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 @@ -4767,6 +4899,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 @@ -4818,6 +4956,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 @@ -4867,6 +5011,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 @@ -4918,6 +5068,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 @@ -4967,6 +5123,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: @@ -5006,6 +5168,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: @@ -5045,6 +5213,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 @@ -5094,6 +5268,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 @@ -5143,6 +5323,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: @@ -5182,6 +5368,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: @@ -5221,6 +5413,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: @@ -5260,6 +5458,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 @@ -5309,6 +5513,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 @@ -5360,6 +5570,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 @@ -5409,6 +5625,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 @@ -5460,6 +5682,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 6e219e6e3..6d95c9a2f 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 @@ -2210,6 +2210,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: @@ -2250,6 +2256,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 +2302,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 @@ -2340,6 +2358,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 @@ -2392,6 +2416,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 @@ -2442,6 +2472,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: @@ -2482,6 +2518,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 +2564,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 +2610,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 @@ -2612,6 +2666,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 @@ -2664,6 +2724,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 @@ -2714,6 +2780,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 @@ -2766,6 +2838,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 e777f7580..cce476ffe 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 @@ -2242,6 +2242,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: @@ -2282,6 +2288,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: @@ -2322,6 +2334,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 @@ -2372,6 +2390,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 @@ -2424,6 +2448,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 @@ -2474,6 +2504,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: @@ -2514,6 +2550,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: @@ -2554,6 +2596,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: @@ -2594,6 +2642,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 @@ -2644,6 +2698,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 @@ -2696,6 +2756,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 @@ -2746,6 +2812,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 @@ -2798,6 +2870,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/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 52d700c63..5fbf05ee8 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 ), @@ -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 { diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index 983e79154..79e26bd79 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, diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index a7ff47d9a..d7944d6aa 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -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(); diff --git a/packages/server/tests/api/rpc.test.ts b/packages/server/tests/api/rpc.test.ts index 5d7708745..f3326f570 100644 --- a/packages/server/tests/api/rpc.test.ts +++ b/packages/server/tests/api/rpc.test.ts @@ -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 () => { 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 { From a0ca15d920c65068cd6efa9b785038231660c902 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 24 Mar 2024 16:09:42 -0700 Subject: [PATCH 074/127] chore: remove prettier dependency (#1171) --- packages/sdk/package.json | 1 - packages/sdk/src/code-gen.ts | 31 ++----------------------------- pnpm-lock.yaml | 3 --- 3 files changed, 2 insertions(+), 33 deletions(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f704f2ca5..7f37f19fa 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -25,7 +25,6 @@ "@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", 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/pnpm-lock.yaml b/pnpm-lock.yaml index 43aa526d9..5b6571803 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -635,9 +635,6 @@ importers: 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 From f14f21bb670276d9d59663ee0fc20635b2b8f515 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 24 Mar 2024 17:02:23 -0700 Subject: [PATCH 075/127] fix(zmodel): don't inherit `@@map` attribute from base (#1172) --- packages/schema/src/utils/ast-utils.ts | 8 +++++--- .../tests/regression/issue-1167.test.ts | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1167.test.ts diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 7f8071c6d..e771f3536 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -28,9 +28,9 @@ import { 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( @@ -68,6 +68,8 @@ export function mergeBaseModel(model: Model, linker: Linker) { .filter((attr) => !attr.$inheritedFrom) // don't inherit `@@delegate` attribute .filter((attr) => attr.decl.$refText !== '@@delegate') + // don't inherit `@@map` attribute + .filter((attr) => attr.decl.$refText !== '@@map') .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); } @@ -142,8 +144,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; } diff --git a/tests/integration/tests/regression/issue-1167.test.ts b/tests/integration/tests/regression/issue-1167.test.ts new file mode 100644 index 000000000..29b81adbf --- /dev/null +++ b/tests/integration/tests/regression/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") + } + ` + ); + }); +}); From 35b4be9bc03339a2aface8b224c7497509e9af68 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 25 Mar 2024 13:23:41 -0700 Subject: [PATCH 076/127] chore: bump version (#1175) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 39e293ea5..9b6ef50b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index bf5e883f5..9b911a7fd 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.2" +version = "2.0.0-beta.3" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index dda806b6f..19dbf9590 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 1aabcce15..53657afff 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 8694497af..70c9f0a8f 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": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 6f37f701b..2397b05f4 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": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 0511b0f5d..75ea35eb8 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": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 061ef8c6c..f4d4d8322 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": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index f7ab12a7a..6523b4a25 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": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 4a15422b5..86925a010 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 190f27aad..cb1a3238e 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": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7f37f19fa..c4a7c3aa6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 6dec15cab..4d77dd923 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 7044af3f6..88440c1d9 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 6e9d24254cee5e927c611d40224738d6338a73cc Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 25 Mar 2024 15:10:02 -0700 Subject: [PATCH 077/127] chore: disallow `this` in collection predicate (#1176) --- .../validator/expression-validator.ts | 29 ++++++++++++++++--- .../validation/datamodel-validation.test.ts | 23 +++++++++++++++ .../tests/regression/issue-925.test.ts | 3 +- .../tests/regression/issues.test.ts | 2 +- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 8a87ddc14..59ad7edbb 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -13,8 +13,8 @@ import { isThisExpr, } from '@zenstackhq/language/ast'; import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; -import { ValidationAcceptor } from 'langium'; -import { findUpAst, getContainingDataModel, isCollectionPredicate } from '../../utils/ast-utils'; +import { ValidationAcceptor, streamAst } from 'langium'; +import { findUpAst, getContainingDataModel } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { typeAssignable } from './utils'; @@ -32,8 +32,6 @@ 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, @@ -221,6 +219,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/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 4f44b109e..0bf12245d 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -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(` diff --git a/tests/integration/tests/regression/issue-925.test.ts b/tests/integration/tests/regression/issue-925.test.ts index b19d9d615..19ef210bf 100644 --- a/tests/integration/tests/regression/issue-925.test.ts +++ b/tests/integration/tests/regression/issue-925.test.ts @@ -35,7 +35,8 @@ describe('Regression: issue 925', () => { ).resolves.toContain("Could not resolve reference to ReferenceTarget named 'test'."); }); - it('reference with this', async () => { + // eslint-disable-next-line jest/no-disabled-tests + it.skip('reference with this', async () => { await loadModel( ` model User { diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index 7c2ca94cd..318682aad 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/issues.test.ts @@ -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) } From 5222c6c814c81316974058702d9722b090e23919 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 26 Mar 2024 22:26:36 -0700 Subject: [PATCH 078/127] feat: add Nest.js module for enhancing Prisma service (#1180) --- packages/server/README.md | 4 +- packages/server/package.json | 5 + packages/server/src/nestjs/index.ts | 1 + packages/server/src/nestjs/zenstack.module.ts | 98 ++++++ packages/server/tests/adapter/nestjs.test.ts | 163 ++++++++++ pnpm-lock.yaml | 302 +++++++++++++++--- 6 files changed, 530 insertions(+), 43 deletions(-) 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 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 4d77dd923..b7d38f5ee 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -37,7 +37,12 @@ "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, + "peerDependencies": { + "@nestjs/common": "^10.0.0" + }, "devDependencies": { + "@nestjs/platform-express": "^10.3.5", + "@nestjs/testing": "^10.3.5", "@sveltejs/kit": "1.21.0", "@types/body-parser": "^1.19.2", "@types/express": "^4.17.17", diff --git a/packages/server/src/nestjs/index.ts b/packages/server/src/nestjs/index.ts new file mode 100644 index 000000000..6fca28a71 --- /dev/null +++ b/packages/server/src/nestjs/index.ts @@ -0,0 +1 @@ +export { ZenStackModule } 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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 5b6571803..9fbfa6f98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -655,6 +655,9 @@ importers: packages/server: dependencies: + '@nestjs/common': + specifier: ^10.0.0 + version: 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime/dist @@ -686,6 +689,12 @@ importers: specifier: ^1.5.0 version: 1.5.0(zod@3.22.4) devDependencies: + '@nestjs/platform-express': + specifier: ^10.3.5 + version: 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5) + '@nestjs/testing': + specifier: ^10.3.5 + version: 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5)(@nestjs/platform-express@10.3.5) '@sveltejs/kit': specifier: 1.21.0 version: 1.21.0(svelte@4.2.1)(vite@4.4.11) @@ -1533,7 +1542,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: @@ -1541,7 +1550,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): @@ -1554,7 +1563,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): @@ -1566,7 +1575,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): @@ -1578,7 +1587,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): @@ -1596,14 +1605,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: @@ -2591,7 +2600,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 @@ -2603,7 +2612,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): @@ -2615,7 +2624,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 @@ -2629,7 +2638,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): @@ -2644,14 +2653,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: @@ -2661,7 +2670,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: @@ -2671,7 +2680,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: @@ -2679,7 +2688,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: @@ -2687,7 +2696,7 @@ 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: @@ -3007,6 +3016,10 @@ 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'} + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -3045,6 +3058,93 @@ packages: - supports-color dev: true + /@nestjs/common@10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1): + resolution: {integrity: sha512-XWxbDf2ey/jAyEa3/XpckgfzJZ9j3I05ZkEFx7cAlebFuVKeq5UDDb5Sq9O7hMmbH9xdQj3pYT19SSj01hKeug==} + 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.1 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + + /@nestjs/core@10.3.5(@nestjs/common@10.3.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(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.5(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/platform-express': 10.3.5(@nestjs/common@10.3.5)(@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.1 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + transitivePeerDependencies: + - encoding + dev: true + + /@nestjs/platform-express@10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5): + resolution: {integrity: sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + dependencies: + '@nestjs/common': 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.5(@nestjs/common@10.3.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(rxjs@7.8.1) + body-parser: 1.20.2 + cors: 2.8.5 + express: 4.18.3 + multer: 1.4.4-lts.1 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@nestjs/testing@10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5)(@nestjs/platform-express@10.3.5): + resolution: {integrity: sha512-j30/lxH0BayeDTigapYtQn/XhMRR7CzlFsm3dHoWViWQv0qT1r2ffe3927BbBLX3N/ZzglE10OAqW06ADZV8dw==} + 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.5(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.5(@nestjs/common@10.3.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/platform-express': 10.3.5(@nestjs/common@10.3.5)(@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'} @@ -3523,6 +3623,18 @@ packages: - vue-tsc dev: true + /@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.4.1: resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} engines: {node: '>=8.0.0'} @@ -4559,13 +4671,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: @@ -5647,14 +5759,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: @@ -5838,6 +5950,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 @@ -6406,7 +6522,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: @@ -6447,7 +6563,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 @@ -6856,6 +6972,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} @@ -6879,6 +7005,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} @@ -6892,7 +7022,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 @@ -6963,6 +7093,14 @@ 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'} @@ -7027,7 +7165,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: @@ -7518,7 +7656,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: @@ -8375,6 +8513,45 @@ packages: - supports-color dev: true + /express@4.18.3: + resolution: {integrity: sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==} + 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.5.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 @@ -9053,7 +9230,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): @@ -9063,7 +9240,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): @@ -9083,7 +9260,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): @@ -9196,7 +9373,7 @@ packages: 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: @@ -9839,6 +10016,10 @@ packages: istanbul-lib-report: 3.0.0 dev: true + /iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10815,7 +10996,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: @@ -11109,6 +11290,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'} @@ -11156,6 +11344,19 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /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==} dev: true @@ -11433,7 +11634,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: @@ -11938,7 +12139,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: @@ -12009,7 +12210,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: @@ -12024,7 +12225,7 @@ 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: @@ -12051,6 +12252,10 @@ 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'} @@ -13138,6 +13343,9 @@ packages: engines: {node: '>=6'} dev: true + /reflect-metadata@0.2.1: + resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: true @@ -13360,8 +13568,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.6.0 - dev: true + tslib: 2.6.2 /sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} @@ -13464,7 +13671,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 @@ -13628,7 +13835,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: @@ -14295,7 +14502,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: @@ -14495,6 +14702,9 @@ packages: /tslib@2.6.0: resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tsup@8.0.1(ts-node@10.9.1)(typescript@5.3.2): resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} engines: {node: '>=18'} @@ -14651,6 +14861,10 @@ packages: underscore: 1.13.6 dev: true + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + /typescript@5.3.2: resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} engines: {node: '>=14.17'} @@ -14680,6 +14894,12 @@ 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 + /ultrahtml@1.5.2: resolution: {integrity: sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw==} dev: true @@ -14939,7 +15159,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: From 9813668d319f25b2e07c587d1546b3b2828b04fd Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 27 Mar 2024 09:17:52 -0700 Subject: [PATCH 079/127] chore: change nestjs dependency from peer to dev (#1182) --- packages/server/package.json | 6 ++---- pnpm-lock.yaml | 12 +++++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index b7d38f5ee..ff5cc8d9b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -37,9 +37,6 @@ "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, - "peerDependencies": { - "@nestjs/common": "^10.0.0" - }, "devDependencies": { "@nestjs/platform-express": "^10.3.5", "@nestjs/testing": "^10.3.5", @@ -57,7 +54,8 @@ "isomorphic-fetch": "^3.0.0", "next": "^13.4.5", "nuxt": "^3.7.4", - "supertest": "^6.3.3" + "supertest": "^6.3.3", + "@nestjs/common": "^10.0.0" }, "exports": { "./package.json": "./package.json", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fbfa6f98..ee2017259 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -655,9 +655,6 @@ importers: packages/server: dependencies: - '@nestjs/common': - specifier: ^10.0.0 - version: 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime/dist @@ -689,6 +686,9 @@ importers: specifier: ^1.5.0 version: 1.5.0(zod@3.22.4) devDependencies: + '@nestjs/common': + specifier: ^10.0.0 + version: 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) '@nestjs/platform-express': specifier: ^10.3.5 version: 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5) @@ -3019,6 +3019,7 @@ packages: /@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==} @@ -3076,6 +3077,7 @@ packages: rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 + dev: true /@nestjs/core@10.3.5(@nestjs/common@10.3.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(rxjs@7.8.1): resolution: {integrity: sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==} @@ -10019,6 +10021,7 @@ packages: /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==} @@ -13345,6 +13348,7 @@ packages: /reflect-metadata@0.2.1: resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} + dev: true /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -13569,6 +13573,7 @@ packages: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: tslib: 2.6.2 + dev: true /sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} @@ -14899,6 +14904,7 @@ packages: engines: {node: '>=8'} dependencies: '@lukeed/csprng': 1.1.0 + dev: true /ultrahtml@1.5.2: resolution: {integrity: sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw==} From a1ed389ac96c71de2e80dd2083a557bd4129db27 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 27 Mar 2024 15:29:15 -0700 Subject: [PATCH 080/127] chore: add more exports to nestjs (#1183) --- packages/server/package.json | 1 + packages/server/src/nestjs/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/package.json b/packages/server/package.json index ff5cc8d9b..2d14a51ae 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -68,6 +68,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/nestjs/index.ts b/packages/server/src/nestjs/index.ts index 6fca28a71..f94469901 100644 --- a/packages/server/src/nestjs/index.ts +++ b/packages/server/src/nestjs/index.ts @@ -1 +1 @@ -export { ZenStackModule } from './zenstack.module'; +export * from './zenstack.module'; From be2aec94495d16fef6d7818b52522dbd06511267 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 27 Mar 2024 15:43:38 -0700 Subject: [PATCH 081/127] chore: bump version (#1184) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 9b6ef50b8..644ed3fe6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 9b911a7fd..a1fe55be2 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 19dbf9590..c10dfcb5d 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 53657afff..ea3a75dda 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 70c9f0a8f..522de4696 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": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 2397b05f4..90da93f32 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": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 75ea35eb8..afd7bcdcc 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": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index f4d4d8322..66e357c35 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": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 6523b4a25..4eac0cbec 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": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 86925a010..cc4e50b5b 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index cb1a3238e..5d2ad06c6 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": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c4a7c3aa6..9d8dc40f4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 2d14a51ae..6ff916bc3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 88440c1d9..301d1afbd 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 1b2f48e01d271d618bfdcfd84ae3298a40c2f736 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 29 Mar 2024 17:46:53 -0700 Subject: [PATCH 082/127] refactor(runtime): unify the handling of fluent api calls (#1187) --- .../src/enhancements/policy/handler.ts | 654 ++++++++---------- .../src/enhancements/policy/policy-utils.ts | 19 +- .../src/enhancements/policy/promise.ts | 38 - packages/runtime/src/enhancements/promise.ts | 99 +++ packages/runtime/src/enhancements/proxy.ts | 125 ++-- .../with-policy/field-level-policy.test.ts | 27 + .../with-policy/fluent-api.test.ts | 190 ++++- 7 files changed, 679 insertions(+), 473 deletions(-) delete mode 100644 packages/runtime/src/enhancements/policy/promise.ts create mode 100644 packages/runtime/src/enhancements/promise.ts diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 382825aa0..f429066f9 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -19,11 +19,11 @@ import { 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 { QueryUtils } from '../query-utils'; import { formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils'; import { PolicyUtil } from './policy-utils'; -import { createDeferredPromise } from './promise'; // a record for post-write policy check type PostWriteCheckRecord = { @@ -80,7 +80,8 @@ export class PolicyProxyHandler implements Pr 'where field is required in query argument' ); } - return this.findWithFluentCallStubs(args, 'findUnique', false, () => null); + + return this.findWithFluent('findUnique', args, () => null); } findUniqueOrThrow(args: any) { @@ -94,17 +95,18 @@ export class PolicyProxyHandler implements Pr 'where field is required in query argument' ); } - return this.findWithFluentCallStubs(args, 'findUniqueOrThrow', true, () => { + + 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, () => { + return this.findWithFluent('findFirstOrThrow', args, () => { throw this.policyUtils.notFound(this.model); }); } @@ -113,20 +115,24 @@ 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 = this.policyUtils.clone(args); + return createFluentPromise( + () => this.doFind(args, method, handleRejection), + args, + this.options.modelMeta, + this.model + ); + } + + private addFluentSelect(args: any, field: string, fluentArgs: any) { + // overwrite include/select with the fluent field + delete args.include; + args.select = { [field]: fluentArgs ?? true }; } - private doFind(args: any, actionName: FindOperations, handleRejection: () => any) { + private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) { const origArgs = args; const _args = this.policyUtils.clone(args); if (!this.policyUtils.injectForRead(this.prisma, this.model, _args)) { @@ -142,88 +148,16 @@ export class PolicyProxyHandler implements Pr this.logger.info(`[policy] \`${actionName}\` ${this.model}:\n${formatObject(_args)}`); } - return new Promise((resolve, reject) => { - this.modelClient[actionName](_args).then( - (value: any) => { - this.policyUtils.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.policyUtils.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.policyUtils.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.policyUtils.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.prismaModule, 'query argument is required'); } @@ -235,63 +169,65 @@ export class PolicyProxyHandler implements Pr ); } - this.policyUtils.tryReject(this.prisma, this.model, 'create'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'create'); - const origArgs = args; - args = this.policyUtils.clone(args); + const origArgs = args; + args = this.policyUtils.clone(args); - // 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 - ); - } + // 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.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 + 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.policyUtils.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.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); + // 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.policyUtils.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 @@ -488,7 +424,7 @@ 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.prismaModule, 'query argument is required'); } @@ -500,47 +436,49 @@ export class PolicyProxyHandler implements Pr ); } - this.policyUtils.tryReject(this.prisma, this.model, 'create'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'create'); - args = this.policyUtils.clone(args); + args = this.policyUtils.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.policyUtils.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.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; + 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.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; - }); - } + 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: CrudContract) { @@ -662,7 +600,7 @@ 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.prismaModule, 'query argument is required'); } @@ -681,24 +619,26 @@ export class PolicyProxyHandler implements Pr ); } - args = this.policyUtils.clone(args); + return createDeferredPromise(async () => { + args = this.policyUtils.clone(args); - 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); + 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.policyUtils.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: CrudContract) { @@ -1131,7 +1071,7 @@ export class PolicyProxyHandler implements Pr } } - async updateMany(args: any) { + updateMany(args: any) { if (!args) { throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); } @@ -1147,58 +1087,60 @@ export class PolicyProxyHandler implements Pr throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); } - this.policyUtils.tryReject(this.prisma, this.model, 'update'); + return createDeferredPromise(() => { + this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.policyUtils.clone(args); - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); - - args.data = this.validateUpdateInputSchema(this.model, args.data); - - 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'); + args = this.policyUtils.clone(args); + this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); - 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.policyUtils.getEntityIds(this.model, preValue), - preValue: preValueSelect ? preValue : undefined, - })) - ); + args.data = this.validateUpdateInputSchema(this.model, args.data); - // proceed with the update - const result = await tx[this.model].updateMany(args); + 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'); - // run post-write checks - await this.runPostWriteChecks(postWriteChecks, tx); + if (this.shouldLogQuery) { + this.logger.info(`[policy] \`findMany\` ${this.model}: ${formatObject(currentSetQuery)}`); + } + const currentSet = await tx[this.model].findMany(currentSetQuery); - return result; - }); - } else { - // proceed without a transaction - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`updateMany\` ${this.model}: ${formatObject(args)}`); + postWriteChecks.push( + ...currentSet.map((preValue) => ({ + model: this.model, + operation: 'postUpdate' as PolicyOperationKind, + uniqueFilter: this.policyUtils.getEntityIds(this.model, preValue), + preValue: preValueSelect ? preValue : undefined, + })) + ); + + // proceed with the update + const result = await tx[this.model].updateMany(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.prismaModule, 'query argument is required'); } @@ -1224,36 +1166,38 @@ export class PolicyProxyHandler implements Pr ); } - this.policyUtils.tryReject(this.prisma, this.model, 'create'); - this.policyUtils.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.policyUtils.clone(args); + args = this.policyUtils.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.queryUtils.transaction(this.prisma, async (tx) => { - const { where, create, update, ...rest } = args; - const existing = await this.policyUtils.checkExistence(tx, this.model, args.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, args.where); - if (existing) { - // update case - const { result, postWriteChecks } = await this.doUpdate({ where, data: update, ...rest }, tx); - await this.runPostWriteChecks(postWriteChecks, tx); - return this.policyUtils.readBack(tx, this.model, 'update', args, result); + if (existing) { + // update case + const { result, postWriteChecks } = await this.doUpdate({ where, 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.policyUtils.readBack(tx, this.model, 'create', args, result); + return result; } }); - - if (error) { - throw error; - } else { - return result; - } } //#endregion @@ -1263,7 +1207,7 @@ 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.prismaModule, 'query argument is required'); } @@ -1275,144 +1219,156 @@ export class PolicyProxyHandler implements Pr ); } - this.policyUtils.tryReject(this.prisma, this.model, 'delete'); + return createDeferredPromise(async () => { + this.policyUtils.tryReject(this.prisma, this.model, 'delete'); - 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; + 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.policyUtils.checkExistence(tx, this.model, args.where, true); + // check existence + await this.policyUtils.checkExistence(tx, this.model, args.where, true); - // inject delete guard - await this.policyUtils.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.policyUtils.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.policyUtils.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.prismaModule, 'query argument is required'); } - args = this.policyUtils.clone(args); + return createDeferredPromise(() => { + args = this.policyUtils.clone(args); - // inject policy conditions - this.policyUtils.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.prismaModule, 'query argument is required'); } - args = this.policyUtils.clone(args); + return createDeferredPromise(() => { + args = this.policyUtils.clone(args); - // inject policy conditions - this.policyUtils.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.policyUtils.clone(args) : {}; - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); + count(args: any) { + return createDeferredPromise(() => { + // inject policy conditions + args = args ? this.policyUtils.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.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)}`); + 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.prismaModule, 'argument must be an object'); - } - if (Object.keys(args).length === 0) { + if (!args) { // include all args = { create: {}, update: {}, delete: {} }; } else { - args = this.policyUtils.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 = this.policyUtils.clone(args); + } } - } - // inject into subscribe conditions + // inject into subscribe conditions - if (args.create) { - args.create.after = this.policyUtils.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.policyUtils.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.policyUtils.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 @@ -1431,10 +1387,6 @@ export class PolicyProxyHandler implements Pr ); } - private makeHandler(model: string) { - return new PolicyProxyHandler(this.prisma, model, this.options, this.context); - } - 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/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 5df9bed70..bc313f7c3 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -897,16 +897,21 @@ export class PolicyUtil extends QueryUtils { * @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) { 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/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 a3141ad0a..e7f55a88c 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -1,10 +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 { InternalEnhancementOptions } from './create-enhancement'; -import { createDeferredPromise } from './policy/promise'; +import type { InternalEnhancementOptions } from './create-enhancement'; +import { createDeferredPromise, createFluentPromise } from './promise'; /** * Prisma batch write operation result @@ -70,93 +71,91 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { protected readonly options: InternalEnhancementOptions ) {} - async findUnique(args: any): Promise { - args = await this.preprocessArgs('findUnique', args); - const r = await this.prisma[this.model].findUnique(args); - return this.processResultEntity(r); + 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; } - async findUniqueOrThrow(args: any): Promise { - args = await this.preprocessArgs('findUniqueOrThrow', args); - const r = await this.prisma[this.model].findUniqueOrThrow(args); - return this.processResultEntity(r); + 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 findFirst(args: any): Promise { - args = await this.preprocessArgs('findFirst', args); - const r = await this.prisma[this.model].findFirst(args); - return this.processResultEntity(r); + findUnique(args: any) { + return this.withFluentCall('findUnique', 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); + findUniqueOrThrow(args: any) { + return this.withFluentCall('findUniqueOrThrow', 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); + findFirst(args: any) { + return this.withFluentCall('findFirst', 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); + findFirstOrThrow(args: any) { + return this.withFluentCall('findFirstOrThrow', args); } - async createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> { - args = await this.preprocessArgs('createMany', args); - return this.prisma[this.model].createMany(args); + findMany(args: any) { + return this.deferred('findMany', args); } - async update(args: any): Promise { - args = await this.preprocessArgs('update', args); - const r = await this.prisma[this.model].update(args); - return this.processResultEntity(r); + create(args: any): Promise { + return this.deferred('create', args); } - async updateMany(args: any): Promise<{ count: number }> { - args = await this.preprocessArgs('updateMany', args); - return this.prisma[this.model].updateMany(args); + createMany(args: { data: any; skipDuplicates?: boolean }) { + return this.deferred<{ count: number }>('createMany', 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); + update(args: any) { + return this.deferred('update', 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); + updateMany(args: any) { + return this.deferred<{ count: number }>('updateMany', args, false); } - async deleteMany(args: any): Promise<{ count: number }> { - args = await this.preprocessArgs('deleteMany', args); - return this.prisma[this.model].deleteMany(args); + upsert(args: any) { + return this.deferred('upsert', args); } - async aggregate(args: any): Promise { - args = await this.preprocessArgs('aggregate', args); - return this.prisma[this.model].aggregate(args); + delete(args: any) { + return this.deferred('delete', args); } - async groupBy(args: any): Promise { - args = await this.preprocessArgs('groupBy', args); - return this.prisma[this.model].groupBy(args); + deleteMany(args: any) { + return this.deferred<{ count: number }>('deleteMany', args, false); } - async count(args: any): Promise { - args = await this.preprocessArgs('count', args); - return this.prisma[this.model].count(args); + aggregate(args: any) { + return this.deferred('aggregate', args, false); } - async subscribe(args: any): Promise { - args = await this.preprocessArgs('subscribe', args); - return this.prisma[this.model].subscribe(args); + groupBy(args: any) { + return this.deferred('groupBy', args, false); + } + + count(args: any): Promise { + return this.deferred('count', args, false); + } + + subscribe(args: any) { + return this.deferred('subscribe', args, false); } /** @@ -177,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. */ @@ -196,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 @@ -245,6 +242,8 @@ export function makeProxy( }, }); + proxy[customInspect] = `$zenstack_prisma_${prisma._clientVersion}`; + return proxy; } 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 ebaf2d858..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 @@ -51,6 +51,18 @@ describe('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('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(); 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 6c27aab1c..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 () => { + 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('all', author == auth()) -}` + @@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 +} + +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 = enhance({ 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(); From 0f558cbb97b7061744e801abf4ed94f4f0f83828 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 30 Mar 2024 08:38:37 -0700 Subject: [PATCH 083/127] fix: `auth()` field from a base model is not properly resolved (#1192) --- .../src/language-server/zmodel-linker.ts | 15 ++--------- .../src/language-server/zmodel-scope.ts | 2 +- .../tests/regression/issue-1179.test.ts | 27 +++++++++++++++++++ 3 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1179.test.ts diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 13de8b968..56e2431d5 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -97,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; @@ -106,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 diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 9d685db27..48cf77995 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -224,7 +224,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { if (model) { const authModel = getAuthModel(getDataModels(model, true)); if (authModel) { - return this.createScopeForNodes(authModel.fields, globalScope); + return this.createScopeForModel(authModel, globalScope); } } return EMPTY_SCOPE; diff --git a/tests/integration/tests/regression/issue-1179.test.ts b/tests/integration/tests/regression/issue-1179.test.ts new file mode 100644 index 000000000..3d5fd8d99 --- /dev/null +++ b/tests/integration/tests/regression/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) + } + ` + ); + }); +}); From a28d592d1a696d142ef1fecdfd562aeea72fd918 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 30 Mar 2024 15:42:40 -0700 Subject: [PATCH 084/127] fix(zmodel): fix linking error post-merging imported declarations (#1193) --- packages/schema/src/cli/cli-util.ts | 21 +++++--- .../src/language-server/zmodel-scope.ts | 2 +- .../tests/regression/issue-1186.test.ts | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1186.test.ts diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 13b82f01f..83804a46a 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -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], { @@ -85,7 +85,9 @@ export async function loadDocument(fileName: string): Promise { const model = document.parseResult.value as Model; + // merge all declarations into the main document const imported = mergeImportsDeclarations(langiumDocuments, model); + // remove imported documents await services.shared.workspace.DocumentBuilder.update( [], @@ -94,11 +96,13 @@ export async function loadDocument(fileName: string): Promise { validationAfterMerge(model); + // merge fields and attributes from base models mergeBaseModel(model, services.references.Linker); - await relinkAll(model, services); + // finally relink all references + const relinkedModel = await relinkAll(model, services); - return model; + return relinkedModel; } // check global unique thing after merge imports @@ -152,13 +156,16 @@ export function mergeImportsDeclarations(documents: LangiumDocuments, model: Mod importedDeclarations.forEach((d) => { const mutable = d as Mutable; - // The plugin might use $container to access the model + // Plugins might use $container to access the model // need to make sure it is always resolved to the main model mutable.$container = model; }); model.declarations.push(...importedDeclarations); + // remove import directives + model.imports = []; + return importedModels; } @@ -314,10 +321,12 @@ async function relinkAll(model: Model, services: ZModelServices) { // remove current document await services.shared.workspace.DocumentBuilder.update([], [doc.uri]); - // recreate the document + // recreate and load the document const newDoc = services.shared.workspace.LangiumDocumentFactory.fromModel(model, doc.uri); - (model as Mutable).$document = newDoc; + 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/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 48cf77995..7dff9c8df 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -108,7 +108,7 @@ 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); } diff --git a/tests/integration/tests/regression/issue-1186.test.ts b/tests/integration/tests/regression/issue-1186.test.ts new file mode 100644 index 000000000..d36efcd57 --- /dev/null +++ b/tests/integration/tests/regression/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 } + ); + }); +}); From f8700cd4bc14fea6580b27d86afe3e59c5f1ca18 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 30 Mar 2024 17:02:36 -0700 Subject: [PATCH 085/127] chore: bump version (#1195) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 644ed3fe6..835ee2407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index a1fe55be2..41af4724b 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.4" +version = "2.0.0-beta.5" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index c10dfcb5d..a5a4ab0ef 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index ea3a75dda..8e5a3ed48 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 522de4696..d1dd07210 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": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 90da93f32..cf79f922c 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": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index afd7bcdcc..5f75fd5d2 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": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 66e357c35..9bf8139eb 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": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 4eac0cbec..c178914d3 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": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index cc4e50b5b..f6f2b59ed 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 5d2ad06c6..734d4f9fd 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": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9d8dc40f4..94ae000ee 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 6ff916bc3..53944cfe8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 301d1afbd..512d1b10a 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From ee3c7168d679f0b40257b7bb25ba1bc374774585 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 30 Mar 2024 17:20:47 -0700 Subject: [PATCH 086/127] chore(server): change default exports to named ones for compatibility (#1194) --- packages/server/package.json | 1 + packages/server/src/api/index.ts | 2 ++ packages/server/src/api/rest/index.ts | 2 ++ packages/server/src/api/rpc/index.ts | 2 ++ packages/server/src/express/index.ts | 2 +- packages/server/src/express/middleware.ts | 6 ++++-- packages/server/src/fastify/index.ts | 2 +- packages/server/src/fastify/plugin.ts | 6 +++++- packages/server/src/next/app-route-handler.ts | 4 ++-- packages/server/src/next/pages-route-handler.ts | 4 ++-- packages/server/src/sveltekit/handler.ts | 2 ++ packages/server/src/sveltekit/index.ts | 2 +- packages/server/tests/api/rpc.test.ts | 4 ++-- 13 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 packages/server/src/api/index.ts diff --git a/packages/server/package.json b/packages/server/package.json index 53944cfe8..77a24b6b9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -59,6 +59,7 @@ }, "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", 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 5fbf05ee8..c65fd7da1 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -1664,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 79e26bd79..a7fb44d72 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -290,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/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 200c57bd6..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,7 +32,7 @@ export interface MiddlewareOptions extends AdapterBaseOptions { const factory = (options: MiddlewareOptions): Handler => { const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler || RPCAPIHandler(); + const requestHandler = options.handler || RPCApiHandler(); return async (request, response, next) => { const prisma = (await options.getPrisma(request, response)) as DbClientContract; @@ -83,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 8651e0bb0..a69d9ac98 100644 --- a/packages/server/src/fastify/plugin.ts +++ b/packages/server/src/fastify/plugin.ts @@ -62,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/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts index 71121d151..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,7 +19,7 @@ export default function factory( ): (req: NextRequest, context: Context) => Promise { const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler || RPCAPIHandler(); + 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 0752775da..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,7 +17,7 @@ export default function factory( ): (req: NextApiRequest, res: NextApiResponse) => Promise { const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler || RPCAPIHandler(); + 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/sveltekit/handler.ts b/packages/server/src/sveltekit/handler.ts index 3d346f762..f5d1b7995 100644 --- a/packages/server/src/sveltekit/handler.ts +++ b/packages/server/src/sveltekit/handler.ts @@ -83,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/tests/api/rpc.test.ts b/packages/server/tests/api/rpc.test.ts index f3326f570..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', () => { @@ -408,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 { From ee0166be04f37a1dc739fe9571278c17d56990cd Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 31 Mar 2024 13:59:14 -0700 Subject: [PATCH 087/127] refactor(policy): optimize post-create read to only use id fields to find the created entity (#1201) --- packages/runtime/src/enhancements/policy/handler.ts | 9 ++------- .../runtime/src/enhancements/policy/policy-utils.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index f429066f9..96e71641c 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -126,12 +126,6 @@ export class PolicyProxyHandler implements Pr ); } - private addFluentSelect(args: any, field: string, fluentArgs: any) { - // overwrite include/select with the fluent field - delete args.include; - args.select = { [field]: fluentArgs ?? true }; - } - private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) { const origArgs = args; const _args = this.policyUtils.clone(args); @@ -364,7 +358,8 @@ 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 }); } }); diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index bc313f7c3..b33f8ded1 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -1235,5 +1235,16 @@ export class PolicyUtil extends QueryUtils { 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 } From e3209c77efd70a8448e5ec815d55ae251a7d2c5e Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 1 Apr 2024 16:08:10 -0700 Subject: [PATCH 088/127] chore: update readme (#1204) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From ef4d5e1e9c9da01ef9d8248c67c4bbbac514e02f Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 1 Apr 2024 17:01:17 -0700 Subject: [PATCH 089/127] chore: merge from dev (#1205) --- .github/workflows/update-samples.yml | 1 + .../plugins/tanstack-query/src/generator.ts | 27 +++++-- .../plugins/tanstack-query/src/runtime/vue.ts | 74 +++++++++++++------ packages/plugins/trpc/src/helpers.ts | 2 +- .../routers/generated/routers/Post.router.ts | 2 +- .../routers/generated/routers/User.router.ts | 2 +- pnpm-lock.yaml | 1 + .../tests/regression/issue-1162.test.ts | 56 -------------- 8 files changed, 76 insertions(+), 89 deletions(-) delete mode 100644 tests/integration/tests/regression/issue-1162.test.ts diff --git a/.github/workflows/update-samples.yml b/.github/workflows/update-samples.yml index 4fd474843..4cc0216df 100644 --- a/.github/workflows/update-samples.yml +++ b/.github/workflows/update-samples.yml @@ -16,6 +16,7 @@ jobs: 'zenstackhq/sample-todo-nextjs-tanstack', 'zenstackhq/sample-todo-trpc', 'zenstackhq/sample-todo-sveltekit', + 'zenstackhq/sample-todo-nuxt', ] steps: diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index c4be4e3c5..77d4feff1 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -94,7 +94,7 @@ function generateQueryHook( const capOperation = upperCaseFirst(operation); const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; - const inputType = `Prisma.SelectSubset`; + const inputType = makeQueryArgsType(target, argsType); const infinite = generateMode.includes('Infinite'); const suspense = generateMode.includes('Suspense'); @@ -567,6 +567,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { return [ `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, + `import type { MaybeRefOrGetter, ComputedRef } from 'vue';`, ...shared, ]; } @@ -586,6 +587,15 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { } } +function makeQueryArgsType(target: string, argsType: string) { + const type = `Prisma.SelectSubset`; + if (target === 'vue') { + return `MaybeRefOrGetter<${type}> | ComputedRef<${type}>`; + } else { + return type; + } +} + function makeQueryOptions( target: string, returnType: string, @@ -604,10 +614,12 @@ function makeQueryOptions( }InfiniteQueryOptions<${returnType}, TError, InfiniteData<${dataType}>>, 'queryKey'>` : `Omit, 'queryKey'>` ) - .with( - 'vue', - () => `Omit, 'queryKey'>` - ) + .with('vue', () => { + const baseOption = `Omit, 'queryKey'>`; + return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`; + }) .with('svelte', () => infinite ? version === 'v4' @@ -632,7 +644,10 @@ function makeQueryOptions( function makeMutationOptions(target: string, returnType: string, argsType: string) { let result = match(target) .with('react', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`) - .with('vue', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`) + .with('vue', () => { + const baseOption = `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`; + return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`; + }) .with('svelte', () => `MutationOptions<${returnType}, DefaultError, ${argsType}>`) .otherwise(() => { throw new PluginError(name, `Unsupported target: ${target}`); diff --git a/packages/plugins/tanstack-query/src/runtime/vue.ts b/packages/plugins/tanstack-query/src/runtime/vue.ts index 24edc225a..016414722 100644 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime/vue.ts @@ -5,12 +5,13 @@ import { useMutation, useQuery, useQueryClient, + type QueryKey, type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, } from '@tanstack/vue-query'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { inject, provide } from 'vue'; +import { computed, inject, provide, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue'; import { APIContext, DEFAULT_QUERY_ENDPOINT, @@ -61,19 +62,30 @@ export function getHooksContext() { export function useModelQuery( model: string, url: string, - args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, + args?: MaybeRefOrGetter | ComputedRef, + options?: + | MaybeRefOrGetter, 'queryKey'> & ExtraQueryOptions> + | ComputedRef, 'queryKey'> & ExtraQueryOptions>, fetch?: FetchFn ) { - const reqUrl = makeUrl(url, args); - return useQuery({ - queryKey: getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }), - queryFn: () => fetcher(reqUrl, undefined, fetch, false), - ...options, + const queryOptions = computed(() => { + const optionsValue = toValue< + (Omit, 'queryKey'> & ExtraQueryOptions) | undefined + >(options); + return { + 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); + }, + ...optionsValue, + }; }); + return useQuery(queryOptions); } /** @@ -89,17 +101,24 @@ export function useModelQuery( export function useInfiniteModelQuery( model: string, url: string, - args?: unknown, - options?: Omit, 'queryKey'>, + args?: MaybeRefOrGetter | ComputedRef, + options?: + | MaybeRefOrGetter, 'queryKey'>> + | ComputedRef, 'queryKey'>>, fetch?: FetchFn ) { - return useInfiniteQuery({ + // CHECKME: vue-query's `useInfiniteQuery`'s input typing seems wrong + const queryOptions: any = computed(() => ({ queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), - queryFn: ({ pageParam }) => { - return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + queryFn: ({ queryKey, pageParam }: { queryKey: QueryKey; pageParam?: unknown }) => { + const [_prefix, _model, _op, args] = queryKey; + const reqUrl = makeUrl(url, pageParam ?? toValue(args)); + return fetcher(reqUrl, undefined, fetch, false); }, - ...options, - }); + ...toValue(options), + })); + + return useInfiniteQuery(queryOptions); } /** @@ -125,7 +144,11 @@ export function useModelMutation< method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, + options?: + | MaybeRefOrGetter< + Omit, 'mutationFn'> & ExtraMutationOptions + > + | ComputedRef, 'mutationFn'> & ExtraMutationOptions>, fetch?: FetchFn, checkReadBack?: C ) { @@ -144,11 +167,14 @@ 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 = { ...options, mutationFn }; + const finalOptions: any = computed(() => ({ ...optionsValue, mutationFn })); const operation = url.split('/').pop(); - const invalidateQueries = options?.invalidateQueries !== false; - const optimisticUpdate = !!options?.optimisticUpdate; + const invalidateQueries = optionsValue?.invalidateQueries !== false; + const optimisticUpdate = !!optionsValue?.optimisticUpdate; if (operation) { const { logging } = getHooksContext(); @@ -157,7 +183,7 @@ export function useModelMutation< model, operation, modelMeta, - finalOptions, + toValue(finalOptions), (predicate) => queryClient.invalidateQueries({ predicate }), logging ); @@ -168,7 +194,7 @@ export function useModelMutation< model, operation, modelMeta, - finalOptions, + toValue(finalOptions), queryClient.getQueryCache().getAll(), (queryKey, data) => queryClient.setQueryData(queryKey, data), invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index 62e2efafd..3580ca700 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -237,7 +237,7 @@ export function generateRouterTypingImports(sourceFile: SourceFile, options: Plu // eslint-disable-next-line @typescript-eslint/no-unused-vars export function generateRouterSchemaImport(sourceFile: SourceFile, zodSchemasImport: string) { - sourceFile.addStatements(`import $Schema from '${zodSchemasImport}/input';`); + sourceFile.addStatements(`import * as $Schema from '${zodSchemasImport}/input';`); } export function generateHelperImport(sourceFile: SourceFile) { 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 e7fa40292..6827584d1 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,6 +1,6 @@ /* eslint-disable */ import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from '.'; -import $Schema from '@zenstackhq/runtime/zod/input'; +import * as $Schema from '@zenstackhq/runtime/zod/input'; import { checkRead, checkMutate } from '../helper'; import type { Prisma } from '@prisma/client'; import type { 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 15bd74328..06ce01f31 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,6 +1,6 @@ /* eslint-disable */ import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from '.'; -import $Schema from '@zenstackhq/runtime/zod/input'; +import * as $Schema from '@zenstackhq/runtime/zod/input'; import { checkRead, checkMutate } from '../helper'; import type { Prisma } from '@prisma/client'; import type { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee2017259..3f2c70a0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13348,6 +13348,7 @@ packages: /reflect-metadata@0.2.1: resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} + deprecated: This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer. dev: true /regenerator-runtime@0.13.11: diff --git a/tests/integration/tests/regression/issue-1162.test.ts b/tests/integration/tests/regression/issue-1162.test.ts deleted file mode 100644 index fd7f0dded..000000000 --- a/tests/integration/tests/regression/issue-1162.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1162', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - companies CompanyUser[] - @@allow('all', true) - } - - model Company { - id String @id @default(cuid()) - users CompanyUser[] - @@allow('all', true) - } - - model CompanyUser { - company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) - companyId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - @@id([companyId, userId]) - @@allow('all', true) - } - `, - { logPrismaQuery: true } - ); - - const db = enhance(); - - await db.user.create({ data: { id: 'abc' } }); - await db.user.create({ data: { id: 'def' } }); - await db.company.create({ data: { id: '1', users: { create: { userId: 'abc' } } } }); - await expect( - db.company.update({ - where: { id: '1' }, - data: { - users: { - createMany: { - data: [{ userId: 'abc' }, { userId: 'def' }], - skipDuplicates: true, - }, - }, - }, - include: { users: true }, - }) - ).resolves.toMatchObject({ - users: expect.arrayContaining([ - { companyId: '1', userId: 'abc' }, - { companyId: '1', userId: 'def' }, - ]), - }); - }); -}); From c3e2db19ab3795dc7627589c78d7b8b8b9216d93 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 6 Apr 2024 05:39:22 +0800 Subject: [PATCH 090/127] feat: support for edge runtime (#1209) --- .../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 | 645 ++++++++---------- .../routers/generated/routers/User.router.ts | 645 ++++++++---------- .../api/routers/generated/routers/index.ts | 31 +- packages/runtime/package.json | 7 +- packages/runtime/res/model-meta.d.ts | 1 + packages/runtime/res/model-meta.js | 10 + .../src/enhancements/create-enhancement.ts | 35 +- packages/runtime/src/enhancements/delegate.ts | 8 +- packages/runtime/src/enhancements/logger.ts | 10 +- packages/runtime/src/enhancements/password.ts | 11 +- .../src/enhancements/policy/handler.ts | 2 +- .../src/enhancements/policy/policy-utils.ts | 8 +- packages/runtime/src/enhancements/utils.ts | 7 +- packages/schema/src/plugins/plugin-utils.ts | 17 +- packages/server/src/api/base.ts | 2 +- packages/server/src/shared.ts | 85 +-- packages/server/src/types.ts | 11 +- packages/server/tests/adapter/express.test.ts | 13 +- packages/server/tests/adapter/fastify.test.ts | 8 +- packages/server/tests/adapter/next.test.ts | 12 +- .../server/tests/adapter/sveltekit.test.ts | 12 +- pnpm-lock.yaml | 19 +- tests/integration/tests/cli/generate.test.ts | 6 +- .../enhancements/with-omit/with-omit.test.ts | 2 +- .../enhancements/with-policy/options.test.ts | 2 +- 28 files changed, 756 insertions(+), 871 deletions(-) create mode 100644 packages/runtime/res/model-meta.d.ts create mode 100644 packages/runtime/res/model-meta.js 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 6827584d1..fbc73cf06 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,222 +1,208 @@ /* 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))), - - 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 + >; + }; create: { - useMutation: ( - 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, @@ -224,62 +210,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, @@ -287,165 +273,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 06ce01f31..c4bdb89de 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,222 +1,208 @@ /* 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))), - - 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 + >; + }; create: { - useMutation: ( - 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, @@ -224,62 +210,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, @@ -287,165 +273,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/runtime/package.json b/packages/runtime/package.json index f6f2b59ed..526f51f0d 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -47,6 +47,10 @@ "require": "./cross/index.js", "default": "./cross/index.js" }, + "./model-meta": { + "types": "./model-meta.d.ts", + "default": "./model-meta.js" + }, "./prisma": { "types": "./prisma.d.ts" } @@ -59,12 +63,12 @@ "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", @@ -82,6 +86,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/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/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index 9ab27448c..2616ae4b4 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -1,10 +1,10 @@ -import colors from 'colors'; 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'; @@ -98,10 +98,6 @@ export type EnhancementContext = { user?: User; }; -let hasPassword: boolean | undefined = undefined; -let hasOmit: boolean | undefined = undefined; -let hasDefaultAuth: boolean | undefined = undefined; - /** * Gets a Prisma client enhanced with all enhancement behaviors, including access * policy, field validation, field omission and password hashing. @@ -129,32 +125,23 @@ export function createEnhancement( ); } - let result = prisma; - - if ( - process.env.ZENSTACK_TEST === '1' || // avoid caching in tests - hasPassword === undefined || - hasOmit === undefined || - hasDefaultAuth === undefined - ) { - const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => - Object.values(modelInfo.fields) - ); - hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password')); - hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit')); - hasDefaultAuth = allFields.some((field) => field.defaultValueProvider); - } + // 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')) { - console.warn( - colors.yellow( - 'Your ZModel contains delegate models but "delegate" enhancement kind is not enabled. This may result in unexpected behavior.' - ) + 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); diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index 061a65114..e2fdc65f2 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -657,9 +657,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { where: entity, ...updatePayload, }; - this.logger.info( - `[delegate] \`updateMany\` update: ${this.getModelName(model)}: ${formatObject(updateArgs)}` - ); + if (this.options.logPrismaQuery) { + this.logger.info( + `[delegate] \`updateMany\` update: ${this.getModelName(model)}: ${formatObject(updateArgs)}` + ); + } return db[model].update(updateArgs); }) ); diff --git a/packages/runtime/src/enhancements/logger.ts b/packages/runtime/src/enhancements/logger.ts index dc61c471e..e53551563 100644 --- a/packages/runtime/src/enhancements/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/password.ts b/packages/runtime/src/enhancements/password.ts index f83939792..297613e1b 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -1,7 +1,6 @@ /* 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 PrismaWriteActionType } from '../cross'; import { DbClientContract } from '../types'; @@ -25,6 +24,14 @@ export function withPassword( ); } +// `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, options: InternalEnhancementOptions) { super(prisma, model, options); @@ -53,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 96e71641c..ff57d9bae 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -712,7 +712,7 @@ export class PolicyProxyHandler implements Pr }; // 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 diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index b33f8ded1..f54285691 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -651,7 +651,7 @@ export class PolicyUtil extends QueryUtils { throw this.deniedByPolicy( model, operation, - `entity ${formatObject(uniqueFilter)} failed policy check`, + `entity ${formatObject(uniqueFilter, false)} failed policy check`, CrudFailureReason.ACCESS_POLICY_VIOLATION ); } @@ -664,7 +664,7 @@ export class PolicyUtil extends QueryUtils { 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 @@ -712,7 +712,7 @@ export class PolicyUtil extends QueryUtils { throw this.deniedByPolicy( model, operation, - `entity ${formatObject(uniqueFilter)} failed policy check`, + `entity ${formatObject(uniqueFilter, false)} failed policy check`, CrudFailureReason.ACCESS_POLICY_VIOLATION ); } @@ -728,7 +728,7 @@ export class PolicyUtil extends QueryUtils { 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 ); diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 9bc7ce0bc..92c8b7726 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -1,12 +1,13 @@ -import * as util from 'util'; +import safeJsonStringify from 'safe-json-stringify'; import { FieldInfo, ModelMeta, resolveField } from '..'; import type { DbClientContract } from '../types'; /** * 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); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 3632dedc3..58e485158 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -28,7 +28,10 @@ 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)) { + if (output) { + if (fs.existsSync(output)) { + fs.rmSync(output, { recursive: true }); + } fs.mkdirSync(output, { recursive: true }); if (!options.output) { const pkgJson = { @@ -55,11 +58,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', + }, './prisma': { types: './prisma.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)); } } diff --git a/packages/server/src/api/base.ts b/packages/server/src/api/base.ts index 96c547204..6b9dbfbbd 100644 --- a/packages/server/src/api/base.ts +++ b/packages/server/src/api/base.ts @@ -59,7 +59,7 @@ export abstract class APIHandlerBase { constructor() { try { - this.defaultModelMeta = getDefaultModelMeta(undefined); + this.defaultModelMeta = getDefaultModelMeta(); } catch { // noop } diff --git a/packages/server/src/shared.ts b/packages/server/src/shared.ts index eaeef2cb3..27a07ddd9 100644 --- a/packages/server/src/shared.ts +++ b/packages/server/src/shared.ts @@ -1,18 +1,17 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import { DEFAULT_RUNTIME_LOAD_PATH, type ModelMeta, type PolicyDef, type ZodSchemas } from '@zenstackhq/runtime'; -import path from 'path'; +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'); } @@ -27,94 +26,24 @@ export function loadAssets(options: AdapterBaseOptions) { * @param loadPath The path to load model metadata from. If not provided, * will use default load path. */ -export function getDefaultModelMeta(loadPath: string | undefined): ModelMeta { +export function getDefaultModelMeta(): ModelMeta { try { - if (loadPath) { - const toLoad = path.resolve(loadPath, 'model-meta'); - return require(toLoad).default; - } else { - // model-meta should be resolved relative to the runtime - const metaPath = require.resolve('.zenstack/model-meta', { - paths: [require.resolve('@zenstackhq/runtime')], - }); - return require(metaPath).default; - } + return require('@zenstackhq/runtime/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', DEFAULT_RUNTIME_LOAD_PATH, '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 { - // policy should be resolved relative to the runtime - const policyPath = require.resolve('.zenstack/policy', { - paths: [require.resolve('@zenstackhq/runtime')], - }); - return require(policyPath).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', DEFAULT_RUNTIME_LOAD_PATH, '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 { +export function getDefaultZodSchemas(): ZodSchemas | undefined { try { - if (loadPath) { - const toLoad = path.resolve(loadPath, 'zod'); - return require(toLoad); - } else { - // policy should be resolved relative to the runtime - const zodPath = require.resolve('.zenstack/zod', { - paths: [require.resolve('@zenstackhq/runtime')], - }); - return require(zodPath); - } + return require('@zenstackhq/runtime/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', DEFAULT_RUNTIME_LOAD_PATH, 'zod')); - } catch { - return undefined; - } - } return undefined; } } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 81e2f5fd7..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,11 +48,6 @@ 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`. 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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 3f2c70a0c..1a316b9ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -406,9 +406,6 @@ 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 @@ -424,6 +421,9 @@ importers: 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 @@ -455,6 +455,9 @@ importers: '@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 @@ -5029,7 +5032,7 @@ packages: /@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: @@ -5184,6 +5187,10 @@ packages: 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==} dev: true @@ -13589,6 +13596,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: diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index f5ac211e8..d90ce14cc 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -93,7 +93,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')).toBeFalsy(); }); @@ -110,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(); }); @@ -127,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(); }); @@ -149,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/enhancements/with-omit/with-omit.test.ts b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts index f7fcc7266..73fdaf806 100644 --- a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts +++ b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts @@ -83,7 +83,7 @@ describe('Omit test', () => { enhancements: ['omit'], }); - const db = enhance(prisma, { loadPath: './zen' }); + const db = enhance(prisma); const r = await db.user.create({ include: { profile: true }, data: { diff --git a/tests/integration/tests/enhancements/with-policy/options.test.ts b/tests/integration/tests/enhancements/with-policy/options.test.ts index 571df8ea4..3f63f54a3 100644 --- a/tests/integration/tests/enhancements/with-policy/options.test.ts +++ b/tests/integration/tests/enhancements/with-policy/options.test.ts @@ -16,7 +16,7 @@ describe('Password test', () => { ); const enhance = require(path.join(projectDir, 'zen/enhance')).enhance; - const db = enhance(prisma, { loadPath: './zen' }); + const db = enhance(prisma); await expect( db.foo.create({ data: { x: 0 }, From 92bc73100e4b6387dad26276c901a9529b3f1e76 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 6 Apr 2024 05:49:19 +0800 Subject: [PATCH 091/127] chore: bump version (#1212) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 835ee2407..33fe7ca6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 41af4724b..78799a632 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index a5a4ab0ef..def37332d 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 8e5a3ed48..c8fa8075f 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index d1dd07210..d38f082c9 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": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index cf79f922c..19b38301d 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": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 5f75fd5d2..a745dafb3 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": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 9bf8139eb..7dd0ba924 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": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index c178914d3..7f55216a0 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": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 526f51f0d..7457eff8e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 734d4f9fd..2f503ffa3 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": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 94ae000ee..3bef973c5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 77a24b6b9..94cba3964 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 512d1b10a..4dc06fd03 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 4e8427de3331803a5964fa4e1d008091e52711f2 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 6 Apr 2024 12:39:22 +0800 Subject: [PATCH 092/127] chore: upgrade ts-japi (#1214) --- packages/server/package.json | 6 +++--- packages/server/src/api/rest/index.ts | 2 +- packages/server/src/api/utils.ts | 19 +++++++++++-------- packages/server/tests/api/rest.test.ts | 4 ++-- pnpm-lock.yaml | 8 ++++---- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 94cba3964..942da2fb1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -31,13 +31,14 @@ "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.0.0", "@nestjs/platform-express": "^10.3.5", "@nestjs/testing": "^10.3.5", "@sveltejs/kit": "1.21.0", @@ -54,8 +55,7 @@ "isomorphic-fetch": "^3.0.0", "next": "^13.4.5", "nuxt": "^3.7.4", - "supertest": "^6.3.3", - "@nestjs/common": "^10.0.0" + "supertest": "^6.3.3" }, "exports": { "./package.json": "./package.json", diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index c65fd7da1..530bfbcc9 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -1121,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 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/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index d7944d6aa..bdb2f4d8c 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -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', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a316b9ca..55b961088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -674,8 +674,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 @@ -14608,8 +14608,8 @@ packages: 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 From 08af2e51c935662202373a5c542da7d0887069c0 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 7 Apr 2024 07:45:03 +0800 Subject: [PATCH 093/127] fix: generate zod input/object stub code even if not generating them (#1216) --- packages/schema/src/plugins/zod/generator.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 542b77a64..854fa75ed 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -92,7 +92,15 @@ export class ZodSchemaGenerator { await this.generateModelSchemas(output, excludeModels); - if (this.options.modelOnly !== true) { + 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 Transformer.provider = dataSourceProvider; addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models); From c78aee27f604d813d8886ab669d09cd343bb4aee Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 7 Apr 2024 07:45:34 +0800 Subject: [PATCH 094/127] chore: bump version (#1218) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 33fe7ca6e..90f7f6ab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 78799a632..2233384c3 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.6" +version = "2.0.0-beta.7" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index def37332d..077abf1ab 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index c8fa8075f..783fd177c 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index d38f082c9..8c408c366 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": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 19b38301d..ce743692e 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": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index a745dafb3..8465bede3 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": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 7dd0ba924..0bba7e4da 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": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 7f55216a0..3b9346f62 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": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 7457eff8e..0f90f4e07 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 2f503ffa3..290741d7c 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": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3bef973c5..61e4d1df9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 942da2fb1..573da73f2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 4dc06fd03..7d60f60c1 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 793478e705c18e9230788454e42379f028ee95cf Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 9 Apr 2024 14:25:48 +0800 Subject: [PATCH 095/127] fix: $containerIndex is not updated after merging imported declarations (#1221) --- .../tests/projects/t3-trpc-v10/package.json | 4 +- .../routers/generated/routers/Post.router.ts | 16 ++++ .../routers/generated/routers/User.router.ts | 16 ++++ packages/schema/src/cli/cli-util.ts | 13 +-- packages/schema/src/utils/ast-utils.ts | 6 +- .../tests/regression/issue-1210.test.ts | 92 +++++++++++++++++++ 6 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1210.test.ts 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/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 fbc73cf06..4d2ade66f 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 @@ -12,6 +12,8 @@ export default function createRouter(router: RouterFa 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))), @@ -60,6 +62,20 @@ export interface ClientType >; + }; + createMany: { + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.PostCreateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >,) => + Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise + }; + }; create: { 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 c4bdb89de..00a591ca1 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 @@ -12,6 +12,8 @@ export default function createRouter(router: RouterFa 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))), @@ -60,6 +62,20 @@ export interface ClientType >; + }; + createMany: { + + useMutation: (opts?: UseTRPCMutationOptions< + Prisma.UserCreateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >,) => + Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { + mutateAsync: + (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise + }; + }; create: { diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 83804a46a..6c8508dc5 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'; @@ -153,19 +153,14 @@ export function mergeImportsDeclarations(documents: LangiumDocuments, model: Mod const importedModels = resolveTransitiveImports(documents, model); const importedDeclarations = importedModels.flatMap((m) => m.declarations); - - importedDeclarations.forEach((d) => { - const mutable = d as Mutable; - // Plugins might use $container to access the model - // need to make sure it is always resolved to the main model - mutable.$container = model; - }); - model.declarations.push(...importedDeclarations); // remove import directives model.imports = []; + // fix $containerIndex + linkContentToContainer(model); + return importedModels; } diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index e771f3536..fbb9e4ae2 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -24,6 +24,7 @@ import { getContainerOfType, getDocument, LangiumDocuments, + linkContentToContainer, Linker, Mutable, Reference, @@ -72,6 +73,9 @@ export function mergeBaseModel(model: Model, linker: Linker) { .filter((attr) => attr.decl.$refText !== '@@map') .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); + + // fix $containerIndex + linkContentToContainer(dataModel); } dataModel.$baseMerged = true; @@ -89,8 +93,6 @@ function cloneAst( ): Mutable { const clone = copyAstNode(node, buildReference) as Mutable; clone.$container = newContainer; - clone.$containerProperty = node.$containerProperty; - clone.$containerIndex = node.$containerIndex; clone.$inheritedFrom = node.$inheritedFrom ?? getContainerOfType(node, isDataModel); return clone; } diff --git a/tests/integration/tests/regression/issue-1210.test.ts b/tests/integration/tests/regression/issue-1210.test.ts new file mode 100644 index 000000000..ef1d407e1 --- /dev/null +++ b/tests/integration/tests/regression/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 } + ); + }); +}); From 2abe48165150bd22041cf08c2f3563d374241b0d Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 9 Apr 2024 19:07:09 +0800 Subject: [PATCH 096/127] fix: clear output dir before plugins run (#1224) --- package.json | 2 +- packages/plugins/swr/src/generator.ts | 2 + packages/plugins/swr/tests/swr.test.ts | 57 ++++++++ .../plugins/tanstack-query/src/generator.ts | 8 +- .../tanstack-query/tests/plugin.test.ts | 59 ++++++++ packages/plugins/trpc/package.json | 4 +- packages/plugins/trpc/src/generator.ts | 10 +- packages/plugins/trpc/src/utils/removeDir.ts | 15 -- packages/plugins/trpc/tests/trpc.test.ts | 56 ++++++++ packages/schema/src/plugins/plugin-utils.ts | 7 +- packages/schema/src/plugins/zod/generator.ts | 15 +- .../schema/src/plugins/zod/utils/removeDir.ts | 15 -- packages/sdk/src/utils.ts | 16 +++ packages/testtools/package.json | 1 + packages/testtools/src/schema.ts | 45 +++--- pnpm-lock.yaml | 128 ++++++++++-------- tests/integration/tests/plugins/zod.test.ts | 70 ++++++++++ 17 files changed, 375 insertions(+), 135 deletions(-) delete mode 100644 packages/plugins/trpc/src/utils/removeDir.ts delete mode 100644 packages/schema/src/plugins/zod/utils/removeDir.ts diff --git a/package.json b/package.json index 90f7f6ab1..a08eddedc 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,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/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index 4a36f545f..fd8072544 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -2,6 +2,7 @@ import type { DMMF } from '@prisma/generator-helper'; import { PluginOptions, createProject, + ensureEmptyDir, generateModelMeta, getDataModels, getPrismaClientImportSpec, @@ -21,6 +22,7 @@ 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[] = []; 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/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 77d4feff1..20f8542d2 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -3,6 +3,7 @@ import { PluginError, PluginOptions, createProject, + ensureEmptyDir, generateModelMeta, getDataModels, getPrismaClientImportSpec, @@ -26,9 +27,6 @@ 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); @@ -43,6 +41,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. 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'), generateAttributes: false, diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index 982ddbd6b..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; @@ -174,4 +176,61 @@ ${sharedModel} } ); }); + + 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/trpc/package.json b/packages/plugins/trpc/package.json index 3b9346f62..bdff0d124 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -40,7 +40,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 3487386a6..6249fbf35 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -4,6 +4,7 @@ import { PluginError, PluginOptions, RUNTIME_PACKAGE, + ensureEmptyDir, getPrismaClientImportSpec, parseOptionAsStrings, requireOption, @@ -27,12 +28,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 +46,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; 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/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index 791b84f17..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; @@ -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/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 58e485158..74febf8fa 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'; @@ -29,10 +29,7 @@ export function getNodeModulesFolder(startPath?: string): string | undefined { export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { const output = options.output ? path.resolve(options.output) : getDefaultOutputFolder(); if (output) { - if (fs.existsSync(output)) { - fs.rmSync(output, { recursive: true }); - } - fs.mkdirSync(output, { recursive: true }); + ensureEmptyDir(output); if (!options.output) { const pkgJson = { name: '.zenstack', diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 854fa75ed..1f25606b4 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -2,6 +2,7 @@ import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { PluginGlobalOptions, PluginOptions, + ensureEmptyDir, getDataModels, getLiteral, getPrismaClientImportSpec, @@ -14,7 +15,6 @@ import { } from '@zenstackhq/sdk'; import { DataModel, DataSource, EnumField, Model, isDataModel, isDataSource, isEnum } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; -import { promises as fs } from 'fs'; import { streamAllContents } from 'langium'; import path from 'path'; import type { SourceFile } from 'ts-morph'; @@ -22,7 +22,6 @@ 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 } from './utils/schema-gen'; export class ZodSchemaGenerator { @@ -52,7 +51,8 @@ export class ZodSchemaGenerator { } } output = resolvePath(output, this.options); - await this.handleGeneratorOutputValue(output); + ensureEmptyDir(output); + Transformer.setOutputPath(output); // calculate the models to be excluded const excludeModels = this.getExcludedModels(); @@ -183,15 +183,6 @@ export class ZodSchemaGenerator { } } - private async 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( 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/sdk/src/utils.ts b/packages/sdk/src/utils.ts index b33f8a257..e3ed44b99 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -30,6 +30,7 @@ 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, type PluginDeclaredOptions, type PluginOptions } from './types'; @@ -483,3 +484,18 @@ export function getRecursiveBases(dataModel: DataModel): DataModel[] { }); 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/testtools/package.json b/packages/testtools/package.json index 7d60f60c1..ef3d6a9ce 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -26,6 +26,7 @@ "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/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 55aaa1ff5..450ea16d1 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -130,6 +130,7 @@ export type SchemaLoadOptions = { enhancements?: EnhancementKind[]; enhanceOptions?: Partial; extraSourceFiles?: { name: string; content: string }[]; + projectDir?: string; }; const defaultOptions: SchemaLoadOptions = { @@ -150,7 +151,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); @@ -158,11 +163,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 = [ @@ -175,12 +180,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 @@ -191,7 +196,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)}`; @@ -199,14 +204,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 { @@ -241,23 +246,23 @@ 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 prismaModule = 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(projectRoot, name), content); + fs.writeFileSync(path.join(projectDir, name), content); }); if (opt.extraSourceFiles && opt.extraSourceFiles.length > 0 && !opt.compile) { @@ -271,14 +276,14 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { // 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/prisma': ['./node_modules/.zenstack/prisma.d.ts'], }; tsconfig.include = ['**/*.ts']; tsconfig.exclude = ['node_modules']; - fs.writeFileSync(path.join(projectRoot, './tsconfig.json'), JSON.stringify(tsconfig, null, 2)); + fs.writeFileSync(path.join(projectDir, './tsconfig.json'), JSON.stringify(tsconfig, null, 2)); run('npx tsc --project tsconfig.json'); } @@ -286,7 +291,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { return { prisma, prismaModule, - projectDir: projectRoot, + projectDir, enhance: undefined as any, enhanceRaw: undefined as any, policy: undefined as any, @@ -298,8 +303,8 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { const outputPath = opt.output ? path.isAbsolute(opt.output) ? opt.output - : path.join(projectRoot, opt.output) - : path.join(projectRoot, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); + : path.join(projectDir, opt.output) + : path.join(projectDir, 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); const policy = require(path.join(outputPath, 'policy')).default; const modelMeta = require(path.join(outputPath, 'model-meta')).default; @@ -314,7 +319,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { const enhance = require(path.join(outputPath, 'enhance')).enhance; return { - projectDir: projectRoot, + projectDir: projectDir, prisma, enhance: (user?: AuthUser, options?: EnhancementOptions): FullDbClientContract => enhance( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55b961088..7bf1847aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: 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) + version: 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.55.0)(typescript@5.4.4) '@typescript-eslint/parser': specifier: ^6.13.1 - version: 6.13.1(eslint@8.55.0)(typescript@5.3.2) + version: 6.13.1(eslint@8.55.0)(typescript@5.4.4) concurrently: specifier: ^7.4.0 version: 7.4.0 @@ -34,7 +34,7 @@ importers: version: 8.55.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) + version: 27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.55.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: @@ -387,12 +387,18 @@ 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: @@ -739,7 +745,7 @@ 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.55.0)(typescript@5.4.4) supertest: specifier: ^6.3.3 version: 6.3.3 @@ -768,6 +774,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 @@ -3566,7 +3575,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.55.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: @@ -3605,7 +3614,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.55.0)(typescript@5.4.4)(vite@4.4.11) vue: 3.3.4 vue-bundle-renderer: 2.0.0 transitivePeerDependencies: @@ -5275,7 +5284,7 @@ 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): + /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.55.0)(typescript@5.4.4): resolution: {integrity: sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -5287,10 +5296,10 @@ packages: 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/parser': 6.13.1(eslint@8.55.0)(typescript@5.4.4) '@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/type-utils': 6.13.1(eslint@8.55.0)(typescript@5.4.4) + '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.4.4) '@typescript-eslint/visitor-keys': 6.13.1 debug: 4.3.4 eslint: 8.55.0 @@ -5298,13 +5307,13 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.3(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/parser@6.13.1(eslint@8.55.0)(typescript@5.3.2): + /@typescript-eslint/parser@6.13.1(eslint@8.55.0)(typescript@5.4.4): resolution: {integrity: sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -5316,11 +5325,11 @@ packages: 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/typescript-estree': 6.13.1(typescript@5.4.4) '@typescript-eslint/visitor-keys': 6.13.1 debug: 4.3.4 eslint: 8.55.0 - typescript: 5.3.2 + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true @@ -5341,7 +5350,7 @@ 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): + /@typescript-eslint/type-utils@6.13.1(eslint@8.55.0)(typescript@5.4.4): resolution: {integrity: sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -5351,12 +5360,12 @@ packages: 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': 6.13.1(typescript@5.4.4) + '@typescript-eslint/utils': 6.13.1(eslint@8.55.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 + ts-api-utils: 1.0.3(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true @@ -5371,7 +5380,7 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@5.60.1(typescript@5.3.2): + /@typescript-eslint/typescript-estree@5.60.1(typescript@5.4.4): resolution: {integrity: sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5386,13 +5395,13 @@ packages: 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 + tsutils: 3.21.0(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.13.1(typescript@5.3.2): + /@typescript-eslint/typescript-estree@6.13.1(typescript@5.4.4): resolution: {integrity: sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -5407,13 +5416,13 @@ packages: 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 + ts-api-utils: 1.0.3(typescript@5.4.4) + typescript: 5.4.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.60.1(eslint@8.55.0)(typescript@5.3.2): + /@typescript-eslint/utils@5.60.1(eslint@8.55.0)(typescript@5.4.4): resolution: {integrity: sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5424,7 +5433,7 @@ packages: '@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) + '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.4.4) eslint: 8.55.0 eslint-scope: 5.1.1 semver: 7.5.4 @@ -5433,7 +5442,7 @@ packages: - 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.55.0)(typescript@5.4.4): resolution: {integrity: sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -5444,7 +5453,7 @@ packages: '@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) + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.4.4) eslint: 8.55.0 semver: 7.5.4 transitivePeerDependencies: @@ -8264,7 +8273,7 @@ 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): + /eslint-plugin-jest@27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.55.0)(jest@29.7.0)(typescript@5.4.4): resolution: {integrity: sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -8277,8 +8286,8 @@ 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) + '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.55.0)(typescript@5.4.4) + '@typescript-eslint/utils': 5.60.1(eslint@8.55.0)(typescript@5.4.4) eslint: 8.55.0 jest: 29.7.0(@types/node@20.10.2)(ts-node@10.9.1) transitivePeerDependencies: @@ -10131,7 +10140,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 @@ -10172,7 +10181,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 @@ -11816,7 +11825,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.55.0)(typescript@5.4.4): resolution: {integrity: sha512-voXN2kheEpi7DJd0hkikfLuA41UiP9IwDDol65dvoJiHnRseWfaw1MyJl6FLHHDHwRzisX9QXWIyMfa9YF4nGg==} engines: {node: ^14.18.0 || >=16.10.0} hasBin: true @@ -11834,7 +11843,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.55.0)(typescript@5.4.4)(vue@3.3.4) '@types/node': 20.10.2 '@unhead/dom': 1.7.4 '@unhead/ssr': 1.7.4 @@ -12581,7 +12590,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 @@ -14535,6 +14544,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 @@ -14595,13 +14609,13 @@ 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-interface-checker@0.1.13: @@ -14613,7 +14627,7 @@ packages: 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 @@ -14644,7 +14658,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 @@ -14662,7 +14676,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: @@ -14688,7 +14702,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 @@ -14722,7 +14736,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsup@8.0.1(ts-node@10.9.1)(typescript@5.3.2): + /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 @@ -14755,20 +14769,20 @@ 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): + /tsutils@3.21.0(typescript@5.4.4): 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 + typescript: 5.4.4 dev: true /tsx@4.7.1: @@ -14882,8 +14896,8 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: true - /typescript@5.3.2: - resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} + /typescript@5.4.4: + resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -15315,7 +15329,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.55.0)(typescript@5.4.4)(vite@4.4.11): resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==} engines: {node: '>=14.16'} peerDependencies: @@ -15360,7 +15374,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 diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index ab64352c6..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; @@ -726,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'); + }); }); From 101e642b5df9ac5fe27ef87d8cf88878af77e1cb Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 9 Apr 2024 19:07:16 +0800 Subject: [PATCH 097/127] chore: add prisma as peer dependency (#1225) --- .../trpc/tests/projects/t3-trpc-v10/package.json | 4 ++++ packages/runtime/package.json | 3 +++ packages/schema/package.json | 3 +++ pnpm-lock.yaml | 10 +++------- 4 files changed, 13 insertions(+), 7 deletions(-) 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 6098ca96b..ba0238e67 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json @@ -20,10 +20,14 @@ "@trpc/next": "^10.43.6", "@trpc/react-query": "^10.43.6", "@trpc/server": "^10.43.6", + "@zenstackhq/language": "file:../../../../../../.build/zenstackhq-language-2.0.0-beta.7.tgz", + "@zenstackhq/runtime": "file:../../../../../../.build/zenstackhq-runtime-2.0.0-beta.7.tgz", + "@zenstackhq/sdk": "file:../../../../../../.build/zenstackhq-sdk-2.0.0-beta.7.tgz", "next": "^14.0.4", "react": "18.2.0", "react-dom": "18.2.0", "superjson": "^2.2.1", + "zenstack": "file:../../../../../../.build/zenstack-2.0.0-beta.7.tgz", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 0f90f4e07..92f738f82 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -78,6 +78,9 @@ "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, + "peerDependencies": { + "@prisma/client": "^5.0.0" + }, "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/package.json b/packages/schema/package.json index 290741d7c..6ad5331f5 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -110,6 +110,9 @@ "zod": "^3.22.4", "zod-validation-error": "^1.5.0" }, + "peerDependencies": { + "prisma": "^5.0.0" + }, "devDependencies": { "@prisma/client": "^5.7.1", "@types/async-exit-hook": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bf1847aa..8a0c7d57f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -403,6 +403,9 @@ importers: packages/runtime: dependencies: + '@prisma/client': + specifier: ^5.0.0 + version: 5.7.1(prisma@5.7.1) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -3853,7 +3856,6 @@ packages: optional: true dependencies: prisma: 5.7.1 - dev: true /@prisma/debug@4.16.2: resolution: {integrity: sha512-7L7WbG0qNNZYgLpsVB8rCHCXEyHFyIycRlRDNwkVfjQmACC2OW6AWCYCbfdjQhkF/t7+S3njj8wAWAocSs+Brw==} @@ -3881,7 +3883,6 @@ packages: /@prisma/debug@5.7.1: resolution: {integrity: sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==} - dev: true /@prisma/engines-version@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9: resolution: {integrity: sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==} @@ -3889,7 +3890,6 @@ packages: /@prisma/engines-version@5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5: resolution: {integrity: sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==} - dev: true /@prisma/engines@4.16.2: resolution: {integrity: sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==} @@ -3919,7 +3919,6 @@ packages: '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 '@prisma/fetch-engine': 5.7.1 '@prisma/get-platform': 5.7.1 - dev: true /@prisma/fetch-engine@4.16.2: resolution: {integrity: sha512-lnCnHcOaNn0kw8qTJbVcNhyfIf5Lus2GFXbj3qpkdKEIB9xLgqkkuTP+35q1xFaqwQ0vy4HFpdRUpFP7njE15g==} @@ -3985,7 +3984,6 @@ packages: '@prisma/debug': 5.7.1 '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 '@prisma/get-platform': 5.7.1 - dev: true /@prisma/generator-helper@4.16.2: resolution: {integrity: sha512-bMOH7y73Ui7gpQrioFeavMQA+Tf8ksaVf8Nhs9rQNzuSg8SSV6E9baczob0L5KGZTSgYoqnrRxuo03kVJYrnIg==} @@ -4059,7 +4057,6 @@ packages: resolution: {integrity: sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==} dependencies: '@prisma/debug': 5.7.1 - dev: true /@prisma/internals@4.16.2: resolution: {integrity: sha512-/3OiSADA3RRgsaeEE+MDsBgL6oAMwddSheXn6wtYGUnjERAV/BmF5bMMLnTykesQqwZ1s8HrISrJ0Vf6cjOxMg==} @@ -12992,7 +12989,6 @@ packages: requiresBuild: true dependencies: '@prisma/engines': 5.7.1 - dev: true /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} From 0a2aaf7a6c41e183c18b1b40e01fbb5f7bca1449 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 9 Apr 2024 22:12:08 +0800 Subject: [PATCH 098/127] refactor: make data validation a separate enhancement kind (#1226) --- .../src/enhancements/create-enhancement.ts | 10 +- .../src/enhancements/policy/policy-utils.ts | 35 +++- .../runtime/src/enhancements/query-utils.ts | 2 +- .../with-policy/field-validation.test.ts | 151 ++++++++++++------ 4 files changed, 139 insertions(+), 59 deletions(-) diff --git a/packages/runtime/src/enhancements/create-enhancement.ts b/packages/runtime/src/enhancements/create-enhancement.ts index 2616ae4b4..596c3e763 100644 --- a/packages/runtime/src/enhancements/create-enhancement.ts +++ b/packages/runtime/src/enhancements/create-enhancement.ts @@ -14,12 +14,12 @@ import type { PolicyDef, ZodSchemas } from './types'; /** * Kinds of enhancements to `PrismaClient` */ -export type EnhancementKind = 'password' | 'omit' | 'policy' | 'delegate'; +export type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate'; /** * All enhancement kinds */ -const ALL_ENHANCEMENTS = ['password', 'omit', 'policy', 'delegate']; +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 @@ -148,10 +148,10 @@ export function createEnhancement( } } - // policy proxy - if (kinds.includes('policy')) { + // 'policy' and 'validation' enhancements are both enabled by `withPolicy` + if (kinds.includes('policy') || kinds.includes('validation')) { result = withPolicy(result, options, context); - if (hasDefaultAuth) { + if (kinds.includes('policy') && hasDefaultAuth) { // @default(auth()) proxy result = withDefaultAuth(result, options, context); } diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index f54285691..bcb946877 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -230,6 +230,25 @@ export class PolicyUtil extends QueryUtils { //# 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. * @@ -237,7 +256,7 @@ export class PolicyUtil extends QueryUtils { * otherwise returns a guard object */ getAuthGuard(db: CrudContract, model: string, operation: PolicyOperationKind, preValue?: 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}`); } @@ -318,7 +337,7 @@ export class PolicyUtil extends QueryUtils { * 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; } @@ -347,7 +366,7 @@ export class PolicyUtil extends QueryUtils { * @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; } @@ -1020,7 +1039,7 @@ export class PolicyUtil extends QueryUtils { * 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}`); } @@ -1028,7 +1047,7 @@ export class PolicyUtil extends QueryUtils { } 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}`); } @@ -1036,7 +1055,7 @@ export class PolicyUtil extends QueryUtils { } 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}`); } @@ -1053,7 +1072,7 @@ export class PolicyUtil extends QueryUtils { } 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}`); } @@ -1228,7 +1247,7 @@ export class PolicyUtil extends QueryUtils { } 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}`); } diff --git a/packages/runtime/src/enhancements/query-utils.ts b/packages/runtime/src/enhancements/query-utils.ts index 6959b922f..c161d5e2c 100644 --- a/packages/runtime/src/enhancements/query-utils.ts +++ b/packages/runtime/src/enhancements/query-utils.ts @@ -13,7 +13,7 @@ import { InternalEnhancementOptions } from './create-enhancement'; import { prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; export class QueryUtils { - constructor(private readonly prisma: DbClientContract, private readonly options: InternalEnhancementOptions) {} + constructor(private readonly prisma: DbClientContract, protected readonly options: InternalEnhancementOptions) {} getIdFields(model: string) { return getIdFields(this.options.modelMeta, model, true); 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 7508333b6..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,7 +1,7 @@ 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 () => { @@ -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,10 +44,9 @@ 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 = enhance(); }); @@ -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(); + }); +}); From c22803d987057854d308b50e8df756d1bd0a8730 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Apr 2024 22:16:39 +0800 Subject: [PATCH 099/127] chore: bump version --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index a08eddedc..8ffa2af4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 2233384c3..ce0e3f64c 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.7" +version = "2.0.0-beta.8" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 077abf1ab..af0f1312d 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 783fd177c..19e18af1e 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 8c408c366..12ff3c897 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": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index ce743692e..b631dfec0 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": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 8465bede3..3da6c46b8 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": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 0bba7e4da..0d816ef53 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": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index bdff0d124..c4e21c232 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": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 92f738f82..a34bc78cb 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 6ad5331f5..0e09fc636 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": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 61e4d1df9..5b83eb08c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 573da73f2..1973629bd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index ef3d6a9ce..0b4e2af53 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From bf688f0e9754251378e7b9809e69e43be9f46e27 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 10 Apr 2024 08:25:05 +0800 Subject: [PATCH 100/127] chore: fix npm install warnings (#1228) --- package.json | 8 +- packages/plugins/tanstack-query/package.json | 1 - .../tests/projects/t3-trpc-v10/package.json | 4 - packages/server/package.json | 7 +- pnpm-lock.yaml | 386 +++++++++--------- 5 files changed, 206 insertions(+), 200 deletions(-) diff --git a/package.json b/package.json index 8ffa2af4a..559572967 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,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", diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 0d816ef53..da54a1600 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -100,7 +100,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/trpc/tests/projects/t3-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json index ba0238e67..6098ca96b 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json @@ -20,14 +20,10 @@ "@trpc/next": "^10.43.6", "@trpc/react-query": "^10.43.6", "@trpc/server": "^10.43.6", - "@zenstackhq/language": "file:../../../../../../.build/zenstackhq-language-2.0.0-beta.7.tgz", - "@zenstackhq/runtime": "file:../../../../../../.build/zenstackhq-runtime-2.0.0-beta.7.tgz", - "@zenstackhq/sdk": "file:../../../../../../.build/zenstackhq-sdk-2.0.0-beta.7.tgz", "next": "^14.0.4", "react": "18.2.0", "react-dom": "18.2.0", "superjson": "^2.2.1", - "zenstack": "file:../../../../../../.build/zenstack-2.0.0-beta.7.tgz", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/server/package.json b/packages/server/package.json index 1973629bd..b234b0027 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -38,9 +38,9 @@ "zod-validation-error": "^1.5.0" }, "devDependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/platform-express": "^10.3.5", - "@nestjs/testing": "^10.3.5", + "@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", @@ -55,6 +55,7 @@ "isomorphic-fetch": "^3.0.0", "next": "^13.4.5", "nuxt": "^3.7.4", + "reflect-metadata": "^0.2.2", "supertest": "^6.3.3" }, "exports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a0c7d57f..fc0ceaf37 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.4.4) + 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.4.4) + 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.4.4) + 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) @@ -307,9 +307,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 @@ -699,14 +696,14 @@ importers: version: 1.5.0(zod@3.22.4) devDependencies: '@nestjs/common': - specifier: ^10.0.0 - version: 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) + specifier: ^10.3.7 + version: 10.3.7(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': - specifier: ^10.3.5 - version: 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5) + specifier: ^10.3.7 + version: 10.3.7(@nestjs/common@10.3.7)(@nestjs/core@10.3.5) '@nestjs/testing': - specifier: ^10.3.5 - version: 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5)(@nestjs/platform-express@10.3.5) + 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) @@ -748,7 +745,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.4.4) + 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 @@ -2541,13 +2541,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 @@ -2564,7 +2564,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 @@ -2573,8 +2573,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 @@ -2714,11 +2714,11 @@ packages: 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: @@ -2735,8 +2735,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: @@ -3074,8 +3074,8 @@ packages: - supports-color dev: true - /@nestjs/common@10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1): - resolution: {integrity: sha512-XWxbDf2ey/jAyEa3/XpckgfzJZ9j3I05ZkEFx7cAlebFuVKeq5UDDb5Sq9O7hMmbH9xdQj3pYT19SSj01hKeug==} + /@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: '*' @@ -3088,13 +3088,13 @@ packages: optional: true dependencies: iterare: 1.2.1 - reflect-metadata: 0.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.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(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): resolution: {integrity: sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==} requiresBuild: true peerDependencies: @@ -3112,13 +3112,13 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) - '@nestjs/platform-express': 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5) + '@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.1 + reflect-metadata: 0.2.2 rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 @@ -3126,25 +3126,25 @@ packages: - encoding dev: true - /@nestjs/platform-express@10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5): - resolution: {integrity: sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==} + /@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.5(reflect-metadata@0.2.1)(rxjs@7.8.1) - '@nestjs/core': 10.3.5(@nestjs/common@10.3.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@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.18.3 + express: 4.19.2 multer: 1.4.4-lts.1 tslib: 2.6.2 transitivePeerDependencies: - supports-color dev: true - /@nestjs/testing@10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5)(@nestjs/platform-express@10.3.5): - resolution: {integrity: sha512-j30/lxH0BayeDTigapYtQn/XhMRR7CzlFsm3dHoWViWQv0qT1r2ffe3927BbBLX3N/ZzglE10OAqW06ADZV8dw==} + /@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 @@ -3156,9 +3156,9 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.3.5(reflect-metadata@0.2.1)(rxjs@7.8.1) - '@nestjs/core': 10.3.5(@nestjs/common@10.3.5)(@nestjs/platform-express@10.3.5)(reflect-metadata@0.2.1)(rxjs@7.8.1) - '@nestjs/platform-express': 10.3.5(@nestjs/common@10.3.5)(@nestjs/core@10.3.5) + '@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 @@ -3578,7 +3578,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.4.4)(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: @@ -3617,7 +3617,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.4.4)(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: @@ -5092,6 +5092,10 @@ 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: @@ -5118,15 +5122,6 @@ packages: 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 @@ -5213,6 +5208,10 @@ 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: @@ -5281,64 +5280,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.4.4): - 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.4.4) - '@typescript-eslint/scope-manager': 6.13.1 - '@typescript-eslint/type-utils': 6.13.1(eslint@8.55.0)(typescript@5.4.4) - '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.4.4) - '@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.4.4) + 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.4.4): - 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.4.4) - '@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 + 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} @@ -5347,55 +5338,42 @@ packages: '@typescript-eslint/visitor-keys': 6.13.1 dev: true - /@typescript-eslint/type-utils@6.13.1(eslint@8.55.0)(typescript@5.4.4): - 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.4.4) - '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.4.4) + '@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.4.4) + 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.4.4): - resolution: {integrity: sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.60.1 - '@typescript-eslint/visitor-keys': 5.60.1 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.4.4) - typescript: 5.4.4 - transitivePeerDependencies: - - supports-color + /@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): @@ -5419,51 +5397,64 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@5.60.1(eslint@8.55.0)(typescript@5.4.4): - resolution: {integrity: sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.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: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true 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.4.4) - eslint: 8.55.0 - eslint-scope: 5.1.1 - semver: 7.5.4 + '@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 + 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.4.4): + /@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.4.4) - eslint: 8.55.0 + 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: @@ -5474,6 +5465,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 @@ -7073,6 +7072,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 @@ -8270,12 +8274,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.4.4): - 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': @@ -8283,23 +8287,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.4.4) - '@typescript-eslint/utils': 5.60.1(eslint@8.55.0)(typescript@5.4.4) - 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} @@ -8313,16 +8309,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 @@ -8343,7 +8339,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 @@ -8393,11 +8389,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'} @@ -8528,8 +8519,8 @@ packages: - supports-color dev: true - /express@4.18.3: - resolution: {integrity: sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==} + /express@4.19.2: + resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} dependencies: accepts: 1.3.8 @@ -8537,7 +8528,7 @@ packages: body-parser: 1.20.2 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.5.0 + cookie: 0.6.0 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 @@ -9568,6 +9559,11 @@ packages: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + /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==} engines: {node: '>=6'} @@ -11254,6 +11250,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'} @@ -11822,7 +11825,7 @@ packages: fsevents: 2.3.3 dev: true - /nuxt@3.7.4(@types/node@20.10.2)(eslint@8.55.0)(typescript@5.4.4): + /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 @@ -11840,7 +11843,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.4.4)(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 @@ -13358,9 +13361,8 @@ packages: engines: {node: '>=6'} dev: true - /reflect-metadata@0.2.1: - resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} - deprecated: This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer. + /reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} dev: true /regenerator-runtime@0.13.11: @@ -13668,6 +13670,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'} @@ -14614,6 +14624,15 @@ packages: 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 @@ -14721,6 +14740,7 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false /tslib@2.4.1: resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} @@ -14771,16 +14791,6 @@ packages: - ts-node dev: true - /tsutils@3.21.0(typescript@5.4.4): - 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.4.4 - dev: true - /tsx@4.7.1: resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} engines: {node: '>=18.0.0'} @@ -15325,7 +15335,7 @@ packages: - terser dev: true - /vite-plugin-checker@0.6.2(eslint@8.55.0)(typescript@5.4.4)(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: @@ -15361,7 +15371,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 From b6f4a9ee0aabfd354865a7237aa7b43dca4d75cd Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 10 Apr 2024 16:34:02 +0800 Subject: [PATCH 101/127] chore: rename runtime/prisma module to runtime/models (#1230) --- packages/runtime/package.json | 4 ++-- packages/runtime/res/models.d.ts | 1 + packages/runtime/res/prisma.d.ts | 1 - packages/schema/src/plugins/enhancer/enhance/index.ts | 4 ++-- packages/schema/src/plugins/enhancer/index.ts | 6 +++--- packages/schema/src/plugins/plugin-utils.ts | 4 ++-- packages/testtools/src/schema.ts | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 packages/runtime/res/models.d.ts delete mode 100644 packages/runtime/res/prisma.d.ts diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a34bc78cb..57bea59f3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -51,8 +51,8 @@ "types": "./model-meta.d.ts", "default": "./model-meta.js" }, - "./prisma": { - "types": "./prisma.d.ts" + "./models": { + "types": "./models.d.ts" } }, "publishConfig": { 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/res/prisma.d.ts b/packages/runtime/res/prisma.d.ts deleted file mode 100644 index 0068ce7ae..000000000 --- a/packages/runtime/res/prisma.d.ts +++ /dev/null @@ -1 +0,0 @@ -export type * from '.zenstack/prisma'; diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 7b042f05a..7e0f90000 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -67,7 +67,7 @@ export class EnhancerGenerator { // create a reexport of the logical prisma client const prismaDts = this.project.createSourceFile( - path.join(this.outDir, 'prisma.d.ts'), + path.join(this.outDir, 'models.d.ts'), `export type * from '${logicalPrismaClientDir}/index-fixed';`, { overwrite: true } ); @@ -75,7 +75,7 @@ export class EnhancerGenerator { } else { // just reexport the prisma client const prismaDts = this.project.createSourceFile( - path.join(this.outDir, 'prisma.d.ts'), + path.join(this.outDir, 'models.d.ts'), `export type * from '${prismaImport}';`, { overwrite: true } ); diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index df518a52e..79e8fd6e6 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -25,13 +25,13 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { if (dmmf) { // a logical client is generated if (typeof options.output === 'string') { - // get the absolute path of the logical prisma client - const prismaClientPathAbs = path.resolve(options.output, 'prisma'); + // 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 = `${RUNTIME_PACKAGE}/prisma`; + prismaClientPath = `${RUNTIME_PACKAGE}/models`; } } diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 74febf8fa..9a3da35e6 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -59,8 +59,8 @@ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { types: './model-meta.d.ts', default: './model-meta.js', }, - './prisma': { - types: './prisma.d.ts', + './models': { + types: './models.d.ts', }, }, }; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 450ea16d1..5742645a8 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -279,7 +279,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { 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/prisma': ['./node_modules/.zenstack/prisma.d.ts'], + '.zenstack/models': ['./node_modules/.zenstack/models.d.ts'], }; tsconfig.include = ['**/*.ts']; tsconfig.exclude = ['node_modules']; From 9334a2dee28934b0759f6def1d92833e9607247a Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 10 Apr 2024 17:00:28 +0800 Subject: [PATCH 102/127] chore: unify prisma internals package to v5 (#1231) --- .../tests/baseline/rpc-3.0.0.baseline.yaml | 587 +++++++++------- .../tests/baseline/rpc-3.1.0.baseline.yaml | 537 +++++++++------ .../rpc-type-coverage-3.0.0.baseline.yaml | 613 +++++++---------- .../rpc-type-coverage-3.1.0.baseline.yaml | 547 +++++++-------- .../tests/generator/prisma-generator.test.ts | 3 +- packages/sdk/package.json | 3 +- packages/sdk/src/prisma.ts | 24 +- pnpm-lock.yaml | 651 ++---------------- 8 files changed, 1219 insertions(+), 1746 deletions(-) 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 c583a19ed..3a82c7f0d 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: 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 4ea9b2547..dbf05a4dc 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: @@ -1720,21 +1796,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 @@ -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: 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 6d95c9a2f..f9da29092 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 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 cce476ffe..fe503bc4e 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: diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 048105ea4..70c59fc0f 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -139,8 +139,7 @@ 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())'); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5b83eb08c..093eff735 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -19,8 +19,7 @@ "license": "MIT", "dependencies": { "@prisma/generator-helper": "^5.0.0", - "@prisma/internals": "^4.16.0", - "@prisma/internals-v5": "npm:@prisma/internals@^5.0.0", + "@prisma/internals": "^5.0.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", "langium": "1.3.1", diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index 19b836cc2..e3e540654 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import type { DMMF } from '@prisma/generator-helper'; +import { getDMMF as _getDMMF, type GetDMMFOptions } from '@prisma/internals'; import path from 'path'; -import * as semver from 'semver'; import { RUNTIME_PACKAGE } from './constants'; import type { PluginOptions } from './types'; @@ -40,27 +40,11 @@ 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 with appropriate version + * Loads Prisma DMMF */ -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 getDMMF(options: GetDMMFOptions): Promise { + return _getDMMF(options); } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc0ceaf37..2553fe847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -627,11 +627,8 @@ importers: specifier: ^5.0.0 version: 5.0.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.0.0 + version: 5.7.0 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -888,11 +885,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'} @@ -3652,11 +3644,6 @@ packages: - encoding dev: true - /@opentelemetry/api@1.4.1: - resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} - engines: {node: '>=8.0.0'} - dev: false - /@opentelemetry/api@1.7.0: resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==} engines: {node: '>=8.0.0'} @@ -3857,16 +3844,6 @@ packages: dependencies: prisma: 5.7.1 - /@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: @@ -3891,16 +3868,6 @@ packages: /@prisma/engines-version@5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5: resolution: {integrity: sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==} - /@prisma/engines@4.16.2: - resolution: {integrity: sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==} - requiresBuild: true - dev: false - - /@prisma/engines@5.0.0: - resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==} - requiresBuild: true - dev: false - /@prisma/engines@5.7.0: resolution: {integrity: sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==} requiresBuild: true @@ -3920,56 +3887,6 @@ packages: '@prisma/fetch-engine': 5.7.1 '@prisma/get-platform': 5.7.1 - /@prisma/fetch-engine@4.16.2: - resolution: {integrity: sha512-lnCnHcOaNn0kw8qTJbVcNhyfIf5Lus2GFXbj3qpkdKEIB9xLgqkkuTP+35q1xFaqwQ0vy4HFpdRUpFP7njE15g==} - 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/fetch-engine@5.7.0: resolution: {integrity: sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==} dependencies: @@ -3985,17 +3902,6 @@ packages: '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 '@prisma/get-platform': 5.7.1 - /@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: @@ -4013,40 +3919,6 @@ packages: '@prisma/debug': 5.7.0 dev: false - /@prisma/get-platform@4.16.2: - resolution: {integrity: sha512-fnDey1/iSefHJRMB+w243BhWENf+paRouPMdCqIVqu8dYkR1NqhldblsSUC4Zr2sKS7Ta2sK4OLdt9IH+PZTfw==} - 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/get-platform@5.7.0: resolution: {integrity: sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==} dependencies: @@ -4058,108 +3930,6 @@ packages: dependencies: '@prisma/debug': 5.7.1 - /@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: @@ -4173,14 +3943,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 @@ -5137,6 +4899,7 @@ packages: /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true /@types/pg@8.10.2: resolution: {integrity: sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==} @@ -5184,10 +4947,6 @@ 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 @@ -5857,14 +5616,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==} @@ -5976,22 +5728,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'} @@ -6004,19 +5740,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'} @@ -6101,6 +5824,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==} @@ -6157,11 +5881,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'} @@ -6173,6 +5892,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==} @@ -6441,6 +6161,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==} @@ -6644,20 +6365,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: @@ -6729,6 +6436,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==} @@ -6745,11 +6453,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 @@ -6773,14 +6476,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} @@ -6958,21 +6653,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'} @@ -7124,14 +6810,7 @@ packages: 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==} @@ -7203,11 +6882,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} @@ -7525,20 +7199,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'} @@ -7625,6 +7285,7 @@ packages: engines: {node: '>=8'} dependencies: path-type: 4.0.0 + dev: true /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} @@ -7699,6 +7360,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==} @@ -7762,6 +7424,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==} @@ -7804,11 +7468,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'} @@ -7826,6 +7485,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==} @@ -8256,6 +7916,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==} @@ -8657,10 +8318,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 @@ -8758,15 +8415,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'} @@ -8789,6 +8437,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==} @@ -8796,6 +8445,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==} @@ -8867,10 +8517,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 @@ -8890,6 +8536,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==} @@ -8907,6 +8555,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==} @@ -8926,12 +8575,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'} @@ -9151,12 +8794,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==} @@ -9186,6 +8831,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==} @@ -9210,6 +8856,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==} @@ -9367,14 +9014,6 @@ 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: @@ -9397,6 +9036,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==} @@ -9452,16 +9092,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'} @@ -9487,16 +9117,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'} @@ -9548,16 +9168,10 @@ 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==} @@ -9589,6 +9203,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==} @@ -9605,6 +9220,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==} @@ -9667,6 +9283,7 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -9724,6 +9341,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==} @@ -9787,14 +9405,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==} @@ -9931,12 +9545,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==} @@ -10592,6 +10208,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==} @@ -10638,6 +10255,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==} @@ -10750,6 +10368,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==} @@ -10786,6 +10405,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==} @@ -10869,12 +10489,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==} @@ -10896,14 +10518,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==} @@ -10927,6 +10542,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==} @@ -10956,10 +10572,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 @@ -11084,6 +10696,7 @@ packages: engines: {node: '>=8'} dependencies: semver: 6.3.1 + dev: true /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -11232,6 +10845,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==} @@ -11362,6 +10976,7 @@ 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==} @@ -11424,11 +11039,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'} @@ -11690,18 +11300,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} @@ -11756,39 +11354,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'} @@ -12017,14 +11594,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'} @@ -12090,6 +11659,7 @@ packages: engines: {node: '>=8'} dependencies: p-map: 2.1.0 + dev: true /p-is-promise@3.0.0: resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} @@ -12101,12 +11671,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==} @@ -12120,35 +11692,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==} @@ -12188,6 +11749,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==} @@ -12250,6 +11812,7 @@ packages: /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==} @@ -12278,6 +11841,7 @@ packages: /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==} @@ -12462,6 +12026,7 @@ packages: engines: {node: '>=8'} dependencies: find-up: 4.1.0 + dev: true /pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} @@ -13012,11 +12577,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 @@ -13254,6 +12814,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==} @@ -13263,6 +12824,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==} @@ -13323,6 +12885,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==} @@ -13431,11 +12994,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'} @@ -13508,11 +13066,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'} @@ -13658,6 +13211,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==} @@ -13820,6 +13374,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==} @@ -13830,15 +13385,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'} @@ -13934,18 +13480,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==} @@ -14124,6 +13674,7 @@ packages: engines: {node: '>=8'} dependencies: min-indent: 1.0.1 + dev: true /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} @@ -14394,6 +13945,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==} @@ -14415,38 +13968,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'} @@ -14842,11 +14363,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'} @@ -14859,10 +14375,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==} @@ -15023,13 +14541,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'} @@ -15043,6 +14554,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==} @@ -15249,12 +14761,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 @@ -15281,6 +14787,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==} @@ -16020,6 +15527,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==} @@ -16030,15 +15538,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'} From f04824e5ed027deccfb7446d7c577a429b2bff3d Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:42:50 +0800 Subject: [PATCH 103/127] chore: bump version --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 559572967..8d2c227af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index ce0e3f64c..d7a6e1ebe 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.8" +version = "2.0.0-beta.9" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index af0f1312d..d335aaee9 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 19e18af1e..f92b25304 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 12ff3c897..7a122df28 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": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index b631dfec0..d07dc6b44 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": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 3da6c46b8..4a38adbd7 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": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index da54a1600..eb351ee37 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": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index c4e21c232..2a6931f42 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": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 57bea59f3..d70f48ee2 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 0e09fc636..c788f6289 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": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 093eff735..b12ab8063 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index b234b0027..fae6f5e22 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 0b4e2af53..40645e383 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 482912b2dda8c6ca57e291680c800ad0d3459c24 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 10 Apr 2024 20:07:07 +0800 Subject: [PATCH 104/127] chore: stricter Prisma peer dependencies (#1234) --- packages/runtime/package.json | 2 +- packages/schema/package.json | 6 +- pnpm-lock.yaml | 78 +++++++++---------- tests/integration/test-run/package.json | 4 +- .../nextjs/test-project/package.json | 4 +- .../frameworks/trpc/test-project/package.json | 4 +- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d70f48ee2..6021e7c6e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -79,7 +79,7 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "@prisma/client": "^5.0.0" + "@prisma/client": "5.0.0 - 5.12.x" }, "author": { "name": "ZenStack Team" diff --git a/packages/schema/package.json b/packages/schema/package.json index c788f6289..8d9ae1005 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -111,10 +111,10 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "prisma": "^5.0.0" + "prisma": "5.0.0 - 5.12.x" }, "devDependencies": { - "@prisma/client": "^5.7.1", + "@prisma/client": "5.12.0", "@types/async-exit-hook": "^2.0.0", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", @@ -126,7 +126,7 @@ "@zenstackhq/runtime": "workspace:*", "dotenv": "^16.0.3", "esbuild": "^0.15.12", - "prisma": "^5.7.1", + "prisma": "5.12.0", "renamer": "^4.0.0", "tmp": "^0.2.1", "tsc-alias": "^1.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2553fe847..ed2414ddb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -401,8 +401,8 @@ importers: packages/runtime: dependencies: '@prisma/client': - specifier: ^5.0.0 - version: 5.7.1(prisma@5.7.1) + specifier: 5.0.0 - 5.12.x + version: 5.12.0(prisma@5.12.0) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -569,8 +569,8 @@ importers: version: 1.5.0(zod@3.22.4) devDependencies: '@prisma/client': - specifier: ^5.7.1 - version: 5.7.1(prisma@5.7.1) + specifier: 5.12.0 + version: 5.12.0(prisma@5.12.0) '@types/async-exit-hook': specifier: ^2.0.0 version: 2.0.0 @@ -605,8 +605,8 @@ importers: specifier: ^0.15.12 version: 0.15.12 prisma: - specifier: ^5.7.1 - version: 5.7.1 + specifier: 5.12.0 + version: 5.12.0 renamer: specifier: ^4.0.0 version: 4.0.0 @@ -3821,8 +3821,8 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true - /@prisma/client@5.7.0: - resolution: {integrity: sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==} + /@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: @@ -3830,10 +3830,11 @@ packages: peerDependenciesMeta: prisma: optional: true - dev: true + dependencies: + prisma: 5.12.0 - /@prisma/client@5.7.1(prisma@5.7.1): - resolution: {integrity: sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==} + /@prisma/client@5.7.0: + resolution: {integrity: sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==} engines: {node: '>=16.13'} requiresBuild: true peerDependencies: @@ -3841,8 +3842,7 @@ packages: peerDependenciesMeta: prisma: optional: true - dependencies: - prisma: 5.7.1 + dev: true /@prisma/debug@5.0.0: resolution: {integrity: sha512-3q/M/KqlQ01/HJXifU/zCNOHkoTWu24kGelMF/IBrRxm7njPqTTbwfnT1dh4JK+nuWM5/Dg1Lv00u2c0l7AHxg==} @@ -3854,19 +3854,28 @@ packages: - 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/debug@5.7.1: - resolution: {integrity: sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==} + /@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-version@5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5: - resolution: {integrity: sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==} + /@prisma/engines@5.12.0: + resolution: {integrity: sha512-rFNRul9JGu0d3tf8etBgmDQ4NVoDwgGrRguvQOc8i+c6g7xPjRuu4aKzMMvHWUuccvRx5+fs1KMBxQ0x2THt+Q==} + requiresBuild: true + 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==} @@ -3878,14 +3887,12 @@ packages: '@prisma/get-platform': 5.7.0 dev: false - /@prisma/engines@5.7.1: - resolution: {integrity: sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==} - requiresBuild: true + /@prisma/fetch-engine@5.12.0: + resolution: {integrity: sha512-qkHQbZ1hspvOwcImvqY4yj7+FUlw0+uP+6tu3g24V4ULHOXLLkvr5ZZc6vy26OF0hkbD3kcDJCeutFis3poKgg==} dependencies: - '@prisma/debug': 5.7.1 - '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 - '@prisma/fetch-engine': 5.7.1 - '@prisma/get-platform': 5.7.1 + '@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==} @@ -3895,13 +3902,6 @@ packages: '@prisma/get-platform': 5.7.0 dev: false - /@prisma/fetch-engine@5.7.1: - resolution: {integrity: sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==} - dependencies: - '@prisma/debug': 5.7.1 - '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 - '@prisma/get-platform': 5.7.1 - /@prisma/generator-helper@5.0.0: resolution: {integrity: sha512-pufQ1mhoH6WzKNtzL79HZDoW4Ql3Lf8QEKVmBoW8e3Tdb50bxpYBYue5LBqp9vNW1xd1pgZO53cNiRfLX2d4Zg==} dependencies: @@ -3919,17 +3919,17 @@ packages: '@prisma/debug': 5.7.0 dev: false + /@prisma/get-platform@5.12.0: + resolution: {integrity: sha512-81Ptv9YJnwTArEBPQ2Lvu58sZPxy4OixKxVVgysFan6A3bFP7q8gIg15WTjsRuH4WXh6B667EM9sqoMTNu0fLQ==} + dependencies: + '@prisma/debug': 5.12.0 + /@prisma/get-platform@5.7.0: resolution: {integrity: sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==} dependencies: '@prisma/debug': 5.7.0 dev: false - /@prisma/get-platform@5.7.1: - resolution: {integrity: sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==} - dependencies: - '@prisma/debug': 5.7.1 - /@prisma/internals@5.7.0: resolution: {integrity: sha512-O9x47W1DECAyvNjYUx6oZHmTX10emKuBgsFHZemUbkIcJdCsp3X8Cy2JMJ5z3hqkRX6a6omMamFsWjuTARoaSw==} dependencies: @@ -12550,13 +12550,13 @@ packages: hasBin: true dev: true - /prisma@5.7.1: - resolution: {integrity: sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==} + /prisma@5.12.0: + resolution: {integrity: sha512-zxw4WSIvpsyNbpv8r7Fxgm7nwTFVmD6wbN6VuH13lClOceSANDOMl4jO3oxE6VzhjxmnEJqOGZjON2T2UpmLag==} engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 5.7.1 + '@prisma/engines': 5.12.0 /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json index 31497a99a..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": "^5.0.0", + "@prisma/client": "5.12.0", "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "^5.0.0", + "prisma": "5.12.0", "react": "^18.2.0", "swr": "^1.3.0", "typescript": "^4.9.3", diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json index 7b93ec340..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": "^5.0.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": "^5.0.0" + "prisma": "5.12.0" } } diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json index 8445cc451..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": "^5.0.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": "^5.0.0" + "prisma": "5.12.0" } } From f3796a8f794fcb266a5fc10406d0fc63ce6d417e Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 11 Apr 2024 13:14:46 +0800 Subject: [PATCH 105/127] chore: merge from dev (#1240) --- .../src/enhancements/policy/handler.ts | 25 ++++++++----- .../enhancer/policy/expression-writer.ts | 2 ++ .../enhancer/policy/policy-guard-generator.ts | 9 ++++- .../src/typescript-expression-transformer.ts | 4 +-- .../tests/regression/issue-1235.test.ts | 35 +++++++++++++++++++ 5 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1235.test.ts diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index ff57d9bae..ea27fc1db 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -641,16 +641,25 @@ export class PolicyProxyHandler implements Pr const postWriteChecks: PostWriteCheckRecord[] = []; // registers a post-update check task - const _registerPostUpdateCheck = async (model: string, uniqueFilter: any) => { + const _registerPostUpdateCheck = async ( + model: string, + preUpdateLookupFilter: any, + postUpdateLookupFilter: any + ) => { // both "post-update" rules and Zod schemas require a post-update check if (this.policyUtils.hasAuthGuard(model, 'postUpdate') || this.policyUtils.getZodSchema(model)) { // select pre-update field values let preValue: any; const preValueSelect = this.policyUtils.getPreValueSelect(model); if (preValueSelect && Object.keys(preValueSelect).length > 0) { - preValue = await db[model].findFirst({ where: uniqueFilter, select: preValueSelect }); + preValue = await db[model].findFirst({ where: preUpdateLookupFilter, select: preValueSelect }); } - postWriteChecks.push({ model, operation: 'postUpdate', uniqueFilter, preValue }); + postWriteChecks.push({ + model, + operation: 'postUpdate', + uniqueFilter: postUpdateLookupFilter, + preValue, + }); } }; @@ -777,7 +786,7 @@ export class PolicyProxyHandler implements Pr await this.policyUtils.checkPolicyForUnique(model, args, 'update', db, checkArgs); // register post-update check - await _registerPostUpdateCheck(model, args); + await _registerPostUpdateCheck(model, args, args); } } }; @@ -824,7 +833,7 @@ export class PolicyProxyHandler implements Pr await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); // handles the case where id fields are updated - const ids = this.policyUtils.clone(existing); + const postUpdateIds = this.policyUtils.clone(existing); for (const key of Object.keys(existing)) { const updateValue = (args as any).data ? (args as any).data[key] : (args as any)[key]; if ( @@ -832,12 +841,12 @@ export class PolicyProxyHandler implements Pr typeof updateValue === 'number' || typeof updateValue === 'bigint' ) { - ids[key] = updateValue; + postUpdateIds[key] = updateValue; } } // register post-update check - await _registerPostUpdateCheck(model, ids); + await _registerPostUpdateCheck(model, existing, postUpdateIds); } }, @@ -929,7 +938,7 @@ export class PolicyProxyHandler implements Pr await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); // register post-update check - await _registerPostUpdateCheck(model, uniqueFilter); + await _registerPostUpdateCheck(model, uniqueFilter, uniqueFilter); // convert upsert to update const convertedUpdate = { diff --git a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts index 9333634fa..3ce681dad 100644 --- a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -75,6 +75,8 @@ export class ExpressionWriter { this.plainExprBuilder = new TypeScriptExpressionTransformer({ context: ExpressionContext.AccessPolicy, isPostGuard: this.isPostGuard, + // in post-guard context, `this` references pre-update value + thisExprContext: this.isPostGuard ? 'context.preValue' : undefined, }); } diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index fa1eb831a..e438f291c 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -549,13 +549,19 @@ export class PolicyGenerator { // visit a reference or member access expression to build a // selection path const visit = (node: Expression): string[] | undefined => { + if (isThisExpr(node)) { + return []; + } + if (isReferenceExpr(node)) { const target = resolved(node.target); if (isDataModelField(target)) { // a field selection, it's a terminal return [target.name]; } - } else if (isMemberAccessExpr(node)) { + } + + if (isMemberAccessExpr(node)) { if (forAuthContext && isAuthInvocation(node.operand)) { return [node.member.$refText]; } @@ -571,6 +577,7 @@ export class PolicyGenerator { return [...inner, node.member.$refText]; } } + return undefined; }; diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts index b033dab96..8e33eb4a7 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -112,9 +112,7 @@ export class TypeScriptExpressionTransformer { throw new TypeScriptExpressionTransformerError(`Unresolved MemberAccessExpr`); } - if (isThisExpr(expr.operand)) { - return expr.member.ref.name; - } else if (isFutureExpr(expr.operand)) { + if (isFutureExpr(expr.operand)) { if (this.options?.isPostGuard !== true) { throw new TypeScriptExpressionTransformerError(`future() is only supported in postUpdate rules`); } diff --git a/tests/integration/tests/regression/issue-1235.test.ts b/tests/integration/tests/regression/issue-1235.test.ts new file mode 100644 index 000000000..1e9f80f86 --- /dev/null +++ b/tests/integration/tests/regression/issue-1235.test.ts @@ -0,0 +1,35 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1235', () => { + it('regression1', async () => { + const { enhance } = await loadSchema( + ` + model Post { + id Int @id @default(autoincrement()) + @@deny("update", future().id != id) + @@allow("all", true) + } + ` + ); + + const db = enhance(); + const post = await db.post.create({ data: {} }); + await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).toBeRejectedByPolicy(); + }); + + it('regression2', async () => { + const { enhance } = await loadSchema( + ` + model Post { + id Int @id @default(autoincrement()) + @@deny("update", future().id != this.id) + @@allow("all", true) + } + ` + ); + + const db = enhance(); + const post = await db.post.create({ data: {} }); + await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).toBeRejectedByPolicy(); + }); +}); From 81c7e87bbb1e9316bb9afa02192672d1b4ee25c6 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 11 Apr 2024 15:41:50 +0800 Subject: [PATCH 106/127] chore: pin version of @prisma/internals and @prisma/generator-helper (#1242) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 3 +- .../plugins/openapi/src/generator-base.ts | 10 ++- .../plugins/openapi/src/rest-generator.ts | 2 +- packages/plugins/openapi/src/rpc-generator.ts | 10 +-- packages/plugins/swr/package.json | 3 +- packages/plugins/swr/src/generator.ts | 2 +- packages/plugins/tanstack-query/package.json | 3 +- .../plugins/tanstack-query/src/generator.ts | 2 +- packages/plugins/trpc/package.json | 3 +- packages/plugins/trpc/src/generator.ts | 4 +- packages/plugins/trpc/src/helpers.ts | 3 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 3 +- packages/schema/src/cli/plugin-runner.ts | 2 +- .../src/plugins/enhancer/enhance/index.ts | 8 +-- packages/schema/src/plugins/zod/generator.ts | 12 +--- .../schema/src/plugins/zod/transformer.ts | 19 +++--- packages/schema/src/plugins/zod/types.ts | 4 +- packages/sdk/package.json | 6 +- .../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/types.ts | 2 + packages/server/package.json | 2 +- packages/testtools/package.json | 3 +- packages/testtools/src/schema.ts | 3 +- pnpm-lock.yaml | 62 ++----------------- 36 files changed, 72 insertions(+), 139 deletions(-) diff --git a/package.json b/package.json index 8d2c227af..90061ea07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index d7a6e1ebe..361f2e1cd 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.9" +version = "2.0.0-beta.10" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index d335aaee9..8616104c3 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index f92b25304..285fee53b 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 7a122df28..1c20b2379 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": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index d07dc6b44..748290a95 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": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "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 e033a5206..2b692dd1b 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,5 +1,11 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions, PluginResult, getDataModels, hasAttribute } from '@zenstackhq/sdk'; +import { + PluginError, + getDataModels, + hasAttribute, + type DMMF, + type PluginOptions, + type PluginResult, +} from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import semver from 'semver'; diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index b1ed77829..d248145fd 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, @@ -10,6 +9,7 @@ import { isRelationshipField, requireOption, resolvePath, + type DMMF, } from '@zenstackhq/sdk'; import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; import fs from 'fs'; diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 724e58839..86c7197ad 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,7 +1,6 @@ // 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 { analyzePolicies, PluginError, requireOption, resolvePath, type DMMF } from '@zenstackhq/sdk'; import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypesForAggregate, @@ -773,10 +772,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 +801,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/swr/package.json b/packages/plugins/swr/package.json index 4a38adbd7..aee59d227 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": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "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 fd8072544..ad2a51223 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -1,4 +1,3 @@ -import type { DMMF } from '@prisma/generator-helper'; import { PluginOptions, createProject, @@ -10,6 +9,7 @@ import { requireOption, resolvePath, saveProject, + type DMMF, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; import { paramCase } from 'change-case'; diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index eb351ee37..234cb8a16 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": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "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", diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 20f8542d2..4a2e0cd8b 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -1,4 +1,3 @@ -import type { DMMF } from '@prisma/generator-helper'; import { PluginError, PluginOptions, @@ -11,6 +10,7 @@ import { requireOption, resolvePath, saveProject, + type DMMF, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; import { paramCase } from 'change-case'; diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 2a6931f42..07c5a5905 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": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { @@ -26,7 +26,6 @@ "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", diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 6249fbf35..cbeb8f191 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -1,8 +1,6 @@ -import type { DMMF } from '@prisma/generator-helper'; import { CrudFailureReason, PluginError, - PluginOptions, RUNTIME_PACKAGE, ensureEmptyDir, getPrismaClientImportSpec, @@ -10,6 +8,8 @@ import { requireOption, resolvePath, saveProject, + type DMMF, + type PluginOptions, } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; import fs from 'fs'; diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index 3580ca700..4b0030c1c 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -1,5 +1,4 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { PluginError, getPrismaClientImportSpec, type PluginOptions } from '@zenstackhq/sdk'; +import { PluginError, getPrismaClientImportSpec, type DMMF, type PluginOptions } from '@zenstackhq/sdk'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 6021e7c6e..4b47cb1db 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 8d9ae1005..794214b99 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": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "author": { "name": "ZenStack Team" }, @@ -79,7 +79,6 @@ }, "dependencies": { "@paralleldrive/cuid2": "^2.2.0", - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", "async-exit-hook": "^2.0.1", diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index b07a592f3..48a4d615c 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -1,6 +1,5 @@ /* 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, @@ -12,6 +11,7 @@ import { PluginError, resolvePath, saveProject, + type DMMF, type OptionValue, type PluginDeclaredOptions, type PluginFunction, diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 7e0f90000..108e2d17d 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -1,23 +1,23 @@ -import type { DMMF } from '@prisma/generator-helper'; import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { + PluginError, getAttribute, getAttributeArg, getAuthModel, - getDataModels, getDMMF, + getDataModels, getPrismaClientImportSpec, isDelegateModel, - PluginError, + type DMMF, type PluginOptions, } from '@zenstackhq/sdk'; import { DataModel, DataModelField, + ReferenceExpr, isArrayExpr, isDataModel, isReferenceExpr, - ReferenceExpr, type Model, } from '@zenstackhq/sdk/ast'; import fs from 'fs'; diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 1f25606b4..baacf0f96 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,10 +1,8 @@ -import { ConnectorType, DMMF } from '@prisma/generator-helper'; import { PluginGlobalOptions, PluginOptions, ensureEmptyDir, getDataModels, - getLiteral, getPrismaClientImportSpec, hasAttribute, isEnumFieldReference, @@ -12,8 +10,9 @@ import { isFromStdlib, parseOptionAsStrings, resolvePath, + type DMMF, } 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 { streamAllContents } from 'langium'; import path from 'path'; @@ -84,12 +83,6 @@ export class ZodSchemaGenerator { prismaClientDmmf.schema.enumTypes.model ?? [] ); - const dataSource = this.model.declarations.find((d): d is DataSource => isDataSource(d)); - - const dataSourceProvider = getLiteral( - dataSource?.fields.find((f) => f.name === 'provider')?.value - ) as ConnectorType; - await this.generateModelSchemas(output, excludeModels); if (this.options.modelOnly) { @@ -102,7 +95,6 @@ export class ZodSchemaGenerator { ); } else { // detailed object schemas referenced from input schemas - Transformer.provider = dataSourceProvider; addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models); const aggregateOperationSupport = resolveAggregateOperationSupport(inputObjectTypes); await this.generateObjectSchemas(inputObjectTypes, output); diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 0471dec3f..7ad736097 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; -import { getPrismaClientImportSpec, getPrismaVersion, type PluginOptions } from '@zenstackhq/sdk'; +import { + getPrismaClientImportSpec, + getPrismaVersion, + type PluginOptions, + type DMMF as PrismaDMMF, +} from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; import { indentString } from '@zenstackhq/sdk/utils'; import path from 'path'; @@ -21,12 +25,11 @@ 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 inputObjectTypes: DMMF.InputType[]; + private inputObjectTypes: PrismaDMMF.InputType[]; public sourceFiles: SourceFile[] = []; constructor(params: TransformerParams) { @@ -187,7 +190,7 @@ export default class Transformer { wrapWithZodValidators( mainValidators: string | string[], field: PrismaDMMF.SchemaArg, - inputType: PrismaDMMF.SchemaArgInputType + inputType: PrismaDMMF.InputTypeRef ) { let line = ''; @@ -217,11 +220,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); diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts index e71b3b03a..a33706e4f 100644 --- a/packages/schema/src/plugins/zod/types.ts +++ b/packages/schema/src/plugins/zod/types.ts @@ -1,4 +1,4 @@ -import { DMMF, DMMF as PrismaDMMF } from '@prisma/generator-helper'; +import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk'; import { Project } from 'ts-morph'; export type TransformerParams = { @@ -11,7 +11,7 @@ export type TransformerParams = { isDefaultPrismaClientOutput?: boolean; prismaClientOutputPath?: string; project: Project; - inputObjectTypes: DMMF.InputType[]; + inputObjectTypes: PrismaDMMF.InputType[]; }; export type AggregateOperationSupport = { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b12ab8063..988b1a06e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { @@ -18,8 +18,8 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", - "@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", diff --git a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts index 662f0b8a4..b53f34260 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 '../types'; 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..0699bca9a 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 '../types'; +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..4e83b8590 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 '../types'; 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..f249d7c20 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 '../types'; 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..444f2ce90 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 '../types'; 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..74b13de55 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 '../types'; 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..83858ba02 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 '../types'; 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/types.ts b/packages/sdk/src/types.ts index a6a4b8629..7ba72807d 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -93,3 +93,5 @@ export class PluginError extends Error { super(message); } } + +export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/server/package.json b/packages/server/package.json index fae6f5e22..1c0e0016d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 40645e383..54ec4cad3 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "ZenStack Test Tools", "main": "index.js", "private": true, @@ -19,7 +19,6 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^5.0.0", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 5742645a8..9e19ab1b6 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,6 +1,5 @@ /* 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 { DEFAULT_RUNTIME_LOAD_PATH, @@ -9,7 +8,7 @@ import { type EnhancementKind, type EnhancementOptions, } from '@zenstackhq/runtime'; -import { getDMMF } from '@zenstackhq/sdk'; +import { getDMMF, type DMMF } from '@zenstackhq/sdk'; import { execSync } from 'child_process'; import * as fs from 'fs'; import json from 'json5'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed2414ddb..8f14f0dee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -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 @@ -477,9 +465,6 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.0 version: 2.2.0 - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -624,10 +609,10 @@ 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: ^5.0.0 + specifier: 5.7.0 version: 5.7.0 '@zenstackhq/language': specifier: workspace:* @@ -753,9 +738,6 @@ importers: packages/testtools: dependencies: - '@prisma/generator-helper': - specifier: ^5.0.0 - version: 5.0.0 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -3844,16 +3826,6 @@ packages: optional: true dev: true - /@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==} @@ -3902,17 +3874,6 @@ packages: '@prisma/get-platform': 5.7.0 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: @@ -4756,18 +4717,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 @@ -4880,10 +4829,6 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true - /@types/ms@0.7.31: - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} - dev: false - /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true @@ -10318,6 +10263,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==} From 2a8db68b4ad6ae7a6433049ea6f323d3a8a60446 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 13 Apr 2024 15:48:14 +0800 Subject: [PATCH 107/127] fix: issue with `auth()` in `@default` accidentally overrides "connect" (#1248) --- .../runtime/src/enhancements/default-auth.ts | 5 +++++ .../enhancements/with-policy/auth.test.ts | 21 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts index 78294f28b..56e43ab29 100644 --- a/packages/runtime/src/enhancements/default-auth.ts +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -92,6 +92,11 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { } 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 diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index 9079da045..0cac82e8a 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -417,10 +417,11 @@ describe('auth() runtime test', () => { }); it('Default auth() with foreign key', async () => { - const { enhance, modelMeta } = await loadSchema( + const { enhance, prisma } = await loadSchema( ` model User { id String @id + email String @unique posts Post[] @@allow('all', true) @@ -438,9 +439,23 @@ describe('auth() runtime test', () => { ` ); + 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' }); - await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); - await expect(db.post.create({ data: { title: 'abc' } })).resolves.toMatchObject({ authorId: '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 () => { From 0355193399dfe40c0f94fbc450f18c88b5415d94 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 13 Apr 2024 16:03:37 +0800 Subject: [PATCH 108/127] fix(zmodel): clean up logic for detecting what attributes should be inherited from base models (#1249) --- packages/schema/src/utils/ast-utils.ts | 33 ++++++++--- packages/schema/tests/schema/abstract.test.ts | 20 +++++++ packages/sdk/src/utils.ts | 56 +++++++++++-------- .../with-delegate/issue-1243.test.ts | 55 ++++++++++++++++++ 4 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1243.test.ts diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index fbb9e4ae2..d33f27e71 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -1,6 +1,7 @@ import { BinaryExpr, DataModel, + DataModelAttribute, DataModelField, Expression, InheritableNode, @@ -63,14 +64,7 @@ export function mergeBaseModel(model: Model, linker: Linker) { .concat(dataModel.fields); dataModel.attributes = bases - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .flatMap((base) => base.attributes) - // don't inherit skip-level attributes - .filter((attr) => !attr.$inheritedFrom) - // don't inherit `@@delegate` attribute - .filter((attr) => attr.decl.$refText !== '@@delegate') - // don't inherit `@@map` attribute - .filter((attr) => attr.decl.$refText !== '@@map') + .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(base, attr))) .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); @@ -85,6 +79,29 @@ export function mergeBaseModel(model: Model, linker: Linker) { model.declarations = model.declarations.filter((x) => !(isDataModel(x) && x.isAbstract)); } +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, 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/sdk/src/utils.ts b/packages/sdk/src/utils.ts index e3ed44b99..6617983aa 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -176,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.$refText === '@@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.$refText === '@@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 []; } /** 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'], + }); + }); +}); From d97c89b8b76c2d713e2b83374dd88533330905db Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 13 Apr 2024 16:04:00 +0800 Subject: [PATCH 109/127] chore: bump version (#1251) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 90061ea07..b1df19b8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 361f2e1cd..f5cba794b 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.10" +version = "2.0.0-beta.11" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 8616104c3..d7fa54ac0 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 285fee53b..c3f76ae8e 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 1c20b2379..553d542e7 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": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 748290a95..a075d2c3a 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": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index aee59d227..5a3cf4bfd 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": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 234cb8a16..845f2e784 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": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 07c5a5905..3db4c79a6 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": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 4b47cb1db..762f84f8e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 794214b99..f9f810f2b 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": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 988b1a06e..722b53d1c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 1c0e0016d..9c20400d8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 54ec4cad3..bff697756 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 70903d3f6c73babdc1d25be0488171d7c85445de Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 13 Apr 2024 16:16:30 +0800 Subject: [PATCH 110/127] merge from dev (#1250) --- packages/plugins/openapi/src/rpc-generator.ts | 10 ++- .../tests/baseline/rpc-3.0.0.baseline.yaml | 30 ++++++- .../tests/baseline/rpc-3.1.0.baseline.yaml | 30 ++++++- .../rpc-type-coverage-3.0.0.baseline.yaml | 10 ++- .../rpc-type-coverage-3.1.0.baseline.yaml | 10 ++- .../routers/generated/routers/Post.router.ts | 16 ---- .../routers/generated/routers/User.router.ts | 16 ---- .../schema/src/plugins/zod/transformer.ts | 2 +- .../tests/regression/issue-1241.test.ts | 88 +++++++++++++++++++ 9 files changed, 170 insertions(+), 42 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1241.test.ts diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 86c7197ad..cd8ef133e 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -175,7 +175,15 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', required: ['data'], properties: { - data: this.ref(`${modelName}CreateManyInput`), + data: this.oneOf( + this.ref(`${modelName}CreateManyInput`), + this.array(this.ref(`${modelName}CreateManyInput`)) + ), + skipDuplicates: { + type: 'boolean', + description: + 'Do not insert records with unique fields or ID fields that already exist.', + }, meta: this.ref('_Meta'), }, }, 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 3a82c7f0d..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 @@ -3275,7 +3275,15 @@ components: - data properties: data: - $ref: '#/components/schemas/UserCreateManyInput' + oneOf: + - $ref: '#/components/schemas/UserCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/UserCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' UserFindUniqueArgs: @@ -3455,7 +3463,15 @@ components: - data properties: data: - $ref: '#/components/schemas/ProfileCreateManyInput' + oneOf: + - $ref: '#/components/schemas/ProfileCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/ProfileCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' ProfileFindUniqueArgs: @@ -3635,7 +3651,15 @@ components: - data properties: data: - $ref: '#/components/schemas/Post_ItemCreateManyInput' + oneOf: + - $ref: '#/components/schemas/Post_ItemCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' Post_ItemFindUniqueArgs: 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 dbf05a4dc..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 @@ -3339,7 +3339,15 @@ components: - data properties: data: - $ref: '#/components/schemas/UserCreateManyInput' + oneOf: + - $ref: '#/components/schemas/UserCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/UserCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' UserFindUniqueArgs: @@ -3519,7 +3527,15 @@ components: - data properties: data: - $ref: '#/components/schemas/ProfileCreateManyInput' + oneOf: + - $ref: '#/components/schemas/ProfileCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/ProfileCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' ProfileFindUniqueArgs: @@ -3699,7 +3715,15 @@ components: - data properties: data: - $ref: '#/components/schemas/Post_ItemCreateManyInput' + oneOf: + - $ref: '#/components/schemas/Post_ItemCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/Post_ItemCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' Post_ItemFindUniqueArgs: 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 f9da29092..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 @@ -1922,7 +1922,15 @@ components: - data properties: data: - $ref: '#/components/schemas/FooCreateManyInput' + oneOf: + - $ref: '#/components/schemas/FooCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/FooCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' FooFindUniqueArgs: 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 fe503bc4e..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 @@ -1964,7 +1964,15 @@ components: - data properties: data: - $ref: '#/components/schemas/FooCreateManyInput' + oneOf: + - $ref: '#/components/schemas/FooCreateManyInput' + - type: array + items: + $ref: '#/components/schemas/FooCreateManyInput' + skipDuplicates: + type: boolean + description: Do not insert records with unique fields or ID fields that already + exist. meta: $ref: '#/components/schemas/_Meta' FooFindUniqueArgs: 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 4d2ade66f..fbc73cf06 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 @@ -12,8 +12,6 @@ export default function createRouter(router: RouterFa 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))), @@ -62,20 +60,6 @@ export interface ClientType >; - }; - createMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostCreateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >,) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - }; create: { 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 00a591ca1..c4bdb89de 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 @@ -12,8 +12,6 @@ export default function createRouter(router: RouterFa 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))), @@ -62,20 +60,6 @@ export interface ClientType >; - }; - createMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserCreateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >,) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - }; create: { diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 7ad736097..5b62687ff 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -490,7 +490,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; imports.push( `import { ${modelName}CreateManyInputObjectSchema } from '../objects/${modelName}CreateManyInput.schema'` ); - codeBody += `createMany: z.object({ data: z.union([${modelName}CreateManyInputObjectSchema, z.array(${modelName}CreateManyInputObjectSchema)]) }),`; + codeBody += `createMany: z.object({ data: z.union([${modelName}CreateManyInputObjectSchema, z.array(${modelName}CreateManyInputObjectSchema)]), skipDuplicates: z.boolean().optional() }),`; operations.push(['createMany', origModelName]); } diff --git a/tests/integration/tests/regression/issue-1241.test.ts b/tests/integration/tests/regression/issue-1241.test.ts new file mode 100644 index 000000000..3a53f567c --- /dev/null +++ b/tests/integration/tests/regression/issue-1241.test.ts @@ -0,0 +1,88 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import { randomBytes } from 'crypto'; + +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) + } + `, + { logPrismaQuery: true } + ); + + const user = await prisma.user.create({ + data: {}, + }); + await prisma.todo.create({ + data: { + user_id: user.id, + + images: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `img-label-${i + 1}`, + })), + }, + + documents: { + create: new Array(3).fill(null).map((_, i) => ({ + s3_key: randomBytes(8).toString('hex'), + label: `doc-label-${i + 1}`, + })), + }, + }, + }); + + const db = enhance(); + + const todo = await db.todo.findFirst({ where: {}, include: { documents: true } }); + await expect( + db.todo.update({ + where: { id: todo.id }, + data: { + documents: { + update: todo.documents.map((doc: any) => { + return { + where: { s3_key: doc.s3_key }, + data: { label: 'updated' }, + }; + }), + }, + }, + include: { documents: true }, + }) + ).toResolveTruthy(); + }); +}); From f05a052a0d113e5f8effc4870b6b279b1dafa87e Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 14 Apr 2024 01:06:47 +0800 Subject: [PATCH 111/127] fix: properly set fields with default auth value to optional in create input (#1252) --- .../src/plugins/enhancer/enhance/index.ts | 2 + .../src/plugins/prisma/schema-generator.ts | 47 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 108e2d17d..0c630e28c 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -245,6 +245,8 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara ]); }); + // 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, }); diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 649362eff..d7a62f0d0 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -28,7 +28,7 @@ import { NumberLiteral, StringLiteral, } from '@zenstackhq/language/ast'; -import { match } from 'ts-pattern'; +import { match, P } from 'ts-pattern'; import { getIdFields } from '../../utils/ast-utils'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; @@ -36,12 +36,10 @@ import { getAttribute, getAttributeArg, getAttributeArgLiteral, - getForeignKeyFields, getLiteral, getPrismaVersion, isDelegateModel, isIdField, - isRelationshipField, PluginError, PluginOptions, resolved, @@ -59,6 +57,7 @@ import { execPackage } from '../../utils/exec-utils'; import { isDefaultWithAuth } from '../enhancer/enhancer-utils'; import { AttributeArgValue, + ModelField, ModelFieldType, AttributeArg as PrismaAttributeArg, AttributeArgValue as PrismaAttributeArgValue, @@ -587,23 +586,6 @@ export class PrismaSchemaGenerator { const type = new ModelFieldType(fieldType, field.type.array, field.type.optional); - if (this.mode === 'logical') { - if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { - // field has `@default` with `auth()`, it should be set optional, and the - // default value setting is handled outside Prisma - type.optional = true; - } - - if (isRelationshipField(field)) { - // if foreign key field has `@default` with `auth()`, the relation - // field should be set optional - const foreignKeyFields = getForeignKeyFields(field); - if (foreignKeyFields.some((fkField) => fkField.attributes.some((attr) => isDefaultWithAuth(attr)))) { - type.optional = true; - } - } - } - const attributes = field.attributes .filter((attr) => this.isPrismaAttribute(attr)) // `@default` with `auth()` is handled outside Prisma @@ -626,10 +608,35 @@ export class PrismaSchemaGenerator { 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); } From 618cc9ddaa1bea24e5703665eee8548bd5b56591 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 14 Apr 2024 08:50:06 +0800 Subject: [PATCH 112/127] chore: bump version --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index b1df19b8e..949514890 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index f5cba794b..505a22fc1 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.11" +version = "2.0.0-beta.12" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index d7fa54ac0..e4302144a 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index c3f76ae8e..e2d8a2fe7 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 553d542e7..eb0998dc1 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": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index a075d2c3a..6f4d68acf 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": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 5a3cf4bfd..84ef25f85 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": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 845f2e784..21c3f3a5d 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": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 3db4c79a6..46bf731f1 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": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 762f84f8e..24d637306 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index f9f810f2b..f5af5c3f3 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": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 722b53d1c..6a7d24fce 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 9c20400d8..45bc536ff 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index bff697756..4e9382ec9 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From b702a1077f9ebe797da9aa52da3ccd4b30f7ef12 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 14 Apr 2024 20:05:29 +0800 Subject: [PATCH 113/127] chore: format generated PLS automatically by default (#1253) --- packages/schema/src/plugins/prisma/schema-generator.ts | 2 +- packages/schema/tests/generator/prisma-generator.test.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index d7a62f0d0..1e5cdb318 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -141,7 +141,7 @@ export class PrismaSchemaGenerator { } 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}`, { stdio: 'ignore' }); diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 70c59fc0f..5e629bc2a 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -36,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 { @@ -44,6 +44,10 @@ describe('Prisma generator test', () => { previewFeatures = ["multiSchema", "postgresqlExtensions"] } + plugin prisma { + provider = '@core/prisma' + } + model User { id String @id @@ -56,6 +60,7 @@ describe('Prisma generator test', () => { provider: '@core/prisma', schemaPath: 'schema.zmodel', output: 'schema.prisma', + format: false, }); const content = fs.readFileSync('schema.prisma', 'utf-8'); @@ -97,6 +102,7 @@ describe('Prisma generator test', () => { provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, + format: false, }); const content = fs.readFileSync(name, 'utf-8'); @@ -328,6 +334,7 @@ describe('Prisma generator test', () => { schemaPath: 'schema.zmodel', output: name, generateClient: false, + format: false, }); const content = fs.readFileSync(name, 'utf-8'); From c37bf9263b357f41692985b24e0b14014da4ddfa Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 14 Apr 2024 22:48:40 +0800 Subject: [PATCH 114/127] fix(sdk): exclude prisma module from barrel file to avoid eager loading of prisma packages (#1255) --- .../plugins/openapi/src/generator-base.ts | 10 ++------- .../plugins/openapi/src/rest-generator.ts | 2 +- packages/plugins/openapi/src/rpc-generator.ts | 3 ++- packages/plugins/swr/src/generator.ts | 12 ++--------- .../plugins/tanstack-query/src/generator.ts | 12 ++--------- packages/plugins/trpc/src/generator.ts | 3 +-- packages/plugins/trpc/src/helpers.ts | 3 ++- packages/schema/src/cli/plugin-runner.ts | 2 +- .../src/plugins/enhancer/enhance/index.ts | 4 +--- .../enhancer/policy/policy-guard-generator.ts | 2 +- packages/schema/src/plugins/prisma/index.ts | 3 ++- .../src/plugins/prisma/schema-generator.ts | 16 +------------- packages/schema/src/plugins/zod/generator.ts | 3 +-- .../schema/src/plugins/zod/transformer.ts | 21 +++---------------- packages/schema/src/plugins/zod/types.ts | 2 +- packages/schema/src/telemetry.ts | 2 +- .../tests/generator/prisma-builder.test.ts | 2 +- .../tests/generator/prisma-generator.test.ts | 2 +- packages/sdk/package.json | 18 ++++++++++++++++ .../sdk/src/dmmf-helpers/aggregate-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/include-helpers.ts | 2 +- .../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 | 2 +- packages/sdk/src/dmmf-helpers/types.ts | 2 +- packages/sdk/src/index.ts | 1 - packages/sdk/src/prisma.ts | 2 ++ packages/sdk/src/types.ts | 2 -- packages/testtools/src/schema.ts | 2 +- 30 files changed, 54 insertions(+), 89 deletions(-) diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index 2b692dd1b..38cddf16c 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,12 +1,6 @@ -import { - PluginError, - getDataModels, - hasAttribute, - type DMMF, - type PluginOptions, - type PluginResult, -} 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'; diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index d248145fd..90383f8f9 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -9,9 +9,9 @@ import { isRelationshipField, requireOption, resolvePath, - type DMMF, } 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'; diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index cd8ef133e..cb388aae2 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,6 +1,6 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator -import { analyzePolicies, PluginError, requireOption, resolvePath, type DMMF } from '@zenstackhq/sdk'; +import { analyzePolicies, PluginError, requireOption, resolvePath } from '@zenstackhq/sdk'; import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypesForAggregate, @@ -10,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'; diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index ad2a51223..4ab3fb79e 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -4,17 +4,14 @@ import { ensureEmptyDir, generateModelMeta, getDataModels, - getPrismaClientImportSpec, - getPrismaVersion, requireOption, resolvePath, saveProject, - type DMMF, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; import path from 'path'; -import semver from 'semver'; import type { OptionalKind, ParameterDeclarationStructure, Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; @@ -74,7 +71,6 @@ function generateModelHooks( ]); const modelNameCap = upperCaseFirst(model.name); - const prismaVersion = getPrismaVersion(); const mutationFuncs: string[] = []; @@ -168,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>>`, diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 4a2e0cd8b..7666cb742 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -5,18 +5,15 @@ import { ensureEmptyDir, generateModelMeta, getDataModels, - getPrismaClientImportSpec, - getPrismaVersion, requireOption, resolvePath, saveProject, - type DMMF, } 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'; @@ -274,7 +271,6 @@ function generateModelHooks( 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 }); @@ -401,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 & diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index cbeb8f191..9d2cc5724 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -3,15 +3,14 @@ import { PluginError, RUNTIME_PACKAGE, ensureEmptyDir, - getPrismaClientImportSpec, parseOptionAsStrings, requireOption, resolvePath, saveProject, - type DMMF, 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'; diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index 4b0030c1c..947b96b98 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -1,4 +1,5 @@ -import { PluginError, getPrismaClientImportSpec, type DMMF, type PluginOptions } from '@zenstackhq/sdk'; +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'; diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 48a4d615c..7b725cc28 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -11,12 +11,12 @@ import { PluginError, resolvePath, saveProject, - type DMMF, type OptionValue, type PluginDeclaredOptions, type PluginFunction, type PluginResult, } from '@zenstackhq/sdk'; +import { type DMMF } from '@zenstackhq/sdk/prisma'; import colors from 'colors'; import ora from 'ora'; import path from 'path'; diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 0c630e28c..cf75c3315 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -4,11 +4,8 @@ import { getAttribute, getAttributeArg, getAuthModel, - getDMMF, getDataModels, - getPrismaClientImportSpec, isDelegateModel, - type DMMF, type PluginOptions, } from '@zenstackhq/sdk'; import { @@ -20,6 +17,7 @@ import { 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 { diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index e438f291c..753ef8f19 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -37,7 +37,6 @@ import { getDataModels, getIdFields, getLiteral, - getPrismaClientImportSpec, hasAttribute, hasValidationAttributes, isAuthInvocation, @@ -47,6 +46,7 @@ import { isFutureExpr, resolved, } 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'; diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 478b6a54b..b016d17f9 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,5 +1,6 @@ -import { PluginError, PluginFunction, getDMMF, getLiteral, resolvePath } from '@zenstackhq/sdk'; +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'; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 1e5cdb318..d7e7f5b9a 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -28,6 +28,7 @@ import { NumberLiteral, StringLiteral, } from '@zenstackhq/language/ast'; +import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; import { match, P } from 'ts-pattern'; import { getIdFields } from '../../utils/ast-utils'; @@ -37,7 +38,6 @@ import { getAttributeArg, getAttributeArgLiteral, getLiteral, - getPrismaVersion, isDelegateModel, isIdField, PluginError, @@ -224,20 +224,6 @@ export 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) { diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index baacf0f96..5e542540d 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -3,17 +3,16 @@ import { PluginOptions, ensureEmptyDir, getDataModels, - getPrismaClientImportSpec, hasAttribute, isEnumFieldReference, isForeignKeyField, isFromStdlib, parseOptionAsStrings, resolvePath, - type DMMF, } from '@zenstackhq/sdk'; import { DataModel, EnumField, Model, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { streamAllContents } from 'langium'; import path from 'path'; import type { SourceFile } from 'ts-morph'; diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 5b62687ff..f4c8a167d 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,14 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { - getPrismaClientImportSpec, - getPrismaVersion, - type PluginOptions, - type DMMF as PrismaDMMF, -} 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 type { Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { AggregateOperationSupport, TransformerParams } from './types'; @@ -568,10 +562,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'` @@ -629,12 +619,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 diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts index a33706e4f..b64995448 100644 --- a/packages/schema/src/plugins/zod/types.ts +++ b/packages/schema/src/plugins/zod/types.ts @@ -1,4 +1,4 @@ -import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk'; +import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import { Project } from 'ts-morph'; export type TransformerParams = { diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index 45983886d..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'; 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 5e629bc2a..c91034d1d 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -1,6 +1,6 @@ /// -import { getDMMF } from '@zenstackhq/sdk'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6a7d24fce..60431d524 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -31,5 +31,23 @@ }, "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/dmmf-helpers/aggregate-helpers.ts b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts index b53f34260..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 { upperCaseFirst } from 'upper-case-first'; -import type { DMMF } from '../types'; +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 0699bca9a..c09c72426 100644 --- a/packages/sdk/src/dmmf-helpers/include-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/include-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; import { checkIsModelRelationField, checkModelHasManyModelRelation, checkModelHasModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForInclude(inputObjectTypes: DMMF.InputType[], models: DMMF.Model[]) { diff --git a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts b/packages/sdk/src/dmmf-helpers/missing-types-helper.ts index 4e83b8590..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 '../types'; +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 f249d7c20..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 '../types'; +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 444f2ce90..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 '../types'; +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 74b13de55..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 '../types'; +import type { DMMF } from '../prisma'; import { checkIsModelRelationField, checkModelHasManyModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForSelect( diff --git a/packages/sdk/src/dmmf-helpers/types.ts b/packages/sdk/src/dmmf-helpers/types.ts index 83858ba02..a1fbc6c59 100644 --- a/packages/sdk/src/dmmf-helpers/types.ts +++ b/packages/sdk/src/dmmf-helpers/types.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; export type TransformerParams = { enumTypes?: DMMF.SchemaEnum[]; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 5013267e8..3c89805d9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,7 +2,6 @@ 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'; diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index e3e540654..ef642f014 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -70,3 +70,5 @@ export function getPrismaVersion(): string | undefined { } } } + +export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 7ba72807d..a6a4b8629 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -93,5 +93,3 @@ export class PluginError extends Error { super(message); } } - -export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 9e19ab1b6..76b2d83da 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -8,7 +8,7 @@ import { type EnhancementKind, type EnhancementOptions, } from '@zenstackhq/runtime'; -import { getDMMF, type DMMF } from '@zenstackhq/sdk'; +import { getDMMF, type DMMF } from '@zenstackhq/sdk/prisma'; import { execSync } from 'child_process'; import * as fs from 'fs'; import json from 'json5'; From b0f5d3b4112a261ac2d216b2eef5d2b0dbbc6acb Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Mon, 15 Apr 2024 09:26:04 +0800 Subject: [PATCH 115/127] feat: support prisma format indentation (#1256) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/schema/src/cli/actions/format.ts | 9 +++- packages/schema/src/cli/cli-util.ts | 4 +- packages/schema/src/cli/index.ts | 1 + .../src/language-server/zmodel-formatter.ts | 43 +++++++++++++++++-- .../schema/tests/schema/formatter.test.ts | 2 +- tests/integration/tests/cli/format.test.ts | 26 +++++++++++ 6 files changed, 78 insertions(+), 7 deletions(-) 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 6c8508dc5..89d194fb9 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -266,7 +266,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))) { @@ -279,6 +279,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, diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index a3ea38238..f1a5c4cf0 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -128,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/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index 3e3d1f018..1121290ec 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -5,12 +5,31 @@ import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver'; export class ZModelFormatter extends AbstractFormatter { private formatOptions?: FormattingOptions; + private isPrismaStyle = true; 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()); @@ -52,4 +71,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/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/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'); From 8044a5496be6629209ee72261c3b75215d50a468 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Mon, 15 Apr 2024 11:25:51 +0800 Subject: [PATCH 116/127] feat: support format switch in VSCode extension (#1259) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/schema/package.json | 12 ++++- .../src/language-server/zmodel-formatter.ts | 44 +++++++++++++++---- .../src/language-server/zmodel-module.ts | 2 +- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/schema/package.json b/packages/schema/package.json index f5af5c3f3..8b78cbcff 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -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" diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index 1121290ec..93dd1b704 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -1,11 +1,28 @@ -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); @@ -55,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 { 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), From 5af5b69a3300c186676288f07a8a868396641ebb Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 15 Apr 2024 11:30:37 +0800 Subject: [PATCH 117/127] fix(zmodel): member access from `auth()` is not properly resolved when the auth model is imported (#1260) --- .../projects/t3-trpc-v10/prisma/schema.prisma | 30 +++++------ packages/schema/src/cli/cli-util.ts | 35 +++++++----- .../validator/expression-validator.ts | 17 +++++- .../validator/schema-validator.ts | 4 +- .../src/language-server/zmodel-linker.ts | 14 ++--- .../src/language-server/zmodel-scope.ts | 27 +++++----- packages/schema/src/utils/ast-utils.ts | 6 ++- .../validation/attribute-validation.test.ts | 2 +- .../tests/regression/issue-1257.test.ts | 53 +++++++++++++++++++ .../tests/regression/issue-756.test.ts | 2 +- 10 files changed, 132 insertions(+), 58 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1257.test.ts 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/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 89d194fb9..e9db60fb6 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -64,23 +64,30 @@ 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; diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 59ad7edbb..cb42e4cb1 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -10,6 +10,7 @@ import { isLiteralExpr, isMemberAccessExpr, isNullExpr, + isReferenceExpr, isThisExpr, } from '@zenstackhq/language/ast'; import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; @@ -33,9 +34,21 @@ export default class ExpressionValidator implements AstValidator { { 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, + }); + } } } diff --git a/packages/schema/src/language-server/validator/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts index 9e0512547..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'; @@ -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/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 56e2431d5..5a15f9336 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -36,9 +36,9 @@ import { isStringLiteral, } from '@zenstackhq/language/ast'; import { + getAuthModel, getContainingModel, getModelFieldsWithBases, - hasAttribute, isAuthInvocation, isFutureExpr, } from '@zenstackhq/sdk'; @@ -58,7 +58,7 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { getAllDeclarationsFromImports, getContainingDataModel } from '../utils/ast-utils'; +import { getAllDataModelsIncludingImports, getContainingDataModel } from '../utils/ast-utils'; import { mapBuiltinTypeToExpressionType } from './validator/utils'; interface DefaultReference extends Reference { @@ -287,14 +287,8 @@ export class ZModelLinker extends DefaultLinker { 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 }; } diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 7dff9c8df..e48a17621 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -10,13 +10,7 @@ import { isReferenceExpr, isThisExpr, } from '@zenstackhq/language/ast'; -import { - getAuthModel, - getDataModels, - getModelFieldsWithBases, - getRecursiveBases, - isAuthInvocation, -} from '@zenstackhq/sdk'; +import { getAuthModel, getModelFieldsWithBases, getRecursiveBases, isAuthInvocation } from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, @@ -37,7 +31,12 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { isCollectionPredicate, isFutureInvocation, resolveImportUri } from '../utils/ast-utils'; +import { + getAllDataModelsIncludingImports, + isCollectionPredicate, + isFutureInvocation, + resolveImportUri, +} from '../utils/ast-utils'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; /** @@ -88,7 +87,7 @@ export class ZModelScopeComputation extends DefaultScopeComputation { } export class ZModelScopeProvider extends DefaultScopeProvider { - constructor(services: LangiumServices) { + constructor(private readonly services: LangiumServices) { super(services); } @@ -145,9 +144,9 @@ export class ZModelScopeProvider extends DefaultScopeProvider { return EMPTY_SCOPE; }) .when(isMemberAccessExpr, (operand) => { - // operand is a member access, it must be resolved to a + // operand is a member access, it must be resolved to a non-array data model type const ref = operand.member.ref; - if (isDataModelField(ref)) { + if (isDataModelField(ref) && !ref.type.array) { const targetModel = ref.type.reference?.ref; return this.createScopeForModel(targetModel, globalScope); } @@ -222,7 +221,11 @@ export class ZModelScopeProvider extends DefaultScopeProvider { private createScopeForAuthModel(node: AstNode, globalScope: Scope) { const model = getContainerOfType(node, isModel); if (model) { - const authModel = getAuthModel(getDataModels(model, true)); + const allDataModels = getAllDataModelsIncludingImports( + this.services.shared.workspace.LangiumDocuments, + model + ); + const authModel = getAuthModel(allDataModels); if (authModel) { return this.createScopeForModel(authModel, globalScope); } diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index d33f27e71..3a255228e 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -216,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); } diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index e3c1c597e..aca9e2674 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1081,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 () => { diff --git a/tests/integration/tests/regression/issue-1257.test.ts b/tests/integration/tests/regression/issue-1257.test.ts new file mode 100644 index 000000000..a692d0464 --- /dev/null +++ b/tests/integration/tests/regression/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/integration/tests/regression/issue-756.test.ts b/tests/integration/tests/regression/issue-756.test.ts index b10e60af2..9f6750ea9 100644 --- a/tests/integration/tests/regression/issue-756.test.ts +++ b/tests/integration/tests/regression/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'.`); }); }); From 5addf06c30d850dfcf54155ce936d92a711a089a Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 15 Apr 2024 11:31:08 +0800 Subject: [PATCH 118/127] chore: bump version (#1261) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 949514890..25ba4199f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 505a22fc1..62d0c5ee6 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.12" +version = "2.0.0-beta.13" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index e4302144a..45ebae557 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index e2d8a2fe7..82cd505b1 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index eb0998dc1..05154c801 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": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 6f4d68acf..1ca6f4ccf 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": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 84ef25f85..f270a6d55 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": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 21c3f3a5d..3ab710144 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": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 46bf731f1..270ee6b45 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": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 24d637306..0742eb25f 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 8b78cbcff..88a1b2ff6 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": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 60431d524..1c0999ca9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 45bc536ff..f35717c87 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 4e9382ec9..18fa3e486 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 79605951513dab4f2334debe7ecf3f7d407be6b6 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 15 Apr 2024 15:22:36 +0800 Subject: [PATCH 119/127] chore(zmodel): add vscode prerelease command (#1262) --- packages/schema/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/schema/package.json b/packages/schema/package.json index 88a1b2ff6..00c5b259f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -26,7 +26,7 @@ "linkDirectory": true }, "engines": { - "vscode": "^1.56.0" + "vscode": "^1.63.0" }, "categories": [ "Programming Languages" @@ -76,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", From c557dc93693df15cce958f02766a47ce9aca2cbf Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 15 Apr 2024 20:27:34 +0800 Subject: [PATCH 120/127] fix: enhancer generation issue with auth model with ignored fields (#1263) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- .../enhancer/enhance/auth-type-generator.ts | 24 +++++++++++++------ packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 15 files changed, 31 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 25ba4199f..2acde1c7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 62d0c5ee6..fada6b95b 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.13" +version = "2.0.0-beta.14" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 45ebae557..49e135d5c 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 82cd505b1..a81765ff6 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 05154c801..c9ae82bbf 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": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 1ca6f4ccf..6ea7a6e87 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": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index f270a6d55..35c24cab3 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": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 3ab710144..cac90ead7 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": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 270ee6b45..c3273bbd1 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": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 0742eb25f..bb8f987fe 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 00c5b259f..b135e8040 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": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts index d8e53c173..18bbd8c72 100644 --- a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts +++ b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts @@ -1,4 +1,4 @@ -import { getIdFields, isAuthInvocation, isDataModelFieldReference } from '@zenstackhq/sdk'; +import { getIdFields, hasAttribute, isAuthInvocation, isDataModelFieldReference } from '@zenstackhq/sdk'; import { DataModel, DataModelField, @@ -73,7 +73,9 @@ export function generateAuthType(model: Model, authModel: DataModel) { addAddField(exprType.name, memberDecl.name, fieldType, memberDecl.type.array); } else { // member is a scalar - addPickField(exprType.name, node.member.$refText); + if (!isIgnoredField(node.member.ref)) { + addPickField(exprType.name, node.member.$refText); + } } } } @@ -87,8 +89,10 @@ export function generateAuthType(model: Model, authModel: DataModel) { ensureType(fieldType.name); addAddField(fieldDecl.$container.name, node.target.$refText, fieldType.name, fieldDecl.type.array); } else { - // field is a scalar - addPickField(fieldDecl.$container.name, node.target.$refText); + if (!isIgnoredField(fieldDecl)) { + // field is a scalar + addPickField(fieldDecl.$container.name, node.target.$refText); + } } } }); @@ -97,8 +101,8 @@ export function generateAuthType(model: Model, authModel: DataModel) { // generate: // ` // namespace auth { - // export type User = WithRequired, 'id'> & { profile: Profile; }; - // export type Profile = WithRequired, 'age'>; + // export type User = WithRequired, 'id'> & { profile: Profile; } & Record; + // export type Profile = WithRequired, 'age'> & Record; // } // ` @@ -109,7 +113,9 @@ ${Array.from(types.entries()) let result = `Partial<_P.${model}>`; if (fields.pickFields.length > 0) { - result = `WithRequired<${result}, ${fields.pickFields.map((f) => `'${f}'`).join('|')}>`; + result = `WithRequired<${result}, ${fields.pickFields + .map((f) => `'${f}'`) + .join('|')}> & Record`; } if (fields.addFields.length > 0) { @@ -139,3 +145,7 @@ function isAuthAccess(node: AstNode): node is Expression { return false; } + +function isIgnoredField(field: DataModelField | undefined) { + return !!(field && hasAttribute(field, '@ignore')); +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 1c0999ca9..19004370d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index f35717c87..a98c2de53 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 18fa3e486..686740f63 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From b19bbab8dcad59d675db935cb759677eab14ba97 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 17 Apr 2024 11:38:33 +0800 Subject: [PATCH 121/127] fix(zod): field with `auth()` and `@default()` should be generated as optional (#1266) --- packages/plugins/trpc/src/generator.ts | 2 +- packages/schema/src/cli/index.ts | 4 +- packages/schema/src/plugins/enhancer/index.ts | 4 +- packages/schema/src/plugins/zod/generator.ts | 2 +- .../src/plugins/zod/utils/schema-gen.ts | 18 +++-- packages/sdk/src/prisma.ts | 6 +- tests/integration/tests/cli/plugins.test.ts | 38 ++++++++++- .../tests/regression/issue-1241.test.ts | 68 +++++++++---------- .../tests/regression/issue-1265.test.ts | 27 ++++++++ 9 files changed, 120 insertions(+), 49 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1265.test.ts diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 9d2cc5724..cf57a1baf 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -83,7 +83,7 @@ function createAppRouter( hiddenModels: string[], generateModelActions: string[] | undefined, generateClientHelpers: string[] | undefined, - zmodel: Model, + _zmodel: Model, zodSchemasImport: string, options: PluginOptions ) { diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index f1a5c4cf0..f430f7662 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -109,9 +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(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); diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index 79e8fd6e6..0c82acfba 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -1,4 +1,4 @@ -import { PluginError, RUNTIME_PACKAGE, createProject, resolvePath, type PluginFunction } from '@zenstackhq/sdk'; +import { PluginError, createProject, resolvePath, type PluginFunction } from '@zenstackhq/sdk'; import path from 'path'; import { getDefaultOutputFolder } from '../plugin-utils'; import { EnhancerGenerator } from './enhance'; @@ -31,7 +31,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { // resolve it relative to the schema path prismaClientPath = path.relative(path.dirname(options.schemaPath), prismaClientPathAbs); } else { - prismaClientPath = `${RUNTIME_PACKAGE}/models`; + prismaClientPath = `.zenstack/models`; } } diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 5e542540d..f2f628b30 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -305,7 +305,7 @@ export class ZodSchemaGenerator { writer.write(`const baseSchema = z.object(`); writer.inlineBlock(() => { scalarFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field, true)},`); + writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); }); }); writer.writeLine(');'); diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 07412513a..9f79a3c66 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -21,8 +21,9 @@ import { } from '@zenstackhq/sdk/ast'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '..'; +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 @@ -139,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; diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index ef642f014..b45dd7cfb 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -2,6 +2,7 @@ import type { DMMF } from '@prisma/generator-helper'; import { getDMMF as _getDMMF, type GetDMMFOptions } from '@prisma/internals'; +import { DEFAULT_RUNTIME_LOAD_PATH } from '@zenstackhq/runtime'; import path from 'path'; import { RUNTIME_PACKAGE } from './constants'; import type { PluginOptions } from './types'; @@ -14,7 +15,10 @@ export function getPrismaClientImportSpec(importingFromDir: string, options: Plu return '@prisma/client'; } - if (options.prismaClientPath.startsWith(RUNTIME_PACKAGE)) { + if ( + options.prismaClientPath.startsWith(RUNTIME_PACKAGE) || + options.prismaClientPath.startsWith(DEFAULT_RUNTIME_LOAD_PATH) + ) { return options.prismaClientPath; } diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index c0465bb0a..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': @@ -275,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/regression/issue-1241.test.ts b/tests/integration/tests/regression/issue-1241.test.ts index 3a53f567c..e5d94c9b7 100644 --- a/tests/integration/tests/regression/issue-1241.test.ts +++ b/tests/integration/tests/regression/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/integration/tests/regression/issue-1265.test.ts b/tests/integration/tests/regression/issue-1265.test.ts new file mode 100644 index 000000000..cd7df4636 --- /dev/null +++ b/tests/integration/tests/regression/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(); + }); +}); From 334b8d36518060ac33020be4d0e13530bff61e01 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 19 Apr 2024 15:39:33 +0800 Subject: [PATCH 122/127] chore: remove the unused prisma.d.ts (#1269) --- packages/runtime/src/prisma.d.ts | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 packages/runtime/src/prisma.d.ts diff --git a/packages/runtime/src/prisma.d.ts b/packages/runtime/src/prisma.d.ts deleted file mode 100644 index c01cbe743..000000000 --- a/packages/runtime/src/prisma.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-expect-error stub for re-exporting PrismaClient -export type * from '.zenstack/prisma'; From 12d8b2a5f35001112a5c7a718c078e6ae750cc28 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 19 Apr 2024 23:50:34 +0800 Subject: [PATCH 123/127] chore(runtime): create a separate export for edge (#1272) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 19 ++++++++++++++++--- packages/runtime/src/edge.ts | 1 + packages/schema/package.json | 2 +- .../src/plugins/enhancer/enhance/index.ts | 3 ++- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 16 files changed, 32 insertions(+), 17 deletions(-) create mode 120000 packages/runtime/src/edge.ts diff --git a/package.json b/package.json index 2acde1c7f..61a941bbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index fada6b95b..30f82e181 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.0.0-beta.14" +version = "2.0.0-beta.15" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 49e135d5c..b6332b227 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index a81765ff6..22f84ded2 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index c9ae82bbf..1656027da 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": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 6ea7a6e87..4f43aa173 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": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 35c24cab3..6eab0541b 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": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index cac90ead7..b0406a9d3 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": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index c3273bbd1..f2abda4bd 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": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index bb8f987fe..fcc7423b6 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "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": { @@ -53,6 +63,9 @@ }, "./models": { "types": "./models.d.ts" + }, + "./package.json": { + "default": "./package.json" } }, "publishConfig": { 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/schema/package.json b/packages/schema/package.json index b135e8040..0de5ea6a2 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": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index cf75c3315..680bf11dd 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -86,7 +86,8 @@ export class EnhancerGenerator { const enhanceTs = this.project.createSourceFile( path.join(this.outDir, 'enhance.ts'), - `import { createEnhancement, type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; + `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;'} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 19004370d..3ce3b5871 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index a98c2de53..c9936bf1b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 686740f63..9866ffa7c 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 8310000cf10b27f84867cc70e02f27c1ac0a74fd Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 24 Apr 2024 08:57:40 +0800 Subject: [PATCH 124/127] chore: split test runs in CI (#1277) --- .github/workflows/integration-test.yml | 104 ++++++++++++++++ .github/workflows/regression-test.yml | 104 ++++++++++++++++ package.json | 4 +- .../testtools/src}/jest-ext.ts | 1 + pnpm-lock.yaml | 114 ++++++++---------- tests/integration/test-setup.ts | 2 +- 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}/issue-1167.test.ts | 0 .../tests}/issue-1179.test.ts | 0 .../tests}/issue-1186.test.ts | 0 .../tests}/issue-1210.test.ts | 0 .../tests}/issue-1235.test.ts | 0 .../tests}/issue-1241.test.ts | 0 .../tests}/issue-1257.test.ts | 0 .../tests}/issue-1265.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 | 0 .../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 | 0 .../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 | 0 .../tests}/issue-925.test.ts | 0 .../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 | 0 tests/regression/tsconfig.json | 12 ++ 54 files changed, 355 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/integration-test.yml create mode 100644 .github/workflows/regression-test.yml rename {tests/integration/utils => packages/testtools/src}/jest-ext.ts (98%) 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%) rename tests/{integration/tests/regression => regression/tests}/issue-1167.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1179.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1186.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1210.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1235.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1241.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1257.test.ts (100%) rename tests/{integration/tests/regression => regression/tests}/issue-1265.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 (100%) 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 (100%) 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 (100%) rename tests/{integration/tests/regression => regression/tests}/issue-925.test.ts (100%) 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 (100%) create mode 100644 tests/regression/tsconfig.json 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/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/package.json b/package.json index 61a941bbf..d28d5eb68 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "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/", 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/pnpm-lock.yaml b/pnpm-lock.yaml index 8f14f0dee..a86c803b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -853,6 +853,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: @@ -2742,7 +2767,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 @@ -2763,14 +2788,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 @@ -2798,7 +2823,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 @@ -2825,7 +2850,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 @@ -2858,7 +2883,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 @@ -2946,7 +2971,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 @@ -4706,7 +4731,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: @@ -4724,7 +4749,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 @@ -4759,7 +4784,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: @@ -4794,7 +4819,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 @@ -4810,7 +4835,7 @@ packages: /@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: @@ -4920,7 +4945,7 @@ packages: 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: @@ -4928,7 +4953,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: @@ -4943,7 +4968,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: @@ -9610,7 +9635,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 @@ -9659,47 +9684,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.4.4) - 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} @@ -9799,7 +9783,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 @@ -9824,7 +9808,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 @@ -9875,7 +9859,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 @@ -9930,7 +9914,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 @@ -9961,7 +9945,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 @@ -10013,7 +9997,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 @@ -10038,7 +10022,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 @@ -10050,7 +10034,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 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/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/integration/tests/regression/issue-1167.test.ts b/tests/regression/tests/issue-1167.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1167.test.ts rename to tests/regression/tests/issue-1167.test.ts diff --git a/tests/integration/tests/regression/issue-1179.test.ts b/tests/regression/tests/issue-1179.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1179.test.ts rename to tests/regression/tests/issue-1179.test.ts diff --git a/tests/integration/tests/regression/issue-1186.test.ts b/tests/regression/tests/issue-1186.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1186.test.ts rename to tests/regression/tests/issue-1186.test.ts diff --git a/tests/integration/tests/regression/issue-1210.test.ts b/tests/regression/tests/issue-1210.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1210.test.ts rename to tests/regression/tests/issue-1210.test.ts 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 100% rename from tests/integration/tests/regression/issue-1241.test.ts rename to tests/regression/tests/issue-1241.test.ts diff --git a/tests/integration/tests/regression/issue-1257.test.ts b/tests/regression/tests/issue-1257.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1257.test.ts rename to tests/regression/tests/issue-1257.test.ts diff --git a/tests/integration/tests/regression/issue-1265.test.ts b/tests/regression/tests/issue-1265.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-1265.test.ts rename to tests/regression/tests/issue-1265.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 100% rename from tests/integration/tests/regression/issue-665.test.ts rename to tests/regression/tests/issue-665.test.ts 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 100% rename from tests/integration/tests/regression/issue-756.test.ts rename to tests/regression/tests/issue-756.test.ts 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 100% rename from tests/integration/tests/regression/issue-886.test.ts rename to tests/regression/tests/issue-886.test.ts diff --git a/tests/integration/tests/regression/issue-925.test.ts b/tests/regression/tests/issue-925.test.ts similarity index 100% rename from tests/integration/tests/regression/issue-925.test.ts rename to tests/regression/tests/issue-925.test.ts 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 100% rename from tests/integration/tests/regression/issues.test.ts rename to tests/regression/tests/issues.test.ts 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 7c4d86c1e8eb61388c9a6426b415a5a4bdcbeccc Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 24 Apr 2024 09:10:42 +0800 Subject: [PATCH 125/127] chore: merge from dev (#1276) --- packages/plugins/swr/tests/test-model-meta.ts | 4 +- .../tanstack-query/tests/test-model-meta.ts | 4 +- .../routers/generated/routers/Post.router.ts | 23 +++ .../routers/generated/routers/User.router.ts | 23 +++ packages/runtime/src/cross/utils.ts | 20 +- .../src/enhancements/policy/handler.ts | 129 ++++++++---- .../runtime/src/enhancements/query-utils.ts | 53 ++++- packages/runtime/src/enhancements/utils.ts | 8 +- packages/sdk/src/model-meta-generator.ts | 45 +++- .../with-policy/multi-id-fields.test.ts | 146 ++++++++++++- .../with-policy/nested-to-many.test.ts | 95 +++++++++ .../with-policy/nested-to-one.test.ts | 58 ++++++ .../with-policy/toplevel-operations.test.ts | 76 +++++++ tests/regression/tests/issue-1271.test.ts | 192 ++++++++++++++++++ 14 files changed, 808 insertions(+), 68 deletions(-) create mode 100644 tests/regression/tests/issue-1271.test.ts diff --git a/packages/plugins/swr/tests/test-model-meta.ts b/packages/plugins/swr/tests/test-model-meta.ts index 71a657bad..001d773a9 100644 --- a/packages/plugins/swr/tests/test-model-meta.ts +++ b/packages/plugins/swr/tests/test-model-meta.ts @@ -32,7 +32,7 @@ export const modelMeta: ModelMeta = { name: 'posts', }, }, - uniqueConstraints: {}, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, post: { name: 'post', @@ -48,7 +48,7 @@ export const modelMeta: ModelMeta = { owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, }, - uniqueConstraints: {}, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, }, deleteCascade: { diff --git a/packages/plugins/tanstack-query/tests/test-model-meta.ts b/packages/plugins/tanstack-query/tests/test-model-meta.ts index 71a657bad..001d773a9 100644 --- a/packages/plugins/tanstack-query/tests/test-model-meta.ts +++ b/packages/plugins/tanstack-query/tests/test-model-meta.ts @@ -32,7 +32,7 @@ export const modelMeta: ModelMeta = { name: 'posts', }, }, - uniqueConstraints: {}, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, post: { name: 'post', @@ -48,7 +48,7 @@ export const modelMeta: ModelMeta = { owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, }, - uniqueConstraints: {}, + uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, }, }, deleteCascade: { 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 fbc73cf06..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 @@ -61,6 +61,29 @@ export interface ClientType; }; + createMany: { + useMutation: ( + opts?: UseTRPCMutationOptions< + Prisma.PostCreateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >, + ) => Omit< + UseTRPCMutationResult< + Prisma.BatchPayload, + TRPCClientErrorLike, + Prisma.SelectSubset, + Context + >, + 'mutateAsync' + > & { + mutateAsync: ( + variables: T, + opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, + ) => Promise; + }; + }; create: { useMutation: (opts?: UseTRPCMutationOptions< 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 c4bdb89de..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 @@ -61,6 +61,29 @@ export interface ClientType; }; + createMany: { + useMutation: ( + opts?: UseTRPCMutationOptions< + Prisma.UserCreateManyArgs, + TRPCClientErrorLike, + Prisma.BatchPayload, + Context + >, + ) => Omit< + UseTRPCMutationResult< + Prisma.BatchPayload, + TRPCClientErrorLike, + Prisma.SelectSubset, + Context + >, + 'mutateAsync' + > & { + mutateAsync: ( + variables: T, + opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>, + ) => Promise; + }; + }; create: { useMutation: (opts?: UseTRPCMutationOptions< diff --git a/packages/runtime/src/cross/utils.ts b/packages/runtime/src/cross/utils.ts index 1982513b3..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 { ModelInfo, ModelMeta } from '.'; +import { requireField, type ModelInfo, type ModelMeta } from '.'; /** * Gets field names in a data model entity, filtering out internal fields. @@ -47,19 +47,17 @@ export function zip(x: Enumerable, y: Enumerable): Array<[T1, T2 } export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) { - let fields = modelMeta.models[lowerCaseFirst(model)]?.fields; - if (!fields) { + const uniqueConstraints = modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {}; + + const entries = Object.values(uniqueConstraints); + if (entries.length === 0) { if (throwIfNotFound) { - throw new Error(`Unable to load fields for ${model}`); - } else { - fields = {}; + throw new Error(`Model ${model} does not have any id field`); } + return []; } - const result = Object.values(fields).filter((f) => f.isId); - if (result.length === 0 && throwIfNotFound) { - throw new Error(`model ${model} does not have an id field`); - } - return result; + + return entries[0].fields.map((f) => requireField(modelMeta, model, f)); } export function getModelInfo( diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index ea27fc1db..d6d893d4e 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -22,7 +22,7 @@ import { Logger } from '../logger'; import { createDeferredPromise, createFluentPromise } from '../promise'; import { PrismaProxyHandler } from '../proxy'; import { QueryUtils } from '../query-utils'; -import { formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils'; +import { clone, formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils'; import { PolicyUtil } from './policy-utils'; // a record for post-write policy check @@ -117,7 +117,7 @@ export class PolicyProxyHandler implements Pr // make a find query promise with fluent API call stubs installed private findWithFluent(method: FindOperations, args: any, handleRejection: () => any) { - args = this.policyUtils.clone(args); + args = clone(args); return createFluentPromise( () => this.doFind(args, method, handleRejection), args, @@ -128,7 +128,7 @@ export class PolicyProxyHandler implements Pr private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) { const origArgs = args; - const _args = this.policyUtils.clone(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`); @@ -167,7 +167,7 @@ export class PolicyProxyHandler implements Pr this.policyUtils.tryReject(this.prisma, this.model, 'create'); const origArgs = args; - args = this.policyUtils.clone(args); + args = clone(args); // static input policy check for top-level create data const inputCheck = this.policyUtils.checkInputGuard(this.model, args.data, 'create'); @@ -364,7 +364,7 @@ export class PolicyProxyHandler implements Pr }); // return only the ids of the top-level entity - const ids = this.policyUtils.getEntityIds(this.model, result); + const ids = this.policyUtils.getEntityIds(model, result); return { result: ids, postWriteChecks: [...postCreateChecks.values()] }; } @@ -434,7 +434,7 @@ export class PolicyProxyHandler implements Pr return createDeferredPromise(async () => { this.policyUtils.tryReject(this.prisma, this.model, 'create'); - args = this.policyUtils.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 @@ -615,7 +615,7 @@ export class PolicyProxyHandler implements Pr } return createDeferredPromise(async () => { - args = this.policyUtils.clone(args); + args = clone(args); const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { // proceed with nested writes and collect post-write checks @@ -743,8 +743,10 @@ export class PolicyProxyHandler implements Pr } // proceed with the create and collect post-create checks - const { postWriteChecks: checks } = await this.doCreate(model, { data: createData }, db); + const { postWriteChecks: checks, result } = await this.doCreate(model, { data: createData }, db); postWriteChecks.push(...checks); + + return result; }; const _createMany = async ( @@ -832,18 +834,10 @@ export class PolicyProxyHandler implements Pr // check pre-update guard await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); - // handles the case where id fields are updated - const postUpdateIds = this.policyUtils.clone(existing); - for (const key of Object.keys(existing)) { - const updateValue = (args as any).data ? (args as any).data[key] : (args as any)[key]; - if ( - typeof updateValue === 'string' || - typeof updateValue === 'number' || - typeof updateValue === 'bigint' - ) { - postUpdateIds[key] = updateValue; - } - } + // handle the case where id fields are updated + const _args: any = args; + const updatePayload = _args.data && typeof _args.data === 'object' ? _args.data : _args; + const postUpdateIds = this.calculatePostUpdateIds(model, existing, updatePayload); // register post-update check await _registerPostUpdateCheck(model, existing, postUpdateIds); @@ -935,10 +929,13 @@ export class PolicyProxyHandler implements Pr // update case // check pre-update guard - await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, '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); // register post-update check - await _registerPostUpdateCheck(model, uniqueFilter, uniqueFilter); + await _registerPostUpdateCheck(model, existing, postUpdateIds); // convert upsert to update const convertedUpdate = { @@ -972,9 +969,22 @@ export class PolicyProxyHandler implements Pr if (existing) { // connect await _connectDisconnect(model, args.where, context); + return true; } else { // create - await _create(model, args.create, context); + const created = await _create(model, args.create, context); + + const upperContext = context.nestingPath[context.nestingPath.length - 2]; + if (upperContext?.where && context.field) { + // check if the where clause of the upper context references the id + // of the connected entity, if so, we need to update it + this.overrideForeignKeyFields(upperContext.model, upperContext.where, context.field, created); + } + + // remove the payload from the parent + this.removeFromParent(context.parent, 'connectOrCreate', args); + + return false; } }, @@ -1044,6 +1054,52 @@ export class PolicyProxyHandler implements Pr return { result, postWriteChecks }; } + // calculate id fields used for post-update check given an update payload + private calculatePostUpdateIds(_model: string, currentIds: any, updatePayload: any) { + 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') { + result[key] = updateValue; + } + } + return result; + } + + // updates foreign key fields inside `payload` based on relation id fields in `newIds` + private overrideForeignKeyFields( + model: string, + payload: any, + relation: FieldInfo, + newIds: Record + ) { + if (!relation.foreignKeyMapping || Object.keys(relation.foreignKeyMapping).length === 0) { + return; + } + + // override foreign key values + for (const [id, fk] of Object.entries(relation.foreignKeyMapping)) { + if (payload[fk] !== undefined && newIds[id] !== undefined) { + payload[fk] = newIds[id]; + } + } + + // deal with compound id fields + const uniqueConstraints = this.policyUtils.getUniqueConstraints(model); + for (const [name, constraint] of Object.entries(uniqueConstraints)) { + if (constraint.fields.length > 1) { + const target = payload[name]; + if (target) { + for (const [id, fk] of Object.entries(relation.foreignKeyMapping)) { + if (target[fk] !== undefined && newIds[id] !== undefined) { + target[fk] = newIds[id]; + } + } + } + } + } + } + // Validates the given update payload against Zod schema if any private validateUpdateInputSchema(model: string, data: any) { const schema = this.policyUtils.getZodSchema(model, 'update'); @@ -1085,16 +1141,12 @@ export class PolicyProxyHandler implements Pr this.prismaModule, 'data field is required in query argument' ); - throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); } return createDeferredPromise(() => { this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.policyUtils.clone(args); + args = clone(args); this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); args.data = this.validateUpdateInputSchema(this.model, args.data); @@ -1174,18 +1226,25 @@ export class PolicyProxyHandler implements Pr this.policyUtils.tryReject(this.prisma, this.model, 'create'); this.policyUtils.tryReject(this.prisma, this.model, 'update'); - args = this.policyUtils.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. 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, args.where); + const existing = await this.policyUtils.checkExistence(tx, this.model, where); if (existing) { // update case - const { result, postWriteChecks } = await this.doUpdate({ where, data: update, ...rest }, tx); + 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 { @@ -1281,7 +1340,7 @@ export class PolicyProxyHandler implements Pr } return createDeferredPromise(() => { - args = this.policyUtils.clone(args); + args = clone(args); // inject policy conditions this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); @@ -1299,7 +1358,7 @@ export class PolicyProxyHandler implements Pr } return createDeferredPromise(() => { - args = this.policyUtils.clone(args); + args = clone(args); // inject policy conditions this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); @@ -1314,7 +1373,7 @@ export class PolicyProxyHandler implements Pr count(args: any) { return createDeferredPromise(() => { // inject policy conditions - args = args ? this.policyUtils.clone(args) : {}; + args = args ? clone(args) : {}; this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); if (this.shouldLogQuery) { @@ -1350,7 +1409,7 @@ export class PolicyProxyHandler implements Pr // include all args = { create: {}, update: {}, delete: {} }; } else { - args = this.policyUtils.clone(args); + args = clone(args); } } diff --git a/packages/runtime/src/enhancements/query-utils.ts b/packages/runtime/src/enhancements/query-utils.ts index c161d5e2c..81c8d1da9 100644 --- a/packages/runtime/src/enhancements/query-utils.ts +++ b/packages/runtime/src/enhancements/query-utils.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - FieldInfo, - NestedWriteVisitorContext, getIdFields, getModelInfo, getUniqueConstraints, resolveField, + type FieldInfo, + type NestedWriteVisitorContext, } from '../cross'; -import { CrudContract, DbClientContract } from '../types'; +import type { CrudContract, DbClientContract } from '../types'; import { getVersion } from '../version'; import { InternalEnhancementOptions } from './create-enhancement'; -import { prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; +import { clone, prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; export class QueryUtils { constructor(private readonly prisma: DbClientContract, protected readonly options: InternalEnhancementOptions) {} @@ -56,7 +56,10 @@ export class QueryUtils { } } - buildReversedQuery(context: NestedWriteVisitorContext, mutating = false, unsafeOperation = false) { + /** + * Builds a reversed query for the given nested path. + */ + buildReversedQuery(context: NestedWriteVisitorContext, forMutationPayload = false, unsafeOperation = false) { let result, currQuery: any; let currField: FieldInfo | undefined; @@ -87,7 +90,7 @@ export class QueryUtils { throw this.unknownError(`missing backLink field ${currField.backLink} in ${currField.type}`); } - if (backLinkField.isArray && !mutating) { + if (backLinkField.isArray && !forMutationPayload) { // many-side of relationship, wrap with "some" query currQuery[currField.backLink] = { some: { ...visitWhere } }; currQuery = currQuery[currField.backLink].some; @@ -97,7 +100,7 @@ export class QueryUtils { // calculate if we should preserve the relation condition (e.g., { user: { id: 1 } }) const shouldPreserveRelationCondition = // doing a mutation - mutating && + forMutationPayload && // and it's a safe mutate !unsafeOperation && // and the current segment is the direct parent (the last one is the mutate itself), @@ -119,6 +122,15 @@ export class QueryUtils { // 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; @@ -127,8 +139,33 @@ export class QueryUtils { 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) { - // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } const uniqueConstraints = getUniqueConstraints(this.options.modelMeta, model); if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) { for (const [field, value] of Object.entries(args)) { diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 92c8b7726..5cd23610e 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -1,5 +1,6 @@ +import deepcopy from 'deepcopy'; import safeJsonStringify from 'safe-json-stringify'; -import { FieldInfo, ModelMeta, resolveField } from '..'; +import { resolveField, type FieldInfo, type ModelMeta } from '..'; import type { DbClientContract } from '../types'; /** @@ -42,3 +43,8 @@ export function isUnsafeMutate(model: string, args: any, modelMeta: ModelMeta) { export function isAutoIncrementIdField(field: FieldInfo) { return field.isId && field.isAutoIncrement; } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function clone(value: unknown): any { + return value ? deepcopy(value) : {}; +} diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 3dc0f3f1e..3072ab202 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -1,6 +1,7 @@ import { ArrayExpr, DataModel, + DataModelAttribute, DataModelField, isArrayExpr, isBooleanLiteral, @@ -344,10 +345,7 @@ function getAttributes(target: DataModelField | DataModel): RuntimeAttribute[] { function getUniqueConstraints(model: DataModel) { const constraints: Array<{ name: string; fields: string[] }> = []; - // model-level constraints - for (const attr of model.attributes.filter( - (attr) => attr.decl.ref?.name === '@@unique' || attr.decl.ref?.name === '@@id' - )) { + const extractConstraint = (attr: DataModelAttribute) => { const argsMap = getAttributeArgs(attr); if (argsMap.fields) { const fieldNames = (argsMap.fields as ArrayExpr).items.map( @@ -358,14 +356,45 @@ function getUniqueConstraints(model: DataModel) { // default constraint name is fields concatenated with underscores constraintName = fieldNames.join('_'); } - constraints.push({ name: constraintName, fields: fieldNames }); + return { name: constraintName, fields: fieldNames }; + } else { + return undefined; + } + }; + + const addConstraint = (constraint: { name: string; fields: string[] }) => { + if (!constraints.some((c) => c.name === constraint.name)) { + constraints.push(constraint); + } + }; + + // field-level @id first + for (const field of model.fields) { + if (hasAttribute(field, '@id')) { + addConstraint({ name: field.name, fields: [field.name] }); } } - // field-level constraints + // then model-level @@id + for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@id')) { + const constraint = extractConstraint(attr); + if (constraint) { + addConstraint(constraint); + } + } + + // then field-level @unique for (const field of model.fields) { - if (hasAttribute(field, '@id') || hasAttribute(field, '@unique')) { - constraints.push({ name: field.name, fields: [field.name] }); + if (hasAttribute(field, '@unique')) { + addConstraint({ name: field.name, fields: [field.name] }); + } + } + + // then model-level @@unique + for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@unique')) { + const constraint = extractConstraint(attr); + if (constraint) { + addConstraint(constraint); } } diff --git a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts index 227dc5a27..0abb45559 100644 --- a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts +++ b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts @@ -12,7 +12,7 @@ describe('With Policy: multiple id fields', () => { process.chdir(origDir); }); - it('multi-id fields', async () => { + it('multi-id fields crud', async () => { const { prisma, enhance } = await loadSchema( ` model A { @@ -69,6 +69,75 @@ describe('With Policy: multiple id fields', () => { ).toResolveTruthy(); }); + it('multi-id fields id update', async () => { + const { prisma, enhance } = await loadSchema( + ` + model A { + x String + y Int + value Int + b B? + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 0 && future().value > 1) + } + + model B { + b1 String + b2 String + value Int + a A @relation(fields: [ax, ay], references: [x, y]) + ax String + ay Int + + @@allow('read', value > 2) + @@allow('create', value > 1) + + @@unique([ax, ay]) + @@id([b1, b2]) + } + ` + ); + + const db = enhance(); + + await db.a.create({ data: { x: '1', y: 2, value: 1 } }); + + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 0 } }) + ).toBeRejectedByPolicy(); + + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }) + ).resolves.toMatchObject({ + x: '2', + y: 3, + value: 2, + }); + + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 0 }, + create: { x: '4', y: 5, value: 5 }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 3 }, + create: { x: '4', y: 5, value: 5 }, + }) + ).resolves.toMatchObject({ + x: '3', + y: 4, + value: 3, + }); + }); + it('multi-id auth', async () => { const { prisma, enhance } = await loadSchema( ` @@ -270,4 +339,79 @@ describe('With Policy: multiple id fields', () => { expect(await db.b.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 5 })); expect(await db.c.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 6 })); }); + + it('multi-id fields nested id update', async () => { + const { enhance } = await loadSchema( + ` + model A { + x String + y Int + value Int + b B @relation(fields: [bId], references: [id]) + bId Int + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 0 && future().value > 1) + } + + model B { + id Int @id @default(autoincrement()) + a A[] + @@allow('all', true) + } + ` + ); + + const db = enhance(); + + await db.b.create({ data: { id: 1, a: { create: { x: '1', y: 1, value: 1 } } } }); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 0 } } } }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, + include: { a: true }, + }) + ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '2', y: 2, value: 2 })]) }); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 0 }, + create: { x: '4', y: '4', value: 4 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 3 }, + create: { x: '4', y: '4', value: 4 }, + }, + }, + }, + include: { a: true }, + }) + ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '3', y: 3, value: 3 })]) }); + }); }); 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 777af1118..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 @@ -284,6 +284,101 @@ describe('With Policy:nested to-many', () => { expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); }); + it('update id field', async () => { + const { enhance } = await loadSchema( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', true) + @@allow('update', value > 1 && future().value > 2) + } + ` + ); + + const db = enhance(); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 2 }, + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { id: '2', value: 1 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + let r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { id: '2', value: 3 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); + + await expect( + db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + upsert: { + where: { id: '2' }, + create: { id: '4', value: 4 }, + update: { id: '3', value: 1 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + upsert: { + where: { id: '2' }, + create: { id: '4', value: 4 }, + update: { id: '3', value: 4 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '3', value: 4 })])); + }); + it('update with create from one to many', async () => { const { enhance } = await loadSchema( ` 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 4b30c095f..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 @@ -212,6 +212,64 @@ describe('With Policy:nested to-one', () => { ).toBeRejectedByPolicy(); }); + it('nested update id tests', async () => { + const { enhance } = await loadSchema( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1 && future().value > 2) + } + ` + ); + + const db = enhance(); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 2 }, + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 1 }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 3 }, + }, + }, + include: { m2: true }, + }) + ).resolves.toMatchObject({ m2: expect.objectContaining({ id: '2', value: 3 }) }); + }); + it('nested create', async () => { const { enhance } = await loadSchema( ` 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 61f25dc25..3543dd7b5 100644 --- a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts +++ b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts @@ -147,6 +147,82 @@ describe('With Policy: toplevel operations', () => { ).toBeTruthy(); }); + it('update id tests', async () => { + const { enhance } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('read', value > 1) + @@allow('create', value > 0) + @@allow('update', value > 1 && future().value > 2) + } + ` + ); + + const db = enhance(); + + await db.model.create({ + data: { + id: '1', + value: 2, + }, + }); + + // update denied + await expect( + db.model.update({ + where: { id: '1' }, + data: { + id: '2', + value: 1, + }, + }) + ).toBeRejectedByPolicy(); + + // update success + await expect( + db.model.update({ + where: { id: '1' }, + data: { + id: '2', + value: 3, + }, + }) + ).resolves.toMatchObject({ id: '2', value: 3 }); + + // upsert denied + await expect( + db.model.upsert({ + where: { id: '2' }, + update: { + id: '3', + value: 1, + }, + create: { + id: '4', + value: 5, + }, + }) + ).toBeRejectedByPolicy(); + + // upsert success + await expect( + db.model.upsert({ + where: { id: '2' }, + update: { + id: '3', + value: 4, + }, + create: { + id: '4', + value: 5, + }, + }) + ).resolves.toMatchObject({ id: '3', value: 4 }); + }); + it('delete tests', async () => { const { enhance, prisma } = await loadSchema( ` diff --git a/tests/regression/tests/issue-1271.test.ts b/tests/regression/tests/issue-1271.test.ts new file mode 100644 index 000000000..d25cabb3b --- /dev/null +++ b/tests/regression/tests/issue-1271.test.ts @@ -0,0 +1,192 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1271', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model User { + id String @id @default(uuid()) + + @@auth + @@allow('all', true) + } + + model Test { + id String @id @default(uuid()) + linkingTable LinkingTable[] + key String @default('test') + locale String @default('EN') + + @@unique([key, locale]) + @@allow("all", true) + } + + model LinkingTable { + test_id String + test Test @relation(fields: [test_id], references: [id]) + + another_test_id String + another_test AnotherTest @relation(fields: [another_test_id], references: [id]) + + @@id([test_id, another_test_id]) + @@allow("all", true) + } + + model AnotherTest { + id String @id @default(uuid()) + status String + linkingTable LinkingTable[] + + @@allow("all", true) + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const test = await db.test.create({ + data: { + key: 'test1', + }, + }); + const anotherTest = await db.anotherTest.create({ + data: { + status: 'available', + }, + }); + + const updated = await db.test.upsert({ + where: { + key_locale: { + key: test.key, + locale: test.locale, + }, + }, + create: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + include: { + linkingTable: true, + }, + }); + + expect(updated.linkingTable).toHaveLength(1); + expect(updated.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const test2 = await db.test.upsert({ + where: { + key_locale: { + key: 'test2', + locale: 'locale2', + }, + }, + create: { + key: 'test2', + locale: 'locale2', + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + update: { + linkingTable: { + create: { + another_test_id: anotherTest.id, + }, + }, + }, + include: { + linkingTable: true, + }, + }); + expect(test2).toMatchObject({ key: 'test2', locale: 'locale2' }); + expect(test2.linkingTable).toHaveLength(1); + expect(test2.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); + + const linkingTable = test2.linkingTable[0]; + + // connectOrCreate: connect case + const test3 = await db.test.create({ + data: { + key: 'test3', + locale: 'locale3', + }, + }); + console.log('test3 created:', test3); + const updated2 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: linkingTable.test_id, + another_test_id: linkingTable.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: test3.key, + locale: test3.locale, + }, + }, + create: { + key: 'test4', + locale: 'locale4', + }, + }, + }, + another_test: { connect: { id: anotherTest.id } }, + }, + include: { test: true }, + }); + expect(updated2).toMatchObject({ + test: expect.objectContaining({ key: 'test3', locale: 'locale3' }), + another_test_id: anotherTest.id, + }); + + // connectOrCreate: create case + const updated3 = await db.linkingTable.update({ + where: { + test_id_another_test_id: { + test_id: updated2.test_id, + another_test_id: updated2.another_test_id, + }, + }, + data: { + test: { + connectOrCreate: { + where: { + key_locale: { + key: 'test4', + locale: 'locale4', + }, + }, + create: { + key: 'test4', + locale: 'locale4', + }, + }, + }, + another_test: { connect: { id: anotherTest.id } }, + }, + include: { test: true }, + }); + expect(updated3).toMatchObject({ + test: expect.objectContaining({ key: 'test4', locale: 'locale4' }), + another_test_id: anotherTest.id, + }); + }); +}); From 386580e94c2908267bc45cdd265824efb1b8c86a Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 24 Apr 2024 20:17:25 +0800 Subject: [PATCH 126/127] fix(zod): make typing of `Bytes` field compatible with both `Buffer` and `Uint8Array` (#1279) --- packages/schema/package.json | 13 ++++---- .../schema/src/plugins/zod/transformer.ts | 10 +++++- .../src/plugins/zod/utils/schema-gen.ts | 2 +- packages/testtools/src/schema.ts | 6 ++-- pnpm-lock.yaml | 32 ++++++++++++------- script/test-scaffold.ts | 2 +- tests/regression/tests/issue-1268.test.ts | 32 +++++++++++++++++++ 7 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 tests/regression/tests/issue-1268.test.ts diff --git a/packages/schema/package.json b/packages/schema/package.json index 0de5ea6a2..41f993a68 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -59,13 +59,13 @@ "configuration": { "title": "ZenStack", "properties": { - "zmodel.format.usePrismaStyle": { - "type": "boolean", - "default": true, - "description": "Use Prisma style indentation." - } + "zmodel.format.usePrismaStyle": { + "type": "boolean", + "default": true, + "description": "Use Prisma style indentation." + } } - } + } }, "activationEvents": [ "onLanguage:zmodel" @@ -90,6 +90,7 @@ }, "dependencies": { "@paralleldrive/cuid2": "^2.2.0", + "@types/node": "^20.12.7", "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", "async-exit-hook": "^2.0.1", diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index f4c8a167d..86829f1ca 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -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)); diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 9f79a3c66..e6a335221 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -186,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/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 76b2d83da..7249e6c4a 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -101,12 +101,12 @@ generator js { plugin enhancer { provider = '@core/enhancer' - preserveTsFiles = true + ${options.preserveTsFiles ? 'preserveTsFiles = true' : ''} } plugin zod { provider = '@core/zod' - // preserveTsFiles = true + ${options.preserveTsFiles ? 'preserveTsFiles = true' : ''} modelOnly = ${!options.fullZod} } `; @@ -130,6 +130,7 @@ export type SchemaLoadOptions = { enhanceOptions?: Partial; extraSourceFiles?: { name: string; content: string }[]; projectDir?: string; + preserveTsFiles?: boolean; }; const defaultOptions: SchemaLoadOptions = { @@ -140,6 +141,7 @@ const defaultOptions: SchemaLoadOptions = { compile: false, logPrismaQuery: false, provider: 'sqlite', + preserveTsFiles: false, }; export async function loadSchemaFromFile(schemaFile: string, options?: SchemaLoadOptions) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a86c803b7..8af4e228d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -465,6 +465,9 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.0 version: 2.2.0 + '@types/node': + specifier: ^20.12.7 + version: 20.12.7 '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -4715,7 +4718,7 @@ packages: 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: @@ -4768,7 +4771,7 @@ 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: @@ -4860,6 +4863,7 @@ packages: /@types/node@18.0.0: resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==} + dev: false /@types/node@20.10.2: resolution: {integrity: sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw==} @@ -4867,6 +4871,11 @@ 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 @@ -4874,7 +4883,7 @@ packages: /@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 @@ -9766,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 @@ -14418,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==} @@ -14729,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 @@ -14739,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 @@ -14825,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 @@ -14850,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 @@ -14935,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 @@ -14954,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 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/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(); + }); +}); From f6dc32a87a688a27d4eae1543890273b306479d5 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 24 Apr 2024 20:39:01 +0800 Subject: [PATCH 127/127] chore: bump version (#1280) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index d28d5eb68..56cc21263 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.0.0-beta.15", + "version": "2.0.0", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 30f82e181..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 = "2.0.0-beta.15" +version = "2.0.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index b6332b227..796e92da1 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.0.0-beta.15", + "version": "2.0.0", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 22f84ded2..ca23c5bce 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.0.0-beta.15", + "version": "2.0.0", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 1656027da..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": "2.0.0-beta.15", + "version": "2.0.0", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 4f43aa173..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": "2.0.0-beta.15", + "version": "2.0.0", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 6eab0541b..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": "2.0.0-beta.15", + "version": "2.0.0", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index b0406a9d3..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": "2.0.0-beta.15", + "version": "2.0.0", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index f2abda4bd..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": "2.0.0-beta.15", + "version": "2.0.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index fcc7423b6..e1b436993 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.0.0-beta.15", + "version": "2.0.0", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 41f993a68..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": "2.0.0-beta.15", + "version": "2.0.0", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3ce3b5871..82aba8ad5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.0.0-beta.15", + "version": "2.0.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index c9936bf1b..87a709075 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.0.0-beta.15", + "version": "2.0.0", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 9866ffa7c..d12aa129f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.0.0-beta.15", + "version": "2.0.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true,