From fa0dafb98f2f9c034731f380ac190e048d0c0d3f Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 11 Feb 2024 22:05:45 +0800 Subject: [PATCH 01/43] fix: incorrect validation errors for abstract models with validation rules (#991) --- .../validator/datamodel-validator.ts | 22 ++++---- .../validation/attribute-validation.test.ts | 9 ++++ .../validation/datamodel-validation.test.ts | 13 +---- .../tests/regression/issue-965.test.ts | 53 +++++++++++++++++++ 4 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 tests/integration/tests/regression/issue-965.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 ce1886f5e..3096d5257 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 } from '@zenstackhq/sdk'; +import { getLiteral, getModelIdFields, getModelUniqueFields } from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; @@ -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 valdaition 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/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 8b7886334..6d8d02b14 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -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 @@ -1118,6 +1126,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 e1f06d268..4212441fe 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,9 +622,8 @@ describe('Data Model Validation Tests', () => { b String } `); - expect(errors.length).toBe(1); - expect(errors[0]).toEqual(`Model A cannot be extended because it's not abstract`); + expect(errors).toContain(`Model A cannot be extended because it's not abstract`); // relation incomplete from multiple level inheritance expect( 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.' + ); + }); +}); From da3388190020041965ff104a346f932a8d32b59d Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 11 Feb 2024 22:30:13 +0800 Subject: [PATCH 02/43] fix: Zmodel linker doesn't recursively visit base types when building resolution scopes (#992) --- .../src/language-server/zmodel-linker.ts | 15 ++++++++---- .../tests/regression/issue-971.test.ts | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/integration/tests/regression/issue-971.test.ts diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index ef97cf4b6..69fdf67c2 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -519,12 +519,17 @@ 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]; + const superTypeProviders: ScopeProvider[] = []; + // build scope providers for super types recursively with breadth-first search + const queue = node.superTypes.map((t) => t.ref!); + while (queue.length > 0) { + const superType = queue.shift()!; + const provider = (name: string) => superType.fields.find((f) => f.name === name); + superTypeProviders.push(provider); + queue.push(...superType.superTypes.map((t) => t.ref!)); + } + extraScopes = [...superTypeProviders, ...extraScopes]; } - return this.resolveDefault(node, document, extraScopes); } diff --git a/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 { + } + ` + ); + }); +}); From a4d3f15746269257bc7fb56332766e3f598e2996 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 11 Feb 2024 23:24:07 +0800 Subject: [PATCH 03/43] fix: @omit doesn't remove fields inside to-many relation (#993) --- packages/runtime/src/enhancements/omit.ts | 15 ++++-- .../enhancements/with-omit/with-omit.test.ts | 48 +++++++++++++++++++ .../tests/regression/issue-992.test.ts | 45 +++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/integration/tests/regression/issue-992.test.ts diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index 8b2937845..2df81b40a 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -54,11 +54,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/tests/integration/tests/enhancements/with-omit/with-omit.test.ts b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts index 61d44b440..e2db0a3f8 100644 --- a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts +++ b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts @@ -105,4 +105,52 @@ describe('Omit test', () => { expect(r1.password).toBeUndefined(); expect(r1.profile.image).toBeUndefined(); }); + + it('to-many', async () => { + const { withOmit } = 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) + } + ` + ); + + const db = withOmit(); + 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/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 d5303676e44d24460c1c232a3954778175884156 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 11 Feb 2024 23:24:30 +0800 Subject: [PATCH 04/43] chore: bump version (#994) --- 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 16b18ea0a..3c08bc9c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.8.2", + "version": "1.9.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 2643f4e2a..b3074746e 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.8.2" +version = "1.9.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index ca7e7c57e..b08392ade 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.8.2", + "version": "1.9.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 ff2cfd5ec..1d06a04c7 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.8.2", + "version": "1.9.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 47cddae1b..011e0dd64 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.8.2", + "version": "1.9.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 a910ab0e8..6584ad022 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.8.2", + "version": "1.9.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 223433bec..351048b69 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.8.2", + "version": "1.9.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 3d5a6d94b..ffbfd7bc0 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.8.2", + "version": "1.9.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 6620e365e..215039ba0 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.8.2", + "version": "1.9.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 4c0125474..494cf907f 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.8.2", + "version": "1.9.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 d7726b3be..7fc652b14 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.8.2", + "version": "1.9.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8fa8cf619..b4ccab542 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.8.2", + "version": "1.9.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index f4b4b68ab..af441d4f1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.8.2", + "version": "1.9.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 107b4d659..8acf19707 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.8.2", + "version": "1.9.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 541cd973081cbbf2d9e2e571ee8f971bc859150c Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Feb 2024 19:42:18 +0800 Subject: [PATCH 05/43] fix: supports for complex usage of "@@index" in zmodel (#995) --- package.json | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 6 +- 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 +- 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 | 4 +- .../function-invocation-validator.ts | 1 + .../zmodel-completion-provider.ts | 10 +- .../src/language-server/zmodel-module.ts | 7 ++ .../src/plugins/prisma/prisma-builder.ts | 6 +- .../src/plugins/prisma/schema-generator.ts | 8 +- packages/schema/src/res/stdlib.zmodel | 97 ++++++++++++++++++- .../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 | 10 +- packages/sdk/package.json | 2 +- packages/sdk/src/constants.ts | 1 + packages/sdk/src/zmodel-code-generator.ts | 8 +- packages/server/package.json | 2 +- packages/testtools/package.json | 4 +- pnpm-lock.yaml | 38 +++++--- 32 files changed, 245 insertions(+), 151 deletions(-) diff --git a/package.json b/package.json index 3c08bc9c3..2d2d2e264 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.9.1", + "version": "1.9.0", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index b08392ade..7305853c2 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.9.1", + "version": "1.9.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 1d06a04c7..a80768913 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.9.1", + "version": "1.9.0", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", @@ -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/misc/redwood/package.json b/packages/misc/redwood/package.json index 011e0dd64..e477bc7b8 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.9.1", + "version": "1.9.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 6584ad022..96a336f58 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.9.1", + "version": "1.9.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 351048b69..c64538378 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.9.1", + "version": "1.9.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 ffbfd7bc0..0c16ca59d 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.9.1", + "version": "1.9.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 215039ba0..0b53d2aa2 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.9.1", + "version": "1.9.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 494cf907f..3cb61dd38 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.9.1", + "version": "1.9.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 7fc652b14..2451a87c8 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.9.1", + "version": "1.9.0", "author": { "name": "ZenStack Team" }, @@ -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/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts index 3bc364bd2..d8c3df900 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -52,6 +52,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 742f7087f..70400db64 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); }; 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); }; super.completionForKeyword(context, keyword, customAcceptor); } 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 64777b62e..ea6317504 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -288,7 +288,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}`; @@ -304,10 +304,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 98dfa717e..2aa426b57 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -217,6 +217,10 @@ export default class PrismaSchemaGenerator { return JSON.stringify(expr.value); } + private exprToText(expr: Expression) { + return new ZModelCodeGenerator({ quote: 'double' }).generate(expr); + } + private generateGenerator(prisma: PrismaModel, decl: GeneratorDecl) { const generator = prisma.addGenerator( decl.name, @@ -368,7 +372,7 @@ export default 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)) { @@ -391,7 +395,7 @@ export default 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/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index be241fe2c..a3bee6b2e 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. @@ -598,3 +679,9 @@ 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) + +/** + * 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 6d8d02b14..b86637b58 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) } `); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b4ccab542..99d7ba495 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.9.1", + "version": "1.9.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { 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/server/package.json b/packages/server/package.json index af441d4f1..7ed1e3e29 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.9.1", + "version": "1.9.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 8acf19707..0aafe452e 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.9.1", + "version": "1.9.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, @@ -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 d501a52ca..9dcef261b 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 @@ -484,8 +484,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 @@ -739,8 +739,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 @@ -10232,8 +10232,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: @@ -10241,12 +10241,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 @@ -12619,6 +12627,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: From 43eb61508fbde4431831343566dd637dff7a6d49 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Feb 2024 21:49:37 +0800 Subject: [PATCH 06/43] fix: generate suspense queries in tanstack-query plugin (#996) --- .../plugins/tanstack-query/src/generator.ts | 171 ++++++++++-------- .../tanstack-query/src/runtime-v5/react.ts | 57 ++++++ 2 files changed, 152 insertions(+), 76 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index bf0c88e0a..10852e826 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -78,68 +78,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( @@ -313,23 +333,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 ); } @@ -565,19 +570,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';`, @@ -587,6 +602,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; + } default: throw new PluginError(name, `Unsupported target: ${target}`); } @@ -597,6 +613,7 @@ function makeQueryOptions( returnType: string, dataType: string, infinite: boolean, + suspense: boolean, version: TanStackVersion ) { switch (target) { @@ -604,8 +621,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 * From 613ac8d2cd638272bcc7b24e0fb96e60c0d43acc Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 13 Feb 2024 09:25:43 +0800 Subject: [PATCH 07/43] fix: use zod parse result data as mutation input (#997) --- .../src/enhancements/policy/handler.ts | 26 ++++++-- .../src/enhancements/policy/policy-utils.ts | 21 +++++++ packages/schema/src/plugins/zod/generator.ts | 6 +- .../with-policy/field-validation.test.ts | 59 +++++++++++++++++++ 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index e9f4daae0..698dcd364 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -249,7 +249,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.utils.makeIdSelection(this.model) }; @@ -305,12 +305,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.utils.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.utils.replace(item, r); + } + }); pushIdFields(model, context); }, @@ -319,7 +327,9 @@ export class PolicyProxyHandler implements Pr throw this.utils.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.utils.checkExistence(db, model, args.where); if (existing) { @@ -468,6 +478,9 @@ export class PolicyProxyHandler implements Pr parseResult.error ); } + return parseResult.data; + } else { + return data; } } @@ -495,7 +508,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.utils.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 388f9cd90..63b83b79f 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -1276,6 +1276,27 @@ export class PolicyUtil { 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/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 2727a781f..d1af70882 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/tests/integration/tests/enhancements/with-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts index 8727f1561..55b9c5cee 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' }), + }); + }); }); From ad1829132d140d2b8cbfff8e3281e29493ed9477 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 13 Feb 2024 10:31:21 +0800 Subject: [PATCH 08/43] chore: skipping several tests that hang intermittently (#998) --- tests/integration/tests/cli/init.test.ts | 8 ++++---- .../integration/tests/frameworks/trpc/generation.test.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts index 96492b286..ab6fcc747 100644 --- a/tests/integration/tests/cli/init.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -9,7 +9,9 @@ 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', () => { +// 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; beforeEach(() => { @@ -39,9 +41,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 .', 'inherit', { npm_config_user_agent: 'yarn', npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), diff --git a/tests/integration/tests/frameworks/trpc/generation.test.ts b/tests/integration/tests/frameworks/trpc/generation.test.ts index 5e15d9943..e4cd0ede1 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(' '); From b4579c7f3a5d47e1f61c5268d3ac8efbebc7557d Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 18 Feb 2024 10:46:59 +0800 Subject: [PATCH 09/43] chore: update JetBrains extension changelog (#1007) --- packages/ide/jetbrains/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) 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. From b2e1635cb1857afebde286a0c077c0f561d0bbec Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 18 Feb 2024 17:57:51 -0800 Subject: [PATCH 10/43] fix: vue-query typing issue (#1009) --- packages/plugins/tanstack-query/src/runtime/vue.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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({ From 0704f9db945fc922746ecd480ae833fd64415784 Mon Sep 17 00:00:00 2001 From: ErikMCM <70036542+ErikMCM@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:01:41 -0600 Subject: [PATCH 11/43] fix: zenstack cli errors while using bun/bunx during docker build (#1011) --- packages/schema/src/cli/actions/repl.ts | 8 +++++++- packages/schema/src/plugins/prisma/schema-generator.ts | 10 +++++----- packages/schema/src/utils/exec-utils.ts | 9 +++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) 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/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 2aa426b57..881983caa 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -47,7 +47,7 @@ import stripColor from 'strip-color'; 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 { ModelFieldType, @@ -127,7 +127,7 @@ export default class PrismaSchemaGenerator { if (options.format === true) { try { // run 'prisma format' - await execSync(`npx prisma format --schema ${outFile}`); + await execPackage(`prisma format --schema ${outFile}`); } catch { warnings.push(`Failed to format Prisma schema file`); } @@ -136,18 +136,18 @@ export default 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, 'ignore'); + await execPackage(generateCmd, '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/utils/exec-utils.ts b/packages/schema/src/utils/exec-utils.ts index f355ae2b4..8f0508dbb 100644 --- a/packages/schema/src/utils/exec-utils.ts +++ b/packages/schema/src/utils/exec-utils.ts @@ -7,3 +7,12 @@ export function execSync(cmd: string, stdio: StdioOptions = 'inherit', env?: Rec const mergedEnv = { ...process.env, ...env }; _exec(cmd, { encoding: 'utf-8', stdio, env: mergedEnv }); } + +/** + * Utility for running package commands through npx/bunx + */ +export function execPackage(cmd: string, stdio: StdioOptions = 'inherit', env?: Record): void { + const packageManager = process?.versions?.bun ? 'bunx' : 'npx'; + const mergedEnv = { ...process.env, ...env }; + _exec(`${packageManager} ${cmd}`, { encoding: 'utf-8', stdio, env: mergedEnv }); +} \ No newline at end of file From d7b75e9c1da943267ae79381e5ee605b864a4f26 Mon Sep 17 00:00:00 2001 From: Jason Kleinberg Date: Mon, 19 Feb 2024 12:38:38 -0500 Subject: [PATCH 12/43] Allows enum types for id fields (#1010) --- .../validator/datamodel-validator.ts | 10 +- packages/schema/tests/schema/stdlib.test.ts | 2 +- .../validation/datamodel-validation.test.ts | 415 ++++++++++-------- .../validation/datasource-validation.test.ts | 25 +- packages/schema/tests/utils.ts | 38 +- 5 files changed, 286 insertions(+), 204 deletions(-) diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 3096d5257..379d4e46a 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 } 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/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/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 4212441fe..78e31204d 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 - } + const result = await safelyLoadModel(` + ${ prelude } + model M { + id String @id + x Int + x String + } `) - ).toContain('Duplicated declaration name "x"'); + + 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,208 +75,258 @@ 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(` - ${prelude} + it('should suceed when @@unique used as id', async () => { + const result = await safelyLoadModel(` + ${ prelude } model M { x Int @@unique([x]) @@allow('all', x > 0) } `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }) + + 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' }); + }) + + 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 - @@deny('all', x <= 0) - } - `) - ).toContain(err); + 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(result).toMatchObject(errorLike(err)); + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @gt(0) - } - `) - ).toContain(err); + 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(result).toMatchObject(errorLike(err)); + }) - 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 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 - y Int - @@id([x, y]) - } - `) - ).toContain(`Model cannot have both field-level @id and model-level @@id attributes`); + 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`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int? @id - } - `) - ).toContain(`Field with @id attribute must not be optional`); + 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 Int? - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must not be optional`); + 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 Int[] @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 + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int[] - @@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([x]) + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - 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 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`)) + }) - 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 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`)) + }) - 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 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`)) + }) - 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 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 +379,7 @@ describe('Data Model Validation Tests', () => { // one-to-one incomplete expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -337,11 +390,11 @@ 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 +407,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 +424,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 +441,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 +458,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 +475,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 +492,11 @@ 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 +508,11 @@ 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 +525,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 +541,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 +656,7 @@ describe('Data Model Validation Tests', () => { }); it('abstract base type', async () => { - const errors = await loadModelWithError(` + const errors = await safelyLoadModel(` ${prelude} abstract model Base { @@ -623,11 +672,11 @@ describe('Data Model Validation Tests', () => { } `); - expect(errors).toContain(`Model A cannot be extended because it's not abstract`); + expect(errors).toMatchObject(errorLike(`Model A cannot be extended because it's not abstract`)); // relation incomplete from multiple level inheritance expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model User { id Int @id @default(autoincrement()) @@ -647,6 +696,6 @@ 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/utils.ts b/packages/schema/tests/utils.ts index f88aae6e2..7369838f5 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) + }, +}) From 43982315c6053161e817e566be8c9f69ded71c73 Mon Sep 17 00:00:00 2001 From: Jason Kleinberg Date: Mon, 19 Feb 2024 18:16:20 -0500 Subject: [PATCH 13/43] Minor language server fixes (#1013) --- .../validator/schema-validator.ts | 3 ++- packages/schema/src/res/stdlib.zmodel | 4 ++-- .../validation/attribute-validation.test.ts | 20 +++++++++++++++++++ .../validation/schema-validation.test.ts | 14 +++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/schema/src/language-server/validator/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts index b80bf890d..6f868c614 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/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index a3bee6b2e..145ffed60 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/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index b86637b58..8eb674b2f 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/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(` From da53753f1bc912272c1a7e640bcecff0faf5a596 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 20 Feb 2024 15:22:56 -0800 Subject: [PATCH 14/43] chore: improve test run speed (#1018) --- .github/workflows/build-test.yml | 2 +- .gitignore | 1 + CONTRIBUTING.md | 8 + jest.config.ts | 6 +- package.json | 6 +- packages/testtools/package.json | 2 +- packages/testtools/src/.npmrc.template | 1 - packages/testtools/src/package.template.json | 21 -- packages/testtools/src/schema.ts | 71 +++-- 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/tests/cli/generate.test.ts | 6 +- tests/integration/tests/cli/plugins.test.ts | 6 +- tests/integration/tests/schema/todo.zmodel | 2 +- 17 files changed, 369 insertions(+), 94 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 f92512bc8..404f42d3a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -97,4 +97,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 f1f733983..97319e08c 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 2d2d2e264..42168988a 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\"" @@ -30,6 +31,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/testtools/package.json b/packages/testtools/package.json index 0aafe452e..38198a59e 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 8ea542361..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": "^4.8.0", - "@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", - "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 f69a845cc..c570c6a30 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -35,14 +35,23 @@ export type FullDbClientContract = Record & { }; 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) { @@ -86,17 +95,17 @@ generator js { plugin meta { provider = '@core/model-meta' - preserveTsFiles = true + // preserveTsFiles = true } plugin policy { provider = '@core/access-policy' - preserveTsFiles = true + // preserveTsFiles = true } plugin zod { provider = '@core/zod' - preserveTsFiles = true + // preserveTsFiles = true modelOnly = ${!options.fullZod} } `; @@ -138,21 +147,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); @@ -189,16 +206,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) { @@ -209,10 +226,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 9dcef261b..b432b95c7 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 @@ -1594,6 +1597,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'} @@ -1612,6 +1624,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'} @@ -1648,6 +1669,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'} @@ -1675,6 +1705,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'} @@ -1702,6 +1741,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'} @@ -1729,6 +1777,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'} @@ -1756,6 +1813,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'} @@ -1783,6 +1849,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'} @@ -1810,6 +1885,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'} @@ -1837,6 +1921,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'} @@ -1864,6 +1957,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'} @@ -1900,6 +2002,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'} @@ -1927,6 +2038,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'} @@ -1954,6 +2074,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'} @@ -1981,6 +2110,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'} @@ -2008,6 +2146,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'} @@ -2035,6 +2182,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'} @@ -2062,6 +2218,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'} @@ -2089,6 +2254,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'} @@ -2116,6 +2290,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'} @@ -2143,6 +2326,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'} @@ -2170,6 +2362,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'} @@ -2197,6 +2398,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'} @@ -7770,6 +7980,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'} @@ -8597,6 +8838,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 @@ -12958,6 +13205,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'} @@ -14255,6 +14506,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/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 0367033bd..90f9e2311 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 005a0f69b..716ac224e 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 733391bd1..079e3b1ef 100644 --- a/tests/integration/tests/schema/todo.zmodel +++ b/tests/integration/tests/schema/todo.zmodel @@ -14,7 +14,7 @@ generator js { plugin zod { provider = '@core/zod' - preserveTsFiles = true + // preserveTsFiles = true } /* From 65473bbbfafda9c59aa2182bec811919706d99ea Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 21 Feb 2024 17:53:32 -0800 Subject: [PATCH 15/43] chore: set up ZENSTACK_TEST environment variable during test setup (#1048) --- 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 +- packages/schema/package.json | 2 +- packages/server/package.json | 2 +- script/set-test-env.ts | 1 + test-setup.ts => script/test-global-setup.ts | 2 +- tests/integration/package.json | 2 +- 11 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 script/set-test-env.ts rename test-setup.ts => script/test-global-setup.ts (70%) 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 42168988a..e8409e3bd 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 96a336f58..04508605c 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 c64538378..b4495136d 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 0c16ca59d..b835479ba 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 0b53d2aa2..8d74b6edb 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/schema/package.json b/packages/schema/package.json index 2451a87c8..f83f8121a 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/server/package.json b/packages/server/package.json index 7ed1e3e29..cbf938ea6 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/test-setup.ts b/script/test-global-setup.ts similarity index 70% rename from test-setup.ts rename to script/test-global-setup.ts index 9856ff4b5..514cccae7 100644 --- a/test-setup.ts +++ b/script/test-global-setup.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; export default function globalSetup() { - if (!fs.existsSync(path.join(__dirname, '.test/scaffold/package-lock.json'))) { + 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": "", From 4bf812e58542bccb807a2107fcd0281aac70e3f4 Mon Sep 17 00:00:00 2001 From: Jonathan S Date: Thu, 22 Feb 2024 03:08:19 +0000 Subject: [PATCH 16/43] Refactor: find up (#1019) --- packages/schema/src/cli/cli-util.ts | 4 +- .../src/plugins/prisma/schema-generator.ts | 4 +- packages/schema/src/utils/pkg-utils.ts | 53 ++++++++++++++----- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 85c38e82a..c8ede65a2 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'; @@ -280,7 +280,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/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 881983caa..0eeea55c5 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -48,7 +48,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 { ModelFieldType, AttributeArg as PrismaAttributeArg, @@ -450,7 +450,7 @@ export default class PrismaSchemaGenerator { 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 { From 9db52dbb77650d7c99380308803b7b4b4b7ae42d Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 23 Feb 2024 21:34:17 -0800 Subject: [PATCH 17/43] fix: validate zod schema before update operation is executed (#1051) --- .../src/enhancements/policy/handler.ts | 65 +++++++-- .../with-policy/field-validation.test.ts | 136 ++++++++++++------ .../enhancements/with-policy/refactor.test.ts | 88 +++++++----- .../tests/schema/refactor-pg.zmodel | 6 +- 4 files changed, 210 insertions(+), 85 deletions(-) diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 698dcd364..e11379cdf 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -467,7 +467,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.utils.getZodSchema(model, 'create'); - if (schema) { + if (schema && data) { const parseResult = schema.safeParse(data); if (!parseResult.success) { throw this.utils.deniedByPolicy( @@ -496,11 +496,18 @@ export class PolicyProxyHandler implements Pr args = this.utils.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.utils.replace(item, validationResult); + } + const inputCheck = this.utils.checkInputGuard(this.model, item, 'create'); if (inputCheck === false) { + // unconditionally deny throw this.utils.deniedByPolicy( this.model, 'create', @@ -508,14 +515,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.utils.replace(item, r); - } + // unconditionally allow } else if (inputCheck === undefined) { // static policy check is not possible, need to do post-create check needPostCreateCheck = true; - break; } } @@ -786,7 +789,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.utils.replace(updatePayload, validatedPayload); + } + if (updatePayload) { for (const key of Object.keys(updatePayload)) { const field = resolveField(this.modelMeta, model, key); @@ -857,6 +866,8 @@ export class PolicyProxyHandler implements Pr ); } + args.data = this.validateUpdateInputSchema(model, args.data); + const updateGuard = this.utils.getAuthGuard(db, model, 'update'); if (this.utils.isTrue(updateGuard) || this.utils.isFalse(updateGuard)) { // injects simple auth guard into where clause @@ -917,7 +928,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 @@ -1016,6 +1030,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.utils.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.utils.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; @@ -1046,6 +1091,8 @@ export class PolicyProxyHandler implements Pr args = this.utils.clone(args); this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); + args.data = this.validateUpdateInputSchema(this.model, args.data); + if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) { // use a transaction to do post-update checks const postWriteChecks: PostWriteCheckRecord[] = []; 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 55b9c5cee..bb505ca55 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 126c038fa..6a329a739 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/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 From 912c83176a57ae2e2397c0aab68c0299a6115025 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 24 Feb 2024 00:34:02 -0800 Subject: [PATCH 18/43] fix: should not reject "update" when there's only field-level override but no model-level policy (#1052) --- .../src/enhancements/policy/policy-utils.ts | 21 ++++++-- .../tests/regression/issue-1014.test.ts | 53 +++++++++++++++++++ .../integration/tests/tsconfig.template.json | 10 ---- 3 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1014.test.ts delete mode 100644 tests/integration/tests/tsconfig.template.json diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 63b83b79f..ea5816f6c 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -319,7 +319,7 @@ export class PolicyUtil { /** * 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; @@ -328,6 +328,21 @@ export class PolicyUtil { 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. * @@ -731,7 +746,7 @@ export class PolicyUtil { 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, @@ -904,7 +919,7 @@ export class PolicyUtil { */ tryReject(db: Record, 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/tests/integration/tests/regression/issue-1014.test.ts b/tests/integration/tests/regression/issue-1014.test.ts new file mode 100644 index 000000000..7f374d24d --- /dev/null +++ b/tests/integration/tests/regression/issue-1014.test.ts @@ -0,0 +1,53 @@ +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) + } + `, + { logPrismaQuery: 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/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 583520e5dce1d898becf3da9553c6faf08db6343 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 24 Feb 2024 11:34:39 -0800 Subject: [PATCH 19/43] fix(zmodel): check optionality consistency between relation and fk fields (#1053) --- .../validator/datamodel-validator.ts | 19 ++++++++++--- .../tests/regression/issue-1014.test.ts | 3 +-- .../tests/regression/issue-177.test.ts | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 tests/integration/tests/regression/issue-177.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 379d4e46a..09af0971c 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/tests/integration/tests/regression/issue-1014.test.ts b/tests/integration/tests/regression/issue-1014.test.ts index 7f374d24d..ad862db42 100644 --- a/tests/integration/tests/regression/issue-1014.test.ts +++ b/tests/integration/tests/regression/issue-1014.test.ts @@ -19,8 +19,7 @@ describe('issue 1014', () => { @@allow('read', true) } - `, - { logPrismaQuery: true } + ` ); const db = enhance(); 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'); + }); +}); From 32f677c6cf0d5c8e152048df756355ea3a5b8acc Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 26 Feb 2024 12:44:34 -0800 Subject: [PATCH 20/43] chore: bump version (#1059) --- 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/plugins/trpc/tests/projects/t3-trpc-v10/package.json | 4 ++++ 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 +- 15 files changed, 18 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e8409e3bd..dbd80b93b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.9.0", + "version": "1.10.0", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index b3074746e..2e7742364 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.9.0" +version = "1.10.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 7305853c2..b380dc39f 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.9.0", + "version": "1.10.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 a80768913..562ff434e 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.9.0", + "version": "1.10.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 e477bc7b8..2c04bf073 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.9.0", + "version": "1.10.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 04508605c..c13ecd0c0 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.9.0", + "version": "1.10.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 b4495136d..733cd6687 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.9.0", + "version": "1.10.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 b835479ba..05a401f30 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.9.0", + "version": "1.10.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 8d74b6edb..44422a105 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.9.0", + "version": "1.10.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { 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..5878a8037 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-alpha.2.tgz", + "@zenstackhq/runtime": "file:../../../../../../.build/zenstackhq-runtime-2.0.0-alpha.2.tgz", + "@zenstackhq/sdk": "file:../../../../../../.build/zenstackhq-sdk-2.0.0-alpha.2.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-alpha.2.tgz", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 3cb61dd38..d4fb0b2a8 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.9.0", + "version": "1.10.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 f83f8121a..0edd09d95 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.9.0", + "version": "1.10.0", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 99d7ba495..d09ea775c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.9.0", + "version": "1.10.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index cbf938ea6..37b7c4fd3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.9.0", + "version": "1.10.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 38198a59e..1ee34f784 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.9.0", + "version": "1.10.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 42e39ecba3db984bdda149df01e679cbcd508a3d Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 1 Mar 2024 12:45:54 +0800 Subject: [PATCH 21/43] doc:Delete outdated packages/README.md (#1063) Co-authored-by: Yiming --- packages/README.md | 122 --------------------------------------------- 1 file changed, 122 deletions(-) delete mode 100644 packages/README.md diff --git a/packages/README.md b/packages/README.md deleted file mode 100644 index 2104ff0eb..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 { withPolicy } 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) }), -}); -``` - -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) From a01065c0aa791d6591776b908f3e1e3c4d21424b Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 29 Feb 2024 21:56:25 -0800 Subject: [PATCH 22/43] fix(tanstack): improve typing of mutation errors (#1066) --- .../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 ++++++++++---- .../tests/projects/t3-trpc-v10/package.json | 4 ---- 10 files changed, 72 insertions(+), 33 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 10852e826..3dd040f71 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -216,7 +216,7 @@ function generateMutationHook( { name: `_mutation`, initializer: ` - useModelMutation<${argsType}, ${ + useModelMutation<${argsType}, DefaultError, ${ overrideReturnType ?? model }, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst( model @@ -565,9 +565,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': { @@ -643,11 +643,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/plugins/trpc/tests/projects/t3-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json index 5878a8037..a9c99ebba 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-alpha.2.tgz", - "@zenstackhq/runtime": "file:../../../../../../.build/zenstackhq-runtime-2.0.0-alpha.2.tgz", - "@zenstackhq/sdk": "file:../../../../../../.build/zenstackhq-sdk-2.0.0-alpha.2.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-alpha.2.tgz", "zod": "^3.22.4" }, "devDependencies": { From b60627c167706728ac232ce06366d914e3dde23f Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 1 Mar 2024 10:32:02 -0800 Subject: [PATCH 23/43] fix(zod): add coercion call when generating schema for DateTime field (#1068) --- .../src/plugins/zod/utils/schema-gen.ts | 2 +- packages/server/tests/api/rest.test.ts | 90 ++++++++++++++++++- packages/server/tests/utils.ts | 1 + tests/integration/tests/plugins/zod.test.ts | 44 +++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 39e7d2bb2..889ab1674 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -174,7 +174,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/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 7b084ef8a..7af912ec3 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 0f9f025df4e101808a535e9724da5008a3e04d55 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 2 Mar 2024 12:02:57 -0800 Subject: [PATCH 24/43] chore: add sponsors and contributors to README (#1070) --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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 From e8268d03ae12f3ccbcf1bb1c531a2816b22f6da8 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 7 Mar 2024 14:33:10 -0800 Subject: [PATCH 25/43] fix: properly handle missing fields when evaluating `@@validate` model-level rules (#1097) --- .../src/plugins/zod/utils/schema-gen.ts | 10 +- .../typescript-expression-transformer.ts | 52 ++++-- packages/testtools/src/schema.ts | 2 +- tests/integration/tests/cli/plugins.test.ts | 1 + .../with-policy/field-validation.test.ts | 154 ++++++++++++++++++ .../tests/regression/issue-1078.test.ts | 52 ++++++ 6 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1078.test.ts diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 889ab1674..a13a316b4 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -4,6 +4,7 @@ import { getAttributeArg, getAttributeArgLiteral, getLiteral, + isDataModelFieldReference, isFromStdlib, } from '@zenstackhq/sdk'; import { @@ -205,10 +206,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/typescript-expression-transformer.ts b/packages/schema/src/utils/typescript-expression-transformer.ts index cd868d76c..ee63b718a 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/schema/src/utils/typescript-expression-transformer.ts @@ -17,7 +17,7 @@ import { ThisExpr, UnaryExpr, } from '@zenstackhq/language/ast'; -import { ExpressionContext, getLiteral, isFromStdlib, isFutureExpr } from '@zenstackhq/sdk'; +import { ExpressionContext, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from '@zenstackhq/sdk'; import { match, P } from 'ts-pattern'; import { getIdFields } from './ast-utils'; @@ -258,7 +258,13 @@ export class TypeScriptExpressionTransformer { } 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)`; + } } // #endregion @@ -300,8 +306,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 = `(${operand} !== undefined ? (${result}): true)`; + } + return result; } private isModelType(expr: Expression) { @@ -316,16 +332,24 @@ 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) { + // in a validation context, we treat binary involving undefined as boolean true + if (isDataModelFieldReference(expr.left)) { + _default = `(${left} !== undefined ? (${_default}): true)`; + } + if (isDataModelFieldReference(expr.right)) { + _default = `(${right} !== undefined ? (${_default}): true)`; + } + } return match(expr.operator) - .with( - 'in', - () => - `(${this.transform(expr.right, false)}?.includes(${this.transform( - expr.left, - normalizeUndefined - )}) ?? false)` + .with('in', () => + this.ensureBoolean( + `${this.transform(expr.right, false)}?.includes(${this.transform(expr.left, normalizeUndefined)})` + ) ) .with(P.union('==', '!='), () => { if (isThisExpr(expr.left) || isThisExpr(expr.right)) { @@ -363,8 +387,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/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index c570c6a30..88ffa9ba8 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -105,7 +105,7 @@ plugin policy { plugin zod { provider = '@core/zod' - // preserveTsFiles = true + preserveTsFiles = true modelOnly = ${!options.fullZod} } `; diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 716ac224e..6efb3bad4 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-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts index bb505ca55..d34c7183b 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -609,3 +609,157 @@ 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 comparison', 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 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(); + }); +}); 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..4f8ad6527 --- /dev/null +++ b/tests/integration/tests/regression/issue-1078.test.ts @@ -0,0 +1,52 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1078', () => { + it('regression', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Counter { + id String @id + + name String + value Int + + @@validate(value >= 0) + @@allow('all', true) + } + ` + ); + + const db = enhance(); + + const counter = await db.counter.create({ + data: { id: '1', name: 'It should create', value: 1 }, + }); + + //! This query fails validation + const updated = await db.counter.update({ + where: { id: '1' }, + data: { name: 'It should update' }, + }); + }); + + 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', + }); + }); +}); From 4dd7aa0d994b157886a78a277b691e847239c0e9 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 7 Mar 2024 16:01:46 -0800 Subject: [PATCH 26/43] chore: bump version (#1098) --- 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 dbd80b93b..a8af271b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.10.0", + "version": "1.10.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 2e7742364..578f2334e 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.10.0" +version = "1.10.1" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index b380dc39f..fcb014277 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.10.0", + "version": "1.10.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 562ff434e..e0a904034 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.10.0", + "version": "1.10.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 2c04bf073..ff15cb51b 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.10.0", + "version": "1.10.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 c13ecd0c0..0aff04595 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.10.0", + "version": "1.10.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 733cd6687..813b750f0 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.10.0", + "version": "1.10.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 05a401f30..49edb485d 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.10.0", + "version": "1.10.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 44422a105..8dbf8719b 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.10.0", + "version": "1.10.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d4fb0b2a8..f77b44d13 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.10.0", + "version": "1.10.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 0edd09d95..cf6598db3 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.10.0", + "version": "1.10.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d09ea775c..71d0e1144 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.10.0", + "version": "1.10.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 37b7c4fd3..76a711e8c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.10.0", + "version": "1.10.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 1ee34f784..ae102ca3d 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.10.0", + "version": "1.10.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From e7e1873744ac2d48e118ae48b23e10723d16db44 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 7 Mar 2024 22:11:47 -0800 Subject: [PATCH 27/43] fix(policy): properly handle array-form of upsert payload (#1101) --- .../runtime/src/cross/nested-write-visitor.ts | 32 +++-- .../src/enhancements/policy/handler.ts | 53 ++++--- .../tests/regression/issue-1078.test.ts | 25 ++-- .../tests/regression/issue-1080.test.ts | 133 ++++++++++++++++++ 4 files changed, 204 insertions(+), 39 deletions(-) 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 7d67f6d9b..7c4e8e5e5 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); } @@ -225,7 +225,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) { @@ -244,7 +244,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) { @@ -258,7 +258,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) { @@ -278,7 +278,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); } @@ -288,7 +288,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); } @@ -336,4 +336,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 e11379cdf..808763ae8 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -343,17 +343,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 { @@ -361,11 +351,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; @@ -895,7 +885,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; @@ -928,14 +918,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 @@ -943,7 +934,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; @@ -1390,5 +1381,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/tests/integration/tests/regression/issue-1078.test.ts b/tests/integration/tests/regression/issue-1078.test.ts index 4f8ad6527..3c0fc7024 100644 --- a/tests/integration/tests/regression/issue-1078.test.ts +++ b/tests/integration/tests/regression/issue-1078.test.ts @@ -2,7 +2,7 @@ import { loadSchema } from '@zenstackhq/testtools'; describe('issue 1078', () => { it('regression', async () => { - const { prisma, enhance } = await loadSchema( + const { enhance } = await loadSchema( ` model Counter { id String @id @@ -12,21 +12,25 @@ describe('issue 1078', () => { @@validate(value >= 0) @@allow('all', true) - } + } ` ); const db = enhance(); - const counter = await db.counter.create({ - data: { id: '1', name: 'It should create', value: 1 }, - }); + await expect( + db.counter.create({ + data: { id: '1', name: 'It should create', value: 1 }, + }) + ).toResolveTruthy(); //! This query fails validation - const updated = await db.counter.update({ - where: { id: '1' }, - data: { name: 'It should update' }, - }); + await expect( + db.counter.update({ + where: { id: '1' }, + data: { name: 'It should update' }, + }) + ).toResolveTruthy(); }); it('read', async () => { @@ -37,8 +41,7 @@ describe('issue 1078', () => { title String @allow('read', true, true) content String } - `, - { logPrismaQuery: true } + ` ); const db = enhance(); 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 2b12e090eb7287d82debcf26a411e7494e650c48 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 7 Mar 2024 22:12:57 -0800 Subject: [PATCH 28/43] chore: bump version (#1102) --- 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 a8af271b3..77186957a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.10.1", + "version": "1.10.2", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 578f2334e..991ec903d 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.10.1" +version = "1.10.2" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index fcb014277..95a8cd155 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.10.1", + "version": "1.10.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 e0a904034..998be4724 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.10.1", + "version": "1.10.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 ff15cb51b..b31af2556 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.10.1", + "version": "1.10.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 0aff04595..85c46c860 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.10.1", + "version": "1.10.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 813b750f0..9e12e6e35 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.10.1", + "version": "1.10.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 49edb485d..993188e34 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.10.1", + "version": "1.10.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 8dbf8719b..f8df06310 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.10.1", + "version": "1.10.2", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index f77b44d13..a789ac665 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.10.1", + "version": "1.10.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 cf6598db3..200ad7d03 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.10.1", + "version": "1.10.2", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 71d0e1144..392afa394 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.10.1", + "version": "1.10.2", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 76a711e8c..3e3e95711 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.10.1", + "version": "1.10.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 ae102ca3d..b5a896b31 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.10.1", + "version": "1.10.2", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 79ef57a67cbdf3b015c92f607d86543a4a169bcb Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 8 Mar 2024 17:08:21 -0800 Subject: [PATCH 29/43] fix: `@@validate` should ignore fields that are not present (#1104) --- .../attribute-application-validator.ts | 15 +- .../validator/expression-validator.ts | 51 +++-- packages/schema/src/utils/ast-utils.ts | 17 +- .../typescript-expression-transformer.ts | 131 ++++++++++--- .../tests/generator/expression-writer.test.ts | 18 +- .../validation/attribute-validation.test.ts | 41 ++-- .../with-policy/field-validation.test.ts | 179 +++++++++++++++++- 7 files changed, 367 insertions(+), 85 deletions(-) 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 7644521b8..7d8c4dd95 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -3,16 +3,17 @@ import { Expression, ExpressionType, isDataModel, + isDataModelAttribute, + isDataModelField, isEnum, + isLiteralExpr, isMemberAccessExpr, isNullExpr, isThisExpr, - isDataModelField, - isLiteralExpr, } from '@zenstackhq/language/ast'; import { isDataModelFieldReference, isEnumFieldReference } from '@zenstackhq/sdk'; -import { ValidationAcceptor } from 'langium'; -import { getContainingDataModel, isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils'; +import { AstNode, ValidationAcceptor } from 'langium'; +import { findUpAst, getContainingDataModel, isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { typeAssignable } from './utils'; @@ -123,6 +124,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; @@ -132,18 +144,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 +223,17 @@ 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) + 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/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 661f14b26..348752fae 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -157,7 +157,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 +166,18 @@ export function getContainingDataModel(node: Expression): DataModel | undefined curr = curr.$container; } return undefined; -} \ No newline at end of file +} + +/** + * 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/typescript-expression-transformer.ts b/packages/schema/src/utils/typescript-expression-transformer.ts index ee63b718a..ec4f89fcb 100644 --- a/packages/schema/src/utils/typescript-expression-transformer.ts +++ b/packages/schema/src/utils/typescript-expression-transformer.ts @@ -5,9 +5,6 @@ import { DataModel, Expression, InvocationExpr, - isDataModel, - isEnumField, - isThisExpr, LiteralExpr, MemberAccessExpr, NullExpr, @@ -16,9 +13,15 @@ import { StringLiteral, ThisExpr, UnaryExpr, + isArrayExpr, + isDataModel, + isEnumField, + isLiteralExpr, + isNullExpr, + isThisExpr, } from '@zenstackhq/language/ast'; import { ExpressionContext, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from '@zenstackhq/sdk'; -import { match, P } from 'ts-pattern'; +import { P, match } from 'ts-pattern'; import { getIdFields } from './ast-utils'; export class TypeScriptExpressionTransformerError extends Error { @@ -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,22 +250,27 @@ 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) { @@ -263,7 +279,22 @@ export class TypeScriptExpressionTransformer { // as boolean true return `(${expr} ?? true)`; } else { - return `(${expr} ?? false)`; + 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)`; } } @@ -315,7 +346,7 @@ export class TypeScriptExpressionTransformer { isDataModelFieldReference(expr.operand) ) { // in a validation context, we treat unary involving undefined as boolean true - result = `(${operand} !== undefined ? (${result}): true)`; + result = this.ensureBooleanTernary(expr.operand, operand, result); } return result; } @@ -336,21 +367,45 @@ export class TypeScriptExpressionTransformer { let _default = `(${left} ${expr.operator} ${right})`; if (this.options.context === ExpressionContext.ValidationRule) { - // in a validation context, we treat binary involving undefined as boolean true - if (isDataModelFieldReference(expr.left)) { - _default = `(${left} !== undefined ? (${_default}): true)`; - } - if (isDataModelFieldReference(expr.right)) { - _default = `(${right} !== undefined ? (${_default}): true)`; + 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.ensureBoolean( - `${this.transform(expr.right, false)}?.includes(${this.transform(expr.left, normalizeUndefined)})` - ) - ) + .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, + 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 @@ -376,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({ diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index f9baa0de9..7121c9589 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 8eb674b2f..eb8a6065b 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 @@ -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/tests/integration/tests/enhancements/with-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts index d34c7183b..d54913bb3 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; @@ -685,7 +685,7 @@ describe('With Policy: model-level validation', () => { await expect(db.model.create({ data: {} })).toResolveTruthy(); }); - it('optionality with comparison', async () => { + it('optionality with binary', async () => { const { enhance } = await loadSchema(` model Model { id Int @id @default(autoincrement()) @@ -705,6 +705,56 @@ describe('With Policy: model-level validation', () => { 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 { @@ -762,4 +812,129 @@ describe('With Policy: model-level validation', () => { 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(); + }); }); From a543ffedc6562d3bad5803f98496b2df75b5f6ed Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 8 Mar 2024 17:08:51 -0800 Subject: [PATCH 30/43] chore: bump version (#1105) --- 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 77186957a..7127fc1b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.10.2", + "version": "1.10.3", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 991ec903d..702173ecc 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.10.2" +version = "1.10.3" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 95a8cd155..1243423e7 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.10.2", + "version": "1.10.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 998be4724..a07cd6772 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.10.2", + "version": "1.10.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 b31af2556..300daaced 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.10.2", + "version": "1.10.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 85c46c860..d0f39b473 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.10.2", + "version": "1.10.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 9e12e6e35..1bd780195 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.10.2", + "version": "1.10.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 993188e34..2cd3e1161 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.10.2", + "version": "1.10.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 f8df06310..bf8107afd 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.10.2", + "version": "1.10.3", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a789ac665..160341df6 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.10.2", + "version": "1.10.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 200ad7d03..bae642f1a 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.10.2", + "version": "1.10.3", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 392afa394..651429ef8 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.10.2", + "version": "1.10.3", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 3e3e95711..1936946b2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.10.2", + "version": "1.10.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 b5a896b31..d8f7ce067 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.10.2", + "version": "1.10.3", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From be9be0c09bf51b00387af5a026fd95e695abb598 Mon Sep 17 00:00:00 2001 From: Yiming Date: Sun, 10 Mar 2024 11:21:39 -0700 Subject: [PATCH 31/43] chore: remove short.io links from README (#1119) --- README.md | 10 +++++----- packages/schema/README.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) 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 From 4d9d09338ae88eac331ec06ec908ca1256f5b8a5 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 12 Mar 2024 20:53:14 -0700 Subject: [PATCH 32/43] fix: make sure fields inherited from abstract base models are properly recognized as id (#1130) --- packages/sdk/src/utils.ts | 9 +- .../tests/regression/issue-1129.test.ts | 87 +++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1129.test.ts diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index d32962f11..a32abc068 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -218,11 +218,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; } @@ -234,12 +237,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 d49731b60ba7eb370bad7099b47545ac72aa8497 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 12 Mar 2024 20:53:45 -0700 Subject: [PATCH 33/43] chore: bump version (#1131) --- 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 7127fc1b8..d61f79174 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.10.3", + "version": "1.11.0", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 702173ecc..23dc11e99 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.10.3" +version = "1.11.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 1243423e7..09476b216 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.10.3", + "version": "1.11.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 a07cd6772..50758ce23 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.10.3", + "version": "1.11.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 300daaced..d95ef1925 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.10.3", + "version": "1.11.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 d0f39b473..3db67dead 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.10.3", + "version": "1.11.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 1bd780195..0d1fb7147 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.10.3", + "version": "1.11.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 2cd3e1161..8d034e8f4 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.10.3", + "version": "1.11.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 bf8107afd..ad76965c2 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.10.3", + "version": "1.11.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 160341df6..5665262c1 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.10.3", + "version": "1.11.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 bae642f1a..249389e68 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.10.3", + "version": "1.11.0", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 651429ef8..110fec6c0 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.10.3", + "version": "1.11.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 1936946b2..ea60177a5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.10.3", + "version": "1.11.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 d8f7ce067..97e842f00 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.10.3", + "version": "1.11.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From f8f214d8d22919d49de17e648b3c1f98ab98b507 Mon Sep 17 00:00:00 2001 From: Jonathan S Date: Sat, 16 Mar 2024 14:44:28 +0000 Subject: [PATCH 34/43] feat: allow users to import from node_modules (#1021) --- packages/schema/src/utils/ast-utils.ts | 22 +++++++++++----- packages/schema/src/utils/pkg-utils.ts | 36 +++++++++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 348752fae..c752c837d 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -18,6 +18,8 @@ import { import { isFromStdlib } from '@zenstackhq/sdk'; import { AstNode, getDocument, LangiumDocuments, Mutable } from 'langium'; 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( @@ -94,15 +96,21 @@ export function getDataModelFieldReference(expr: Expression): DataModelField | u } export function resolveImportUri(imp: ModelImport): URI | undefined { - if (imp.path === undefined || imp.path.length === 0) { - return undefined; + if (!imp.path) return undefined; // This will return true if imp.path is undefined, null, or an empty string (""). + + if (!imp.path.endsWith('.zmodel')) { + imp.path += '.zmodel'; } - const dirUri = Utils.dirname(getDocument(imp).uri); - let grammarPath = imp.path; - if (!grammarPath.endsWith('.zmodel')) { - grammarPath += '.zmodel'; + + if ( + !imp.path.startsWith('.') // Respect relative paths + && !isAbsolute(imp.path) // Respect Absolute paths + ) { + imp.path = findNodeModulesFile(imp.path) ?? imp.path; } - return Utils.resolvePath(dirUri, grammarPath); + + const dirUri = Utils.dirname(getDocument(imp).uri); + return Utils.resolvePath(dirUri, imp.path); } export function resolveTransitiveImports(documents: LangiumDocuments, model: Model): Model[] { diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index ce41dac34..99593dcd3 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -5,8 +5,8 @@ import { execSync } from './exec-utils'; 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 @@ -14,9 +14,9 @@ export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; */ 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 @@ -37,6 +37,30 @@ export function findUp(names: string[], cwd: string = return findUp(names, up, multiple, result); } + +/** + * Find a Node module/file given its name in a specific directory, with a fallback to the current working directory. + * If the name is empty, return undefined. + * Try to resolve the module/file using require.resolve with the specified directory as the starting point. + * Return the resolved path if successful, otherwise return undefined. + * + * @export + * @param {string} name The name of the module/file to find + * @param {string} [cwd=process.cwd()] + * @returns {*} Finds a specified module or file using require.resolve starting from a specified directory path, or the current working directory if not provided. + */ +export function findNodeModulesFile(name: string, cwd: string = process.cwd()) { + if (!name) return undefined; + try { + // Use require.resolve to find the module/file. The paths option allows specifying the directory to start from. + const resolvedPath = require.resolve(name, { paths: [cwd] }) + return resolvedPath + } catch (error) { + // If require.resolve fails to find the module/file, it will throw an error. + return undefined + } +} + function getPackageManager(projectPath = '.'): PackageManagers { const lockFile = findUp(['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'], projectPath); @@ -106,7 +130,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 */ From d7dbba9477302307a071bc7d96be250b8e27700b Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 16 Mar 2024 18:01:12 -0700 Subject: [PATCH 35/43] chore: update jetbrains changelog (#1147) --- packages/ide/jetbrains/CHANGELOG.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index 1fa15f2eb..226419eb2 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -1,11 +1,23 @@ # Changelog ## [Unreleased] + +### Fixed + +- General improvements to language service. + +## 1.9.0 + ### Added -- Added support to complex usage of `@@index` attribute like `@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)`. + +- 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. + +- Fixed several ZModel validation issues related to model inheritance. ## 1.7.0 + ### Added -- Auto-completion is now supported inside attributes. + +- Auto-completion is now supported inside attributes. From 24b189eb61d874ed269e9527ee5ed9c60a5955c9 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Tue, 19 Mar 2024 12:27:35 +0800 Subject: [PATCH 36/43] test: fix test failing in windows (#1153) --- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- .../openapi/tests/openapi-restful.test.ts | 16 ++++++------- .../plugins/openapi/tests/openapi-rpc.test.ts | 24 +++++++++---------- packages/plugins/swr/package.json | 2 +- packages/plugins/swr/tests/swr.test.ts | 6 ++--- packages/plugins/tanstack-query/package.json | 2 +- .../tanstack-query/tests/plugin.test.ts | 14 +++++------ packages/plugins/trpc/package.json | 2 +- packages/plugins/trpc/tests/trpc.test.ts | 22 ++++++++--------- packages/testtools/src/schema.ts | 9 ++++--- 12 files changed, 53 insertions(+), 50 deletions(-) diff --git a/packages/language/package.json b/packages/language/package.json index 50758ce23..b6e146450 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -9,7 +9,7 @@ "generate": "langium generate && npx ts-node script/generate-plist.ts", "watch": "concurrently \"langium generate --watch\" \"tsc --watch\"", "lint": "eslint src --ext ts", - "build": "pnpm lint --max-warnings=0 && pnpm clean && pnpm generate && tsc && copyfiles -F ./README.md ./LICENSE ./package.json 'syntaxes/**/*' dist && pnpm pack dist --pack-destination '../../../.build'", + "build": "pnpm lint --max-warnings=0 && pnpm clean && pnpm generate && tsc && copyfiles -F ./README.md ./LICENSE ./package.json 'syntaxes/**/*' dist && pnpm pack dist --pack-destination ../../../.build", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index d95ef1925..207173636 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -9,7 +9,7 @@ }, "scripts": { "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && pnpm pack dist --pack-destination '../../../.build'", + "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && pnpm pack dist --pack-destination ../../../.build", "watch": "tsc --watch", "lint": "eslint src --ext ts", "prepublishOnly": "pnpm build" diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 3db67dead..a8b6f1d49 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -14,7 +14,7 @@ }, "scripts": { "clean": "rimraf dist", - "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'", + "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": "jest", diff --git a/packages/plugins/openapi/tests/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts index fb01e390e..9e84ad047 100644 --- a/packages/plugins/openapi/tests/openapi-restful.test.ts +++ b/packages/plugins/openapi/tests/openapi-restful.test.ts @@ -4,7 +4,7 @@ import OpenAPIParser from '@readme/openapi-parser'; import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk'; import { Model, Plugin, isPlugin } from '@zenstackhq/sdk/ast'; -import { loadZModelAndDmmf } from '@zenstackhq/testtools'; +import { loadZModelAndDmmf, normalizePath } from '@zenstackhq/testtools'; import fs from 'fs'; import path from 'path'; import * as tmp from 'tmp'; @@ -16,7 +16,7 @@ describe('Open API Plugin RESTful Tests', () => { for (const specVersion of ['3.0.0', '3.1.0']) { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '${specVersion}' } @@ -114,7 +114,7 @@ model Bar { it('options', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '3.0.0' title = 'My Awesome API' version = '1.0.0' @@ -151,7 +151,7 @@ model User { it('security schemes valid', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'http', scheme: 'basic' }, myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, @@ -198,7 +198,7 @@ model Post { it('security model level override', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'http', scheme: 'basic' } } @@ -230,7 +230,7 @@ model User { it('security schemes invalid', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'invalid', scheme: 'basic' } } @@ -251,7 +251,7 @@ model User { it('ignored model used as relation', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' } model User { @@ -284,7 +284,7 @@ model Post { for (const specVersion of ['3.0.0', '3.1.0']) { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '${specVersion}' } diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts index c0cb74ab6..8e8e3a6ac 100644 --- a/packages/plugins/openapi/tests/openapi-rpc.test.ts +++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts @@ -4,7 +4,7 @@ import OpenAPIParser from '@readme/openapi-parser'; import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk'; import { Model, Plugin, isPlugin } from '@zenstackhq/sdk/ast'; -import { loadZModelAndDmmf } from '@zenstackhq/testtools'; +import { loadZModelAndDmmf, normalizePath } from '@zenstackhq/testtools'; import fs from 'fs'; import path from 'path'; import * as tmp from 'tmp'; @@ -16,7 +16,7 @@ describe('Open API Plugin RPC Tests', () => { for (const specVersion of ['3.0.0', '3.1.0']) { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '${specVersion}' } @@ -127,7 +127,7 @@ model Bar { it('options', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '3.0.0' title = 'My Awesome API' version = '1.0.0' @@ -164,7 +164,7 @@ model User { it('security schemes valid', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'http', scheme: 'basic' }, myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, @@ -198,7 +198,7 @@ model User { it('security schemes invalid', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'invalid', scheme: 'basic' } } @@ -219,7 +219,7 @@ model User { it('security model level override', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'http', scheme: 'basic' } } @@ -247,7 +247,7 @@ model User { it('security operation level override', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'http', scheme: 'basic' } } @@ -280,7 +280,7 @@ model User { it('security inferred', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' securitySchemes = { myBasic: { type: 'http', scheme: 'basic' } } @@ -306,7 +306,7 @@ model User { it('v3.1.0 fields', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' summary = 'awesome api' } @@ -330,7 +330,7 @@ model User { it('ignored model used as relation', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' } model User { @@ -362,7 +362,7 @@ model Post { for (const specVersion of ['3.0.0', '3.1.0']) { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' specVersion = '${specVersion}' } @@ -408,7 +408,7 @@ generator js { } plugin openapi { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' } enum role { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 0d1fb7147..376ad2e3b 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -10,7 +10,7 @@ }, "scripts": { "clean": "rimraf dist", - "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'", + "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": "jest", diff --git a/packages/plugins/swr/tests/swr.test.ts b/packages/plugins/swr/tests/swr.test.ts index 9d198269b..d12c3b37b 100644 --- a/packages/plugins/swr/tests/swr.test.ts +++ b/packages/plugins/swr/tests/swr.test.ts @@ -1,6 +1,6 @@ /// -import { loadSchema } from '@zenstackhq/testtools'; +import { loadSchema, normalizePath } from '@zenstackhq/testtools'; import path from 'path'; describe('SWR Plugin Tests', () => { @@ -50,7 +50,7 @@ model Foo { await loadSchema( ` plugin swr { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' } @@ -60,7 +60,7 @@ ${sharedModel} provider: 'postgresql', pushDb: false, extraDependencies: [ - `${path.join(__dirname, '../dist')}`, + `${normalizePath(path.join(__dirname, '../dist'))}`, 'react@18.2.0', '@types/react@18.2.0', 'swr@^2', diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 8d034e8f4..69c3f5fda 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -66,7 +66,7 @@ }, "scripts": { "clean": "rimraf dist", - "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'", + "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": "jest", diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index 38370d38a..824174ba5 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -1,6 +1,6 @@ /// -import { loadSchema } from '@zenstackhq/testtools'; +import { loadSchema, normalizePath } from '@zenstackhq/testtools'; import path from 'path'; describe('Tanstack Query Plugin Tests', () => { @@ -50,7 +50,7 @@ model Foo { await loadSchema( ` plugin tanstack { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'react' } @@ -71,7 +71,7 @@ ${sharedModel} await loadSchema( ` plugin tanstack { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'react' version = 'v5' @@ -93,7 +93,7 @@ ${sharedModel} await loadSchema( ` plugin tanstack { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'vue' } @@ -114,7 +114,7 @@ ${sharedModel} await loadSchema( ` plugin tanstack { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'vue' version = 'v5' @@ -136,7 +136,7 @@ ${sharedModel} await loadSchema( ` plugin tanstack { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'svelte' } @@ -157,7 +157,7 @@ ${sharedModel} await loadSchema( ` plugin tanstack { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'svelte' version = 'v5' diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index ad76965c2..b0a5a28c9 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -10,7 +10,7 @@ }, "scripts": { "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist && pnpm pack dist --pack-destination '../../../../.build'", + "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": "jest", diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts index ca4a9c14d..4c79c740a 100644 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ b/packages/plugins/trpc/tests/trpc.test.ts @@ -1,6 +1,6 @@ /// -import { loadSchema } from '@zenstackhq/testtools'; +import { loadSchema, normalizePath } from '@zenstackhq/testtools'; import fs from 'fs'; import path from 'path'; @@ -19,7 +19,7 @@ describe('tRPC Plugin Tests', () => { await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/trpc' } @@ -67,7 +67,7 @@ model Foo { const { projectDir } = await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = './trpc' } @@ -110,7 +110,7 @@ model Foo { const { projectDir } = await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = './trpc' } @@ -141,7 +141,7 @@ model Post { const { projectDir } = await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = './trpc' generateModelActions = 'findMany,findUnique,update' } @@ -171,7 +171,7 @@ model Post { const { projectDir } = await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = './trpc' generateModelActions = ['findMany', 'findUnique', 'update'] } @@ -220,7 +220,7 @@ model Post { await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/trpc' generateClientHelpers = 'react' } @@ -245,7 +245,7 @@ model Post { await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/trpc' generateClientHelpers = 'next' } @@ -265,7 +265,7 @@ model Post { await loadSchema( ` plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/trpc' } @@ -304,7 +304,7 @@ generator js { } plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/trpc' generateModels = ['Post'] generateModelActions = ['findMany', 'update'] @@ -375,7 +375,7 @@ plugin zod { } plugin trpc { - provider = '${path.resolve(__dirname, '../dist')}' + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/trpc' generateModels = ['Post'] generateModelActions = ['findMany', 'update'] diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 88ffa9ba8..5fef95299 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -54,7 +54,7 @@ export function installPackage(pkg: string, dev = false) { run(`npm install ${dev ? '-D' : ''} --no-audit --no-fund ${pkg}`); } -function normalizePath(p: string) { +export function normalizePath(p: string) { return p ? p.split(path.sep).join(path.posix.sep) : p; } @@ -174,6 +174,9 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { const files = schema.split(FILE_SPLITTER); + // Use this one to replace $projectRoot placeholder in the schema file + const normalizedProjectRoot = normalizePath(projectRoot); + if (files.length > 1) { // multiple files files.forEach((file, index) => { @@ -190,12 +193,12 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { } } - fileContent = fileContent.replaceAll('$projectRoot', projectRoot); + fileContent = fileContent.replaceAll('$projectRoot', normalizedProjectRoot); const filePath = path.join(projectRoot, fileName); fs.writeFileSync(filePath, fileContent); }); } else { - schema = schema.replaceAll('$projectRoot', projectRoot); + schema = schema.replaceAll('$projectRoot', normalizedProjectRoot); const content = opt.addPrelude ? `${makePrelude(opt)}\n${schema}` : schema; if (opt.customSchemaFilePath) { zmodelPath = path.join(projectRoot, opt.customSchemaFilePath); From 269809a360e389de1df40f751056b345a09cab32 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Tue, 19 Mar 2024 12:39:23 +0800 Subject: [PATCH 37/43] doc: add new sponsor (#1159) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d874cfefb..968f5b16a 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ Thank you for your support! Johann Rohn
Johann Rohn
Benjamin Zecirovic
Benjamin Zecirovic
+ Fabian Jocks
Fabian Jocks
+ From 018d59f58295cee4530b9650c49dc868251029dd Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 19 Mar 2024 19:03:49 -0700 Subject: [PATCH 38/43] feat: allow to pass in a custom `Prisma` module when calling `enhance` (#1160) --- .../src/enhancements/policy/handler.ts | 47 ++++++++++--------- .../src/enhancements/policy/policy-utils.ts | 9 ++-- packages/runtime/src/enhancements/types.ts | 7 +++ packages/runtime/src/enhancements/utils.ts | 25 +++++++--- .../enhancements/with-policy/options.test.ts | 37 +++++++++++++-- 5 files changed, 90 insertions(+), 35 deletions(-) diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 808763ae8..598d87881 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -4,6 +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 type { WithPolicyOptions } from '.'; import { CrudFailureReason } from '../../constants'; import { ModelDataVisitor, @@ -23,7 +24,6 @@ import { formatObject, prismaClientValidationError } from '../utils'; import { Logger } from './logger'; import { PolicyUtil } from './policy-utils'; import { createDeferredPromise } from './promise'; -import { WithPolicyOptions } from '.'; // a record for post-write policy check type PostWriteCheckRecord = { @@ -58,6 +58,7 @@ export class PolicyProxyHandler implements Pr this.logger = new Logger(prisma); this.utils = new PolicyUtil( this.prisma, + this.options, this.modelMeta, this.policy, this.zodSchemas, @@ -77,20 +78,20 @@ export class PolicyProxyHandler implements Pr findUnique(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, '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.options, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); } return this.findWithFluentCallStubs(args, 'findUniqueOrThrow', true, () => { throw this.utils.notFound(this.model); @@ -220,10 +221,10 @@ export class PolicyProxyHandler implements Pr async create(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, 'data field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); } this.utils.tryReject(this.prisma, this.model, 'create'); @@ -476,10 +477,10 @@ 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.options, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, 'data field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); } this.utils.tryReject(this.prisma, this.model, 'create'); @@ -596,13 +597,13 @@ export class PolicyProxyHandler implements Pr async update(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, '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.options, 'data field is required in query argument'); } args = this.utils.clone(args); @@ -1071,10 +1072,10 @@ export class PolicyProxyHandler implements Pr async updateMany(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); } if (!args.data) { - throw prismaClientValidationError(this.prisma, 'data field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument'); } this.utils.tryReject(this.prisma, this.model, 'update'); @@ -1130,16 +1131,16 @@ export class PolicyProxyHandler implements Pr async upsert(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, '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.options, '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.options, 'update field is required in query argument'); } this.utils.tryReject(this.prisma, this.model, 'create'); @@ -1183,10 +1184,10 @@ export class PolicyProxyHandler implements Pr async delete(args: any) { if (!args) { - throw prismaClientValidationError(this.prisma, 'query argument is required'); + throw prismaClientValidationError(this.prisma, this.options, 'query argument is required'); } if (!args.where) { - throw prismaClientValidationError(this.prisma, 'where field is required in query argument'); + throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument'); } this.utils.tryReject(this.prisma, this.model, 'delete'); @@ -1239,7 +1240,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.options, 'query argument is required'); } args = this.utils.clone(args); @@ -1255,7 +1256,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.options, 'query argument is required'); } args = this.utils.clone(args); @@ -1299,7 +1300,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.options, 'argument must be an object'); } if (Object.keys(args).length === 0) { // include all diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index ea5816f6c..5a04c0430 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -5,6 +5,7 @@ import { lowerCaseFirst } from 'lower-case-first'; import { upperCaseFirst } from 'upper-case-first'; import { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; +import type { EnhancementOptions } from '..'; import { CrudFailureReason, FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX, @@ -48,6 +49,7 @@ export class PolicyUtil { constructor( private readonly db: DbClientContract, + private readonly options: EnhancementOptions | undefined, private readonly modelMeta: ModelMeta, private readonly policy: PolicyDef, private readonly zodSchemas: ZodSchemas | undefined, @@ -1098,24 +1100,25 @@ export class PolicyUtil { return prismaClientKnownRequestError( this.db, + this.options, `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.options, `entity not found for model ${model}`, { clientVersion: getVersion(), code: 'P2025', }); } validationError(message: string) { - return prismaClientValidationError(this.db, message); + return prismaClientValidationError(this.db, this.options, message); } unknownError(message: string) { - return prismaClientUnknownRequestError(this.db, message, { + return prismaClientUnknownRequestError(this.db, this.options, message, { clientVersion: getVersion(), }); } diff --git a/packages/runtime/src/enhancements/types.ts b/packages/runtime/src/enhancements/types.ts index 9c8080096..dec8f097e 100644 --- a/packages/runtime/src/enhancements/types.ts +++ b/packages/runtime/src/enhancements/types.ts @@ -19,6 +19,13 @@ export interface CommonEnhancementOptions { * Path for loading CLI-generated code */ loadPath?: string; + + /** + * The `Prisma` module generated together with `PrismaClient`. You only need to + * pass it when you specified a custom `PrismaClient` output path. The module can + * be loaded like: `import { Prisma } from '';`. + */ + prismaModule?: any; } /** diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 73b4d42a0..72b1e393d 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -3,6 +3,7 @@ import path from 'path'; import * as util from 'util'; import type { DbClientContract } from '../types'; +import type { EnhancementOptions } from './enhance'; /** * Formats an object for pretty printing. @@ -53,25 +54,37 @@ function loadPrismaModule(prisma: any) { } } -export function prismaClientValidationError(prisma: DbClientContract, message: string) { +export function prismaClientValidationError( + prisma: DbClientContract, + options: EnhancementOptions | undefined, + message: string +) { if (!_PrismaClientValidationError) { - const _prisma = loadPrismaModule(prisma); + const _prisma = options?.prismaModule ?? loadPrismaModule(prisma); _PrismaClientValidationError = _prisma.PrismaClientValidationError; } throw new _PrismaClientValidationError(message, { clientVersion: prisma._clientVersion }); } -export function prismaClientKnownRequestError(prisma: DbClientContract, ...args: unknown[]) { +export function prismaClientKnownRequestError( + prisma: DbClientContract, + options: EnhancementOptions | undefined, + ...args: unknown[] +) { if (!_PrismaClientKnownRequestError) { - const _prisma = loadPrismaModule(prisma); + const _prisma = options?.prismaModule ?? loadPrismaModule(prisma); _PrismaClientKnownRequestError = _prisma.PrismaClientKnownRequestError; } return new _PrismaClientKnownRequestError(...args); } -export function prismaClientUnknownRequestError(prisma: DbClientContract, ...args: unknown[]) { +export function prismaClientUnknownRequestError( + prisma: DbClientContract, + options: EnhancementOptions | undefined, + ...args: unknown[] +) { if (!_PrismaClientUnknownRequestError) { - const _prisma = loadPrismaModule(prisma); + const _prisma = options?.prismaModule ?? loadPrismaModule(prisma); _PrismaClientUnknownRequestError = _prisma.PrismaClientUnknownRequestError; } throw new _PrismaClientUnknownRequestError(...args); diff --git a/tests/integration/tests/enhancements/with-policy/options.test.ts b/tests/integration/tests/enhancements/with-policy/options.test.ts index 2c661ceb4..9c82e4305 100644 --- a/tests/integration/tests/enhancements/with-policy/options.test.ts +++ b/tests/integration/tests/enhancements/with-policy/options.test.ts @@ -1,4 +1,4 @@ -import { withPolicy } from '@zenstackhq/runtime'; +import { enhance } from '@zenstackhq/runtime'; import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; @@ -20,17 +20,48 @@ describe('Password test', () => { id String @id @default(cuid()) x Int + @@allow('read', true) @@allow('create', x > 0) }`, { getPrismaOnly: true, output: './zen' } ); - const db = withPolicy(prisma, undefined, { loadPath: './zen' }); + const db = enhance(prisma, undefined, { loadPath: './zen' }); await expect( db.foo.create({ data: { x: 0 }, }) ).toBeRejectedByPolicy(); + await expect( + db.foo.create({ + data: { x: 1 }, + }) + ).toResolveTruthy(); + }); + + it('prisma module', async () => { + const { prisma, Prisma, modelMeta, policy } = await loadSchema( + ` + model Foo { + id String @id @default(cuid()) + x Int + + @@allow('read', true) + @@allow('create', x > 0) + }` + ); + + const db = enhance(prisma, undefined, { modelMeta, policy, prismaModule: Prisma }); + await expect( + db.foo.create({ + data: { x: 0 }, + }) + ).toBeRejectedByPolicy(); + await expect( + db.foo.create({ + data: { x: 1 }, + }) + ).toResolveTruthy(); }); it('overrides', async () => { @@ -45,7 +76,7 @@ describe('Password test', () => { { getPrismaOnly: true, output: './zen' } ); - const db = withPolicy(prisma, undefined, { + const db = enhance(prisma, undefined, { modelMeta: require(path.resolve('./zen/model-meta')).default, policy: require(path.resolve('./zen/policy')).default, }); From 1cff7f0513d8d1af8435e5f41a2b87ba3907a59e Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 19 Mar 2024 19:08:49 -0700 Subject: [PATCH 39/43] chore: bump version (#1161) --- 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 d61f79174..0bb55596e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.11.0", + "version": "1.11.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index 23dc11e99..ea192ff0b 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "1.11.0" +version = "1.11.1" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 09476b216..564af0351 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.11.0", + "version": "1.11.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 b6e146450..7e8fa9fdc 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.11.0", + "version": "1.11.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 207173636..33d41a01c 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "1.11.0", + "version": "1.11.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 a8b6f1d49..38c85462b 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.11.0", + "version": "1.11.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 376ad2e3b..9266a6a36 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.11.0", + "version": "1.11.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 69c3f5fda..51871526a 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.11.0", + "version": "1.11.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 b0a5a28c9..e9fddc330 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.11.0", + "version": "1.11.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5665262c1..8d582796e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.11.0", + "version": "1.11.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 249389e68..25ebd253b 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.11.0", + "version": "1.11.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 110fec6c0..5e3707444 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.11.0", + "version": "1.11.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index ea60177a5..699933197 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.11.0", + "version": "1.11.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 97e842f00..8e68dabc0 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.11.0", + "version": "1.11.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From fef6e83a36f451f671ac2b7db1bc06e2e29faf43 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 19 Mar 2024 20:29:37 -0700 Subject: [PATCH 40/43] fix: nested `createMany` with `skipDuplicates` option is not handled correctly (#1163) --- .../src/enhancements/policy/handler.ts | 5 +- .../tests/regression/issue-1162.test.ts | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/integration/tests/regression/issue-1162.test.ts diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 598d87881..322b0261e 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -735,7 +735,10 @@ export class PolicyProxyHandler implements Pr ) => { for (const item of enumerate(args.data)) { if (args.skipDuplicates) { - if (await this.hasDuplicatedUniqueConstraint(model, item, db)) { + // get a reversed query to include fields inherited from upstream mutation, + // it'll be merged with the create payload for unique constraint checking + const reversedQuery = this.utils.buildReversedQuery(context); + if (await this.hasDuplicatedUniqueConstraint(model, { ...reversedQuery, ...item }, db)) { if (this.shouldLogQuery) { this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`); } diff --git a/tests/integration/tests/regression/issue-1162.test.ts b/tests/integration/tests/regression/issue-1162.test.ts new file mode 100644 index 000000000..fd7f0dded --- /dev/null +++ b/tests/integration/tests/regression/issue-1162.test.ts @@ -0,0 +1,56 @@ +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 9f89c7ea76adfa73406843e3c2f222ea0bfcb969 Mon Sep 17 00:00:00 2001 From: Yiming Date: Wed, 20 Mar 2024 11:52:08 -0700 Subject: [PATCH 41/43] fix: additional fixes for unique constraint conflict detection (#1165) --- .../src/enhancements/policy/handler.ts | 71 +++++++++++++++++-- .../with-policy/deep-nested.test.ts | 56 ++++++++++++--- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index 322b0261e..ef48f7f38 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -537,7 +537,7 @@ export class PolicyProxyHandler implements Pr let createResult = await Promise.all( enumerate(args.data).map(async (item) => { if (args.skipDuplicates) { - if (await this.hasDuplicatedUniqueConstraint(model, item, db)) { + if (await this.hasDuplicatedUniqueConstraint(model, item, undefined, db)) { if (this.shouldLogQuery) { this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`); } @@ -565,23 +565,82 @@ export class PolicyProxyHandler implements Pr }; } - private async hasDuplicatedUniqueConstraint(model: string, createData: any, db: Record) { + private async hasDuplicatedUniqueConstraint( + model: string, + createData: any, + upstreamQuery: any, + db: Record + ) { // 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); + 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] }), {}); + // the unique filter used to check existence + const uniqueFilter: any = {}; + + // unique constraint fields not covered yet + const remainingConstraintFields = new Set(constraint.fields); + + // collect constraint fields from the create data + for (const [k, v] of Object.entries(createData)) { + if (v === undefined) { + continue; + } + + if (remainingConstraintFields.has(k)) { + uniqueFilter[k] = v; + remainingConstraintFields.delete(k); + } + } + + // collect constraint fields from the upstream query + if (upstreamQuery) { + for (const [k, v] of Object.entries(upstreamQuery)) { + if (v === undefined) { + continue; + } + + if (remainingConstraintFields.has(k)) { + uniqueFilter[k] = v; + remainingConstraintFields.delete(k); + continue; + } + + // check if the upstream query contains a relation field which covers + // a foreign key field constraint + + const fieldInfo = requireField(this.modelMeta, model, k); + if (!fieldInfo.isDataModel) { + // only care about relation fields + continue; + } + + // merge the upstream query into the unique filter + uniqueFilter[k] = v; + + // mark the corresponding foreign key fields as covered + const fkMapping = fieldInfo.foreignKeyMapping ?? {}; + for (const fk of Object.values(fkMapping)) { + remainingConstraintFields.delete(fk); + } + } + } + + if (remainingConstraintFields.size === 0) { + // all constraint fields set, check existence const existing = await this.utils.checkExistence(db, model, uniqueFilter); if (existing) { return true; } } } + return false; } @@ -737,8 +796,8 @@ export class PolicyProxyHandler implements Pr if (args.skipDuplicates) { // get a reversed query to include fields inherited from upstream mutation, // it'll be merged with the create payload for unique constraint checking - const reversedQuery = this.utils.buildReversedQuery(context); - if (await this.hasDuplicatedUniqueConstraint(model, { ...reversedQuery, ...item }, db)) { + const upstreamQuery = this.utils.buildReversedQuery(context); + if (await this.hasDuplicatedUniqueConstraint(model, item, upstreamQuery, db)) { if (this.shouldLogQuery) { this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`); } 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 ee8f16467..d80c3c311 100644 --- a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts +++ b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts @@ -52,6 +52,8 @@ describe('With Policy:deep nested', () => { m2 M2? @relation(fields: [m2Id], references: [id], onDelete: Cascade) m2Id Int? + @@unique([m2Id, value]) + @@allow('read', true) @@allow('create', value > 20) @@allow('update', value > 21) @@ -164,7 +166,7 @@ describe('With Policy:deep nested', () => { m4: { create: [ { id: 'm4-1', value: 22 }, - { id: 'm4-2', value: 22 }, + { id: 'm4-2', value: 23 }, ], }, }, @@ -190,11 +192,11 @@ describe('With Policy:deep nested', () => { connectOrCreate: [ { where: { id: 'm4-2' }, - create: { id: 'm4-new', value: 22 }, + create: { id: 'm4-new', value: 24 }, }, { where: { id: 'm4-3' }, - create: { id: 'm4-3', value: 23 }, + create: { id: 'm4-3', value: 25 }, }, ], }, @@ -327,7 +329,7 @@ describe('With Policy:deep nested', () => { await db.m4.create({ data: { id: 'm4-3', - value: 23, + value: 24, }, }); const r = await db.m1.update({ @@ -446,6 +448,19 @@ describe('With Policy:deep nested', () => { myId: '1', m2: { create: { + id: 1, + value: 2, + }, + }, + }, + }); + + await db.m1.create({ + data: { + myId: '2', + m2: { + create: { + id: 2, value: 2, }, }, @@ -483,9 +498,9 @@ describe('With Policy:deep nested', () => { createMany: { skipDuplicates: true, data: [ - { id: 'm4-1', value: 21 }, - { id: 'm4-1', value: 211 }, - { id: 'm4-2', value: 22 }, + { id: 'm4-1', value: 21 }, // should be created + { id: 'm4-1', value: 211 }, // should be skipped + { id: 'm4-2', value: 22 }, // should be created ], }, }, @@ -495,6 +510,29 @@ describe('With Policy:deep nested', () => { }); await expect(db.m4.findMany()).resolves.toHaveLength(2); + // createMany skip duplicate with compound unique involving fk + await db.m1.update({ + where: { myId: '2' }, + data: { + m2: { + update: { + m4: { + createMany: { + skipDuplicates: true, + data: [ + { id: 'm4-3', value: 21 }, // should be created + { id: 'm4-4', value: 21 }, // should be skipped + ], + }, + }, + }, + }, + }, + }); + const allM4 = await db.m4.findMany({ select: { value: true } }); + await expect(allM4).toHaveLength(3); + await expect(allM4).toEqual(expect.arrayContaining([{ value: 21 }, { value: 21 }, { value: 22 }])); + // updateMany, filtered out by policy await db.m1.update({ where: { myId: '1' }, @@ -556,7 +594,7 @@ describe('With Policy:deep nested', () => { }, }, }); - await expect(db.m4.findMany()).resolves.toHaveLength(2); + await expect(db.m4.findMany()).resolves.toHaveLength(3); // deleteMany, success await db.m1.update({ @@ -573,7 +611,7 @@ describe('With Policy:deep nested', () => { }, }, }); - await expect(db.m4.findMany()).resolves.toHaveLength(1); + await expect(db.m4.findMany()).resolves.toHaveLength(2); }); it('delete', async () => { From 7f0b91fed61f414b3ac63edd7ae970b0f92c05f3 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 21 Mar 2024 10:41:55 -0700 Subject: [PATCH 42/43] chore: setup tmp package cleanup (#1168) --- .../openapi/tests/openapi-restful.test.ts | 2 ++ .../plugins/openapi/tests/openapi-rpc.test.ts | 2 ++ .../tests/generator/expression-writer.test.ts | 2 ++ .../tests/generator/prisma-generator.test.ts | 2 ++ packages/schema/tests/utils.ts | 22 +++++++++++-------- packages/testtools/src/model.ts | 2 ++ packages/testtools/src/schema.ts | 2 ++ tests/integration/tests/cli/config.test.ts | 2 ++ tests/integration/tests/cli/format.test.ts | 3 +++ tests/integration/tests/cli/generate.test.ts | 2 ++ tests/integration/tests/cli/init.test.ts | 2 ++ tests/integration/tests/cli/plugins.test.ts | 2 ++ .../integration/tests/plugins/prisma.test.ts | 2 ++ 13 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/plugins/openapi/tests/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts index 9e84ad047..9997dd540 100644 --- a/packages/plugins/openapi/tests/openapi-restful.test.ts +++ b/packages/plugins/openapi/tests/openapi-restful.test.ts @@ -11,6 +11,8 @@ import * as tmp from 'tmp'; import YAML from 'yaml'; import generate from '../src'; +tmp.setGracefulCleanup(); + describe('Open API Plugin RESTful Tests', () => { it('run plugin', async () => { for (const specVersion of ['3.0.0', '3.1.0']) { diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts index 8e8e3a6ac..6873ab5ce 100644 --- a/packages/plugins/openapi/tests/openapi-rpc.test.ts +++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts @@ -11,6 +11,8 @@ import * as tmp from 'tmp'; import YAML from 'yaml'; import generate from '../src'; +tmp.setGracefulCleanup(); + describe('Open API Plugin RPC Tests', () => { it('run plugin', async () => { for (const specVersion of ['3.0.0', '3.1.0']) { diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index 7121c9589..13f69fd89 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -6,6 +6,8 @@ import { Project, VariableDeclarationKind } from 'ts-morph'; import { ExpressionWriter } from '../../src/plugins/access-policy/expression-writer'; import { loadModel } from '../utils'; +tmp.setGracefulCleanup(); + describe('Expression Writer Tests', () => { it('boolean literal', async () => { await check( diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 30a477026..0e8987703 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -8,6 +8,8 @@ import { loadDocument } from '../../src/cli/cli-util'; import PrismaSchemaGenerator from '../../src/plugins/prisma/schema-generator'; import { loadModel } from '../utils'; +tmp.setGracefulCleanup(); + describe('Prisma generator test', () => { it('datasource coverage', async () => { const model = await loadModel(` diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index 7369838f5..2746a3362 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -7,21 +7,25 @@ import { URI } from 'vscode-uri'; import { createZModelServices } from '../src/language-server/zmodel-module'; import { mergeBaseModel } from '../src/utils/ast-utils'; +tmp.setGracefulCleanup(); + type Errorish = Error | { message: string; stack?: string } | string; export class SchemaLoadingError extends Error { - cause: Errors + 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 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 }`); + super(`Schema error:\n${message}`); if (stack) { const shiftedStack = stack.split('\n').slice(1).join('\n'); - this.stack = shiftedStack + this.stack = shiftedStack; } - this.cause = errors + this.cause = errors; } } @@ -83,13 +87,13 @@ export async function loadModelWithError(content: string, verbose = false) { } export async function safelyLoadModel(content: string, validate = true, verbose = false) { - const [ result ] = await Promise.allSettled([ loadModel(content, validate, verbose) ]); + const [result] = await Promise.allSettled([loadModel(content, validate, verbose)]); - return result + return result; } export const errorLike = (msg: string) => ({ reason: { - message: expect.stringContaining(msg) + message: expect.stringContaining(msg), }, -}) +}); diff --git a/packages/testtools/src/model.ts b/packages/testtools/src/model.ts index 4be8a1613..310a2c019 100644 --- a/packages/testtools/src/model.ts +++ b/packages/testtools/src/model.ts @@ -7,6 +7,8 @@ import { URI } from 'vscode-uri'; import { createZModelServices } from 'zenstack/language-server/zmodel-module'; import { mergeBaseModel } from 'zenstack/utils/ast-utils'; +tmp.setGracefulCleanup(); + export class SchemaLoadingError extends Error { constructor(public readonly errors: string[]) { super('Schema error:\n' + errors.join('\n')); diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 5fef95299..a6885237f 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -24,6 +24,8 @@ import prismaPlugin from 'zenstack/plugins/prisma'; */ export const FILE_SPLITTER = '#FILE_SPLITTER#'; +tmp.setGracefulCleanup(); + export type FullDbClientContract = Record & { $on(eventType: any, callback: (event: any) => void): void; $use(cb: any): void; diff --git a/tests/integration/tests/cli/config.test.ts b/tests/integration/tests/cli/config.test.ts index f047889fd..096e86ca3 100644 --- a/tests/integration/tests/cli/config.test.ts +++ b/tests/integration/tests/cli/config.test.ts @@ -5,6 +5,8 @@ import * as fs from 'fs'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; +tmp.setGracefulCleanup(); + describe('CLI Config Tests', () => { let origDir: string; diff --git a/tests/integration/tests/cli/format.test.ts b/tests/integration/tests/cli/format.test.ts index 781d6c917..9d7b2a52b 100644 --- a/tests/integration/tests/cli/format.test.ts +++ b/tests/integration/tests/cli/format.test.ts @@ -1,6 +1,9 @@ import * as fs from 'fs'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; + +tmp.setGracefulCleanup(); + describe('CLI format test', () => { let origDir: string; diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 90f9e2311..5dd8ddf1c 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -8,6 +8,8 @@ import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; import { createNpmrc } from './share'; +tmp.setGracefulCleanup(); + describe('CLI generate command tests', () => { let origDir: string; const MODEL = ` diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts index ab6fcc747..0f9f69894 100644 --- a/tests/integration/tests/cli/init.test.ts +++ b/tests/integration/tests/cli/init.test.ts @@ -9,6 +9,8 @@ import { createProgram } from '../../../../packages/schema/src/cli'; import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; import { createNpmrc } from './share'; +tmp.setGracefulCleanup(); + // 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', () => { diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 6efb3bad4..5a8e44ae7 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -7,6 +7,8 @@ import * as path from 'path'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; +tmp.setGracefulCleanup(); + describe('CLI Plugins Tests', () => { let origDir: string; diff --git a/tests/integration/tests/plugins/prisma.test.ts b/tests/integration/tests/plugins/prisma.test.ts index 9e462f578..32d5a8e59 100644 --- a/tests/integration/tests/plugins/prisma.test.ts +++ b/tests/integration/tests/plugins/prisma.test.ts @@ -3,6 +3,8 @@ import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; +tmp.setGracefulCleanup(); + describe('Prisma plugin tests', () => { let origDir: string; From 1ae38c13d00eab8e6f5a4d2d717258053ceab032 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 21 Mar 2024 10:45:17 -0700 Subject: [PATCH 43/43] chore: add github workflow for updating sample repros on release (#1169) --- .github/workflows/update-samples.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/update-samples.yml diff --git a/.github/workflows/update-samples.yml b/.github/workflows/update-samples.yml new file mode 100644 index 000000000..4fd474843 --- /dev/null +++ b/.github/workflows/update-samples.yml @@ -0,0 +1,27 @@ +name: Update Samples + +on: + release: + types: [published] + +jobs: + dispatch: + runs-on: ubuntu-latest + + strategy: + matrix: + repo: + [ + 'zenstackhq/sample-todo-nextjs', + 'zenstackhq/sample-todo-nextjs-tanstack', + 'zenstackhq/sample-todo-trpc', + 'zenstackhq/sample-todo-sveltekit', + ] + + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ secrets.RELEASE_TRIGGER_TOKEN }} + repository: ${{ matrix.repo }} + event-type: zenstack-release