From 317ba8db9df0d11ddf9085b8b1f30f9e10c13d97 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Wed, 22 Mar 2023 10:55:26 +0000 Subject: [PATCH 1/7] fix: Support implicit many-to-many (#286) --- .../validator/datamodel-validator.ts | 21 +++++++++++-------- .../validation/datamodel-validation.test.ts | 17 +++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 0d8b5eeff..1468311c8 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -256,15 +256,18 @@ export default class DataModelValidator implements AstValidator { relationOwner = field; } } else { - [field, oppositeField].forEach((f) => { - if (!this.isSelfRelation(f, thisRelation.name)) { - accept( - 'error', - 'Field for one side of relation must carry @relation attribute with both "fields" and "references" fields', - { node: f } - ); - } - }); + // if both the field is array, then it's an implicit many-to-many relation + if (!(field.type.array && oppositeField.type.array)) { + [field, oppositeField].forEach((f) => { + if (!this.isSelfRelation(f, thisRelation.name)) { + accept( + 'error', + 'Field for one side of relation must carry @relation attribute with both "fields" and "references" fields', + { node: f } + ); + } + }); + } return; } diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 6e0c40b1d..76c38a3f1 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -257,6 +257,23 @@ describe('Data Model Validation Tests', () => { } `); + // many-to-many implicit + //https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations#implicit-many-to-many-relations + await loadModel(` + ${prelude} + model Post { + id Int @id @default(autoincrement()) + title String + categories Category[] + } + + model Category { + id Int @id @default(autoincrement()) + name String + posts Post[] + } + `); + // one-to-one incomplete expect( await loadModelWithError(` From 79144709b3bd56adf0a30f27b69426702980b95f Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 24 Mar 2023 11:00:00 +0800 Subject: [PATCH 2/7] feat: add support for filter operator functions (#289) --- .github/workflows/build-test.yml | 4 +- package.json | 2 +- packages/language/package.json | 2 +- packages/language/src/ast.ts | 7 +- packages/language/src/generated/ast.ts | 11 +- packages/language/src/generated/grammar.ts | 278 +++++++++++------- packages/language/src/zmodel.langium | 13 +- .../language/syntaxes/zmodel.tmLanguage.json | 2 +- packages/next/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/react/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- .../schema/src/language-server/constants.ts | 17 ++ .../validator/expression-validator.ts | 38 ++- .../function-invocation-validator.ts | 129 ++++++++ .../src/language-server/validator/utils.ts | 11 +- .../validator/zmodel-validator.ts | 17 +- .../src/language-server/zmodel-linker.ts | 1 + .../access-policy/expression-writer.ts | 219 +++++++++----- packages/schema/src/res/stdlib.zmodel | 48 +++ packages/schema/src/utils/ast-utils.ts | 18 ++ .../tests/generator/expression-writer.test.ts | 246 +++++++++++++--- ...rator.test.ts => zmodel-generator.test.ts} | 2 +- .../validation/attribute-validation.test.ts | 156 ++++++++++ packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- tests/integration/test-run/package-lock.json | 4 +- .../e2e/filter-function-coverage.test.ts | 63 ++++ 31 files changed, 1054 insertions(+), 252 deletions(-) create mode 100644 packages/schema/src/language-server/validator/function-invocation-validator.ts rename packages/schema/tests/generator/{code-generator.test.ts => zmodel-generator.test.ts} (98%) create mode 100644 tests/integration/tests/e2e/filter-function-coverage.test.ts diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 511bd9326..28481aecf 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,14 +8,12 @@ env: DO_NOT_TRACK: '1' on: - push: - branches: ['dev', 'main', 'canary'] pull_request: branches: ['dev', 'main', 'canary'] jobs: build-test: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 strategy: matrix: diff --git a/package.json b/package.json index edcfe5c82..b89c20425 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 75de13f1f..958842c96 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/ast.ts b/packages/language/src/ast.ts index b58e2c737..e5abfb58c 100644 --- a/packages/language/src/ast.ts +++ b/packages/language/src/ast.ts @@ -28,10 +28,11 @@ export const BinaryExprOperatorPriority: Record '<': 3, '>=': 3, '<=': 3, + in: 4, //CollectionPredicateExpr - '^': 4, - '?': 4, - '!': 4, + '^': 5, + '?': 5, + '!': 5, }; declare module './generated/ast' { diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 46d8e0e0d..711ecadae 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -150,7 +150,7 @@ export interface BinaryExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | DataSourceField | FieldInitializer | FunctionDecl | GeneratorField | MemberAccessExpr | PluginField | UnaryExpr; readonly $type: 'BinaryExpr'; left: Expression - operator: '!' | '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '?' | '^' | '||' + operator: '!' | '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '?' | '^' | 'in' | '||' right: Expression } @@ -318,6 +318,7 @@ export interface FunctionParam extends AstNode { readonly $container: DataModel | Enum | FunctionDecl; readonly $type: 'FunctionParam'; name: string + optional: boolean type: FunctionParamType } @@ -752,6 +753,14 @@ export class ZModelAstReflection extends AbstractAstReflection { ] }; } + case 'FunctionParam': { + return { + name: 'FunctionParam', + mandatory: [ + { name: 'optional', type: 'boolean' } + ] + }; + } case 'FunctionParamType': { return { name: 'FunctionParamType', diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 635c514e8..0292c7c88 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -64,28 +64,28 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@29" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@32" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@34" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "arguments": [] } @@ -107,7 +107,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -123,7 +123,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -167,7 +167,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -179,7 +179,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -237,7 +237,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -253,7 +253,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -297,7 +297,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -309,7 +309,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -360,7 +360,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -376,7 +376,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -420,7 +420,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -432,7 +432,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -480,7 +480,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@23" }, "arguments": [] }, @@ -504,21 +504,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@53" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] } @@ -605,7 +605,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] } @@ -627,7 +627,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "arguments": [] } @@ -657,7 +657,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] }, @@ -802,7 +802,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@33" + "$ref": "#/rules@34" }, "deprecatedSyntax": false } @@ -814,7 +814,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@27" }, "arguments": [], "cardinality": "?" @@ -881,7 +881,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@24" }, "arguments": [] }, @@ -911,7 +911,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@29" + "$ref": "#/rules@30" }, "deprecatedSyntax": false } @@ -1015,7 +1015,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel }, { "$type": "ParserRule", - "name": "ComparisonExpr", + "name": "InExpr", "inferredType": { "$type": "InferredType", "name": "Expression" @@ -1030,6 +1030,68 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel }, "arguments": [] }, + { + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "BinaryExpr" + }, + "feature": "left", + "operator": "=" + }, + { + "$type": "Assignment", + "feature": "operator", + "operator": "=", + "terminal": { + "$type": "Keyword", + "value": "in" + } + }, + { + "$type": "Assignment", + "feature": "right", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "ComparisonExpr", + "inferredType": { + "$type": "InferredType", + "name": "Expression" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@20" + }, + "arguments": [] + }, { "$type": "Group", "elements": [ @@ -1075,7 +1137,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@20" }, "arguments": [] } @@ -1105,7 +1167,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@21" }, "arguments": [] }, @@ -1146,7 +1208,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@21" }, "arguments": [] } @@ -1176,7 +1238,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -1217,7 +1279,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@22" }, "arguments": [] } @@ -1316,7 +1378,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1349,7 +1411,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@26" }, "arguments": [] } @@ -1368,7 +1430,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@26" }, "arguments": [] } @@ -1410,7 +1472,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1454,7 +1516,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@28" }, "arguments": [] } @@ -1473,7 +1535,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@28" }, "arguments": [] } @@ -1505,7 +1567,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1551,7 +1613,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1568,7 +1630,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1587,7 +1649,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@30" }, "arguments": [] } @@ -1599,7 +1661,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" }, "arguments": [] } @@ -1633,7 +1695,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1646,7 +1708,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1658,7 +1720,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@31" }, "arguments": [] } @@ -1670,7 +1732,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" }, "arguments": [] }, @@ -1701,7 +1763,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -1718,7 +1780,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] }, @@ -1778,7 +1840,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1795,7 +1857,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1814,7 +1876,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@33" }, "arguments": [] } @@ -1826,7 +1888,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" }, "arguments": [] } @@ -1860,7 +1922,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1873,7 +1935,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1885,7 +1947,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" }, "arguments": [] }, @@ -1909,7 +1971,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -1925,7 +1987,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -1944,7 +2006,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@35" }, "arguments": [] } @@ -1963,7 +2025,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@35" }, "arguments": [] } @@ -1989,7 +2051,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@36" }, "arguments": [] } @@ -2033,7 +2095,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -2045,7 +2107,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2061,10 +2123,20 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@36" }, "arguments": [] } + }, + { + "$type": "Assignment", + "feature": "optional", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "?" + }, + "cardinality": "?" } ] }, @@ -2091,7 +2163,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2148,7 +2220,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] }, @@ -2165,14 +2237,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2204,7 +2276,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -2231,7 +2303,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -2258,7 +2330,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -2281,21 +2353,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2317,7 +2389,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -2333,7 +2405,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2352,7 +2424,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2371,7 +2443,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2393,7 +2465,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@47" }, "arguments": [] }, @@ -2417,7 +2489,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -2439,7 +2511,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2455,7 +2527,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@44" }, "arguments": [] } @@ -2488,7 +2560,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2519,7 +2591,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] }, @@ -2579,12 +2651,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] }, @@ -2601,7 +2673,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [], "cardinality": "?" @@ -2631,7 +2703,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [], "cardinality": "*" @@ -2643,12 +2715,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] }, @@ -2665,7 +2737,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [], "cardinality": "?" @@ -2699,12 +2771,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "arguments": [] }, @@ -2721,7 +2793,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [], "cardinality": "?" @@ -2756,7 +2828,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2775,7 +2847,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2807,7 +2879,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -3062,7 +3134,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "AtomType", "refType": { - "$ref": "#/rules@34" + "$ref": "#/rules@35" }, "isArray": false, "isRef": false @@ -3070,7 +3142,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "AtomType", "refType": { - "$ref": "#/rules@29" + "$ref": "#/rules@30" }, "isArray": false, "isRef": false @@ -3078,7 +3150,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "AtomType", "refType": { - "$ref": "#/rules@32" + "$ref": "#/rules@33" }, "isArray": false, "isRef": false @@ -3092,7 +3164,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "AtomType", "refType": { - "$ref": "#/rules@28" + "$ref": "#/rules@29" }, "isArray": false, "isRef": false @@ -3100,7 +3172,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "AtomType", "refType": { - "$ref": "#/rules@31" + "$ref": "#/rules@32" }, "isArray": false, "isRef": false diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 95da83dbd..40566e697 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -94,13 +94,20 @@ CollectionPredicateExpr infers Expression: // right=MultDivExpr // )*; -ComparisonExpr infers Expression: +InExpr infers Expression: CollectionPredicateExpr ( {infer BinaryExpr.left=current} - operator=('>'|'<'|'>='|'<=') + operator=('in') right=CollectionPredicateExpr )*; +ComparisonExpr infers Expression: + InExpr ( + {infer BinaryExpr.left=current} + operator=('>'|'<'|'>='|'<=') + right=InExpr + )*; + EqualityExpr infers Expression: ComparisonExpr ( {infer BinaryExpr.left=current} @@ -174,7 +181,7 @@ FunctionDecl: TRIPLE_SLASH_COMMENT* 'function' name=ID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; FunctionParam: - TRIPLE_SLASH_COMMENT* name=ID ':' type=FunctionParamType; + TRIPLE_SLASH_COMMENT* name=ID ':' type=FunctionParamType (optional?='?')?; FunctionParamType: (type=ExpressionType | reference=[TypeDeclaration]) (array?='[' ']')?; diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index 0230e8ab2..f33d54cf5 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|attribute|BigInt|Boolean|Bytes|ContextType|datasource|DateTime|Decimal|Desc|enum|FieldReference|Float|function|generator|Int|Json|model|Null|Object|plugin|sort|String|TransitiveFieldReference)\\b" + "match": "\\b(Any|Asc|attribute|BigInt|Boolean|Bytes|ContextType|datasource|DateTime|Decimal|Desc|enum|FieldReference|Float|function|generator|in|Int|Json|model|Null|Object|plugin|sort|String|TransitiveFieldReference)\\b" }, { "name": "string.quoted.double.zmodel", diff --git a/packages/next/package.json b/packages/next/package.json index 6a189d0fb..92acef8fb 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 62fc4a242..6e97fa57c 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.0.0-alpha.81", + "version": "1.0.0-alpha.82", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index 1ab246060..36bc89aaf 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index edee7573c..362681501 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.0.0-alpha.81", + "version": "1.0.0-alpha.82", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index e45798a46..37586726a 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "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 4e8378105..0d57272d3 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/language-server/constants.ts b/packages/schema/src/language-server/constants.ts index 448a2fae1..9c62f2882 100644 --- a/packages/schema/src/language-server/constants.ts +++ b/packages/schema/src/language-server/constants.ts @@ -18,6 +18,23 @@ export const STD_LIB_MODULE_NAME = 'stdlib.zmodel'; */ export const PLUGIN_MODULE_NAME = 'plugin.zmodel'; +/** + * Validation issues + */ export enum IssueCodes { MissingOppositeRelation = 'miss-opposite-relation', } + +/** + * Filter operation function names (mapped to Prisma filter operators) + */ +export const FILTER_OPERATOR_FUNCTIONS = [ + 'contains', + 'search', + 'startsWith', + 'endsWith', + 'has', + 'hasEvery', + 'hasSome', + 'isEmpty', +]; diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 2f9ca1cf2..ea15766db 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -1,6 +1,6 @@ -import { Expression, isBinaryExpr } from '@zenstackhq/language/ast'; +import { BinaryExpr, Expression, isArrayExpr, isBinaryExpr, isEnum, isLiteralExpr } from '@zenstackhq/language/ast'; import { ValidationAcceptor } from 'langium'; -import { isAuthInvocation } from '../../utils/ast-utils'; +import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '../../utils/ast-utils'; import { AstValidator } from '../types'; /** @@ -8,6 +8,7 @@ import { AstValidator } from '../types'; */ export default class ExpressionValidator implements AstValidator { validate(expr: Expression, accept: ValidationAcceptor): void { + // deal with a few cases where reference resolution fail silently if (!expr.$resolvedType) { if (isAuthInvocation(expr)) { // check was done at link time @@ -20,6 +21,39 @@ export default class ExpressionValidator implements AstValidator { }); } } + + // extra validations by expression type + switch (expr.$type) { + case 'BinaryExpr': + this.validateBinaryExpr(expr, accept); + break; + } + } + + private validateBinaryExpr(expr: BinaryExpr, accept: ValidationAcceptor) { + switch (expr.operator) { + case 'in': { + if (!isDataModelFieldReference(expr.left)) { + accept('error', 'left operand of "in" must be a field reference', { node: expr.left }); + } + + if (typeof expr.left.$resolvedType?.decl !== 'string' && !isEnum(expr.left.$resolvedType?.decl)) { + accept('error', 'left operand of "in" must be of scalar type', { node: expr.left }); + } + + if ( + !( + isArrayExpr(expr.right) && + expr.right.items.every((item) => isLiteralExpr(item) || isEnumFieldReference(item)) + ) + ) { + accept('error', 'right operand of "in" must be an array of literals or enum values', { + node: expr.right, + }); + } + break; + } + } } private isCollectionPredicate(expr: Expression) { diff --git a/packages/schema/src/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts new file mode 100644 index 000000000..e5a5c76f0 --- /dev/null +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -0,0 +1,129 @@ +import { + Argument, + Expression, + FunctionDecl, + FunctionParam, + InvocationExpr, + isArrayExpr, + isLiteralExpr, +} from '@zenstackhq/language/ast'; +import { ValidationAcceptor } from 'langium'; +import { isDataModelFieldReference, isEnumFieldReference } from '../../utils/ast-utils'; +import { FILTER_OPERATOR_FUNCTIONS } from '../constants'; +import { AstValidator } from '../types'; +import { isFromStdlib } from '../utils'; +import { typeAssignable } from './utils'; + +/** + * InvocationExpr validation + */ +export default class FunctionInvocationValidator implements AstValidator { + validate(expr: InvocationExpr, accept: ValidationAcceptor): void { + const funcDecl = expr.function.ref; + if (!funcDecl) { + accept('error', 'function cannot be resolved', { node: expr }); + return; + } + + if (!this.validateArgs(funcDecl, expr.args, accept)) { + return; + } + + if (isFromStdlib(funcDecl)) { + // validate standard library functions + + if (FILTER_OPERATOR_FUNCTIONS.includes(funcDecl.name)) { + // filter operation functions + + // first argument must refer to a model field + const firstArg = expr.args?.[0]?.value; + if (firstArg) { + if (!isDataModelFieldReference(firstArg)) { + accept('error', 'first argument must be a field reference', { node: firstArg }); + } + } + + // second argument must be a literal or array of literal + const secondArg = expr.args?.[1]?.value; + if ( + secondArg && + // literal + !isLiteralExpr(secondArg) && + // enum field + !isEnumFieldReference(secondArg) && + // array of literal/enum + !( + isArrayExpr(secondArg) && + secondArg.items.every((item) => isLiteralExpr(item) || isEnumFieldReference(item)) + ) + ) { + accept('error', 'second argument must be a literal, an enum, or an array of them', { + node: secondArg, + }); + } + } + } + } + + private validateArgs(funcDecl: FunctionDecl, args: Argument[], accept: ValidationAcceptor) { + let success = true; + for (let i = 0; i < funcDecl.params.length; i++) { + const param = funcDecl.params[i]; + const arg = args[i]; + if (!arg) { + if (!param.optional) { + accept('error', `missing argument for parameter "${param.name}"`, { node: funcDecl }); + success = false; + } + } else { + if (!this.validateInvocationArg(arg, param, accept)) { + success = false; + } + } + } + // TODO: do we need to complain for extra arguments? + return success; + } + + private validateInvocationArg(arg: Argument, param: FunctionParam, accept: ValidationAcceptor) { + const argResolvedType = arg?.value?.$resolvedType; + if (!argResolvedType) { + accept('error', 'argument type cannot be resolved', { node: arg }); + return false; + } + + const dstType = param.type.type; + if (!dstType) { + accept('error', 'parameter type cannot be resolved', { node: param }); + return false; + } + + const dstIsArray = param.type.array; + const dstRef = param.type.reference; + + if (dstType === 'Any' && !dstIsArray) { + // scalar 'any' can be assigned with anything + return true; + } + + if (typeof argResolvedType.decl === 'string') { + // scalar type + if (!typeAssignable(dstType, argResolvedType.decl, arg.value) || dstIsArray !== argResolvedType.array) { + accept('error', `argument is not assignable to parameter`, { + node: arg, + }); + return false; + } + } else { + // enum or model type + if ((dstRef?.ref !== argResolvedType.decl && dstType !== 'Any') || dstIsArray !== argResolvedType.array) { + accept('error', `argument is not assignable to parameter`, { + node: arg, + }); + return false; + } + } + + return true; + } +} diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 163bef002..7cdbc9037 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -122,6 +122,10 @@ export function assignableToAttributeParam( let dstIsArray = param.type.array; const dstRef = param.type.reference; + if (dstType === 'Any' && !dstIsArray) { + return true; + } + // destination is field reference or transitive field reference, check if // argument is reference or array or reference if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { @@ -168,13 +172,10 @@ export function assignableToAttributeParam( } } - return ( - typeAssignable(dstType, argResolvedType.decl, arg.value) && - (dstType === 'Any' || dstIsArray === argResolvedType.array) - ); + return typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array; } else { // reference type - return dstRef?.ref === argResolvedType.decl && dstIsArray === argResolvedType.array; + return (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array; } } diff --git a/packages/schema/src/language-server/validator/zmodel-validator.ts b/packages/schema/src/language-server/validator/zmodel-validator.ts index 077add682..4ea4c59dd 100644 --- a/packages/schema/src/language-server/validator/zmodel-validator.ts +++ b/packages/schema/src/language-server/validator/zmodel-validator.ts @@ -1,5 +1,14 @@ import { AstNode, LangiumDocument, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium'; -import { Attribute, DataModel, DataSource, Enum, Expression, Model, ZModelAstType } from '@zenstackhq/language/ast'; +import { + Attribute, + DataModel, + DataSource, + Enum, + Expression, + InvocationExpr, + Model, + ZModelAstType, +} from '@zenstackhq/language/ast'; import type { ZModelServices } from '../zmodel-module'; import SchemaValidator from './schema-validator'; import DataSourceValidator from './datasource-validator'; @@ -7,6 +16,7 @@ import DataModelValidator from './datamodel-validator'; import AttributeValidator from './attribute-validator'; import EnumValidator from './enum-validator'; import ExpressionValidator from './expression-validator'; +import FunctionInvocationValidator from './function-invocation-validator'; /** * Registry for validation checks. @@ -22,6 +32,7 @@ export class ZModelValidationRegistry extends ValidationRegistry { Enum: validator.checkEnum, Attribute: validator.checkAttribute, Expression: validator.checkExpression, + InvocationExpr: validator.checkFunctionInvocation, }; this.register(checks, validator); } @@ -68,4 +79,8 @@ export class ZModelValidator { checkExpression(node: Expression, accept: ValidationAcceptor): void { this.shouldCheck(node) && new ExpressionValidator().validate(node, accept); } + + checkFunctionInvocation(node: InvocationExpr, accept: ValidationAcceptor): void { + this.shouldCheck(node) && new FunctionInvocationValidator().validate(node, accept); + } } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index cbeffab2b..63ca3448a 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -184,6 +184,7 @@ export class ZModelLinker extends DefaultLinker { case '!=': case '&&': case '||': + case 'in': this.resolve(node.left, document, extraScopes); this.resolve(node.right, document, extraScopes); this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index e2449326f..e80fcf43c 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -2,6 +2,7 @@ import { BinaryExpr, DataModel, Expression, + InvocationExpr, isDataModel, isDataModelField, isEnumField, @@ -13,14 +14,30 @@ import { ReferenceExpr, UnaryExpr, } from '@zenstackhq/language/ast'; -import { GUARD_FIELD_NAME, PluginError } from '@zenstackhq/sdk'; +import { getLiteral, GUARD_FIELD_NAME, PluginError } from '@zenstackhq/sdk'; import { CodeBlockWriter } from 'ts-morph'; +import { FILTER_OPERATOR_FUNCTIONS } from '../../language-server/constants'; import { getIdField, isAuthInvocation } from '../../utils/ast-utils'; import TypeScriptExpressionTransformer from './typescript-expression-transformer'; import { isFutureExpr } from './utils'; type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<='; +type FilterOperators = + | 'is' + | 'some' + | 'every' + | 'none' + | 'in' + | 'contains' + | 'search' + | 'startsWith' + | 'endsWith' + | 'has' + | 'hasEvery' + | 'hasSome' + | 'isEmpty'; + /** * Utility for writing ZModel expression as Prisma query argument objects into a ts-morph writer */ @@ -61,6 +78,10 @@ export class ExpressionWriter { this.writeMemberAccess(expr as MemberAccessExpr); break; + case InvocationExpr: + this.writeInvocation(expr as InvocationExpr); + break; + default: throw new Error(`Not implemented: ${expr.$type}`); } @@ -79,15 +100,11 @@ export class ExpressionWriter { private writeMemberAccess(expr: MemberAccessExpr) { this.block(() => { // must be a boolean member - this.writeFieldCondition( - expr.operand, - () => { - this.block(() => { - this.writer.write(`${expr.member.ref?.name}: true`); - }); - }, - 'is' - ); + this.writeFieldCondition(expr.operand, () => { + this.block(() => { + this.writer.write(`${expr.member.ref?.name}: true`); + }); + }); }); } @@ -118,6 +135,10 @@ export class ExpressionWriter { this.writeComparison(expr, expr.operator); break; + case 'in': + this.writeIn(expr); + break; + case '?': case '!': case '^': @@ -126,6 +147,18 @@ export class ExpressionWriter { } } + private writeIn(expr: BinaryExpr) { + this.block(() => { + this.writeFieldCondition( + expr.left, + () => { + this.plain(expr.right); + }, + 'in' + ); + }); + } + private writeCollectionPredicate(expr: BinaryExpr, operator: string) { this.block(() => { this.writeFieldCondition( @@ -217,44 +250,40 @@ export class ExpressionWriter { } this.block(() => { - this.writeFieldCondition( - fieldAccess, - () => { - this.block( - () => { - const dataModel = this.isModelTyped(fieldAccess); - if (dataModel) { - const idField = getIdField(dataModel); - if (!idField) { - throw new PluginError(`Data model ${dataModel.name} does not have an id field`); - } - // comparing with an object, convert to "id" comparison instead - this.writer.write(`${idField.name}: `); - this.block(() => { - this.writeOperator(operator, () => { - // operand ? operand.field : null - this.writer.write('('); - this.plain(operand); - this.writer.write(' ? '); - this.plain(operand); - this.writer.write(`.${idField.name}`); - this.writer.write(' : null'); - this.writer.write(')'); - }); - }); - } else { + this.writeFieldCondition(fieldAccess, () => { + this.block( + () => { + const dataModel = this.isModelTyped(fieldAccess); + if (dataModel) { + const idField = getIdField(dataModel); + if (!idField) { + throw new PluginError(`Data model ${dataModel.name} does not have an id field`); + } + // comparing with an object, convert to "id" comparison instead + this.writer.write(`${idField.name}: `); + this.block(() => { this.writeOperator(operator, () => { + // operand ? operand.field : null + this.writer.write('('); this.plain(operand); + this.writer.write(' ? '); + this.plain(operand); + this.writer.write(`.${idField.name}`); + this.writer.write(' : null'); + this.writer.write(')'); }); - } - }, - // "this" expression is compiled away (to .id access), so we should - // avoid generating a new layer - !isThisExpr(fieldAccess) - ); - }, - 'is' - ); + }); + } else { + this.writeOperator(operator, () => { + this.plain(operand); + }); + } + }, + // "this" expression is compiled away (to .id access), so we should + // avoid generating a new layer + !isThisExpr(fieldAccess) + ); + }); }); } @@ -278,7 +307,8 @@ export class ExpressionWriter { private writeFieldCondition( fieldAccess: Expression, writeCondition: () => void, - relationOp: 'is' | 'some' | 'every' | 'none' + filterOp?: FilterOperators, + extraArgs?: Record ) { let selector: string | undefined; let operand: Expression | undefined; @@ -305,43 +335,37 @@ export class ExpressionWriter { throw new PluginError(`Failed to write FieldAccess expression`); } - if (operand) { - // member access expression - this.writeFieldCondition( - operand, - () => { - this.block( - () => { - this.writer.write(selector + ': '); - if (this.isModelTyped(fieldAccess)) { - // expression is resolved to a model, generate relation query - this.block(() => { - this.writer.write(`${relationOp}: `); - writeCondition(); - }); - } else { - // generate plain query - writeCondition(); - } - }, - // if operand is "this", it doesn't really generate a new layer of query, - // so we should avoid generating a new block - !isThisExpr(operand) - ); - }, - 'is' - ); - } else if (this.isModelTyped(fieldAccess)) { - // reference resolved to a model, generate relation query + const writerFilterOutput = () => { this.writer.write(selector + ': '); - this.block(() => { - this.writer.write(`${relationOp}: `); + if (filterOp) { + this.block(() => { + this.writer.write(`${filterOp}: `); + writeCondition(); + + if (extraArgs) { + for (const [k, v] of Object.entries(extraArgs)) { + this.writer.write(`,\n${k}: `); + this.plain(v); + } + } + }); + } else { writeCondition(); + } + }; + + if (operand) { + // member access expression + this.writeFieldCondition(operand, () => { + this.block( + writerFilterOutput, + // if operand is "this", it doesn't really generate a new layer of query, + // so we should avoid generating a new block + !isThisExpr(operand) + ); }); } else { - // generate a plain query - this.writer.write(selector + ': '); - writeCondition(); + writerFilterOutput(); } } @@ -414,4 +438,41 @@ export class ExpressionWriter { }); }); } + + private writeInvocation(expr: InvocationExpr) { + const funcDecl = expr.function.ref; + if (!funcDecl) { + throw new PluginError(`Failed to resolve function declaration`); + } + + if (FILTER_OPERATOR_FUNCTIONS.includes(funcDecl.name)) { + let valueArg = expr.args[1]?.value; + + // isEmpty function is zero arity, it's mapped to a boolean literal + if (funcDecl.name === 'isEmpty') { + valueArg = { $type: LiteralExpr, value: true } as LiteralExpr; + } + + // contains function has a 3rd argument that indicates whether the comparison should be case-insensitive + let extraArgs: Record | undefined = undefined; + if (funcDecl.name === 'contains') { + if (getLiteral(expr.args[2]?.value) === true) { + extraArgs = { mode: { $type: LiteralExpr, value: 'insensitive' } as LiteralExpr }; + } + } + + this.block(() => { + this.writeFieldCondition( + expr.args[0].value, + () => { + this.plain(valueArg); + }, + funcDecl.name as FilterOperators, + extraArgs + ); + }); + } else { + throw new PluginError(`Unsupported function ${funcDecl.name}`); + } + } } diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 984f0c1d7..f76ef7251 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -98,6 +98,54 @@ function dbgenerated(expr: String): Any { function future(): Any { } +/* + * If the field value contains the search string + */ +function contains(field: String, search: String, caseSensitive: Boolean?): Boolean { +} + +/* + * If the field value matches the search condition with [full-text-search](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). Need to enable "fullTextSearch" preview feature to use. + */ +function search(field: String, search: String): Boolean { +} + +/* + * If the field value starts with the search string + */ +function startsWith(field: String, search: String): Boolean { +} + +/* + * If the field value ends with the search string + */ +function endsWith(field: String, search: String): Boolean { +} + +/* + * If the field value (a list) has the given search value + */ +function has(field: Any[], search: Any): Boolean { +} + +/* + * If the field value (a list) has every element of the search list + */ +function hasEvery(field: Any[], search: Any[]): Boolean { +} + +/* + * If the field value (a list) has at least one element of the search list + */ +function hasSome(field: Any[], search: Any[]): Boolean { +} + +/* + * If the field value (a list) is empty + */ +function isEmpty(field: Any[]): Boolean { +} + /** * Marks an attribute to be only applicable to certain field types. */ diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index ebde2e6c8..7452d50fa 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -3,7 +3,11 @@ import { DataModelAttribute, Expression, isDataModel, + isDataModelField, + isEnumField, isInvocationExpr, + isMemberAccessExpr, + isReferenceExpr, Model, } from '@zenstackhq/language/ast'; import { PolicyOperationKind } from '@zenstackhq/runtime'; @@ -103,3 +107,17 @@ export function getIdField(dataModel: DataModel) { export function isAuthInvocation(expr: Expression) { return isInvocationExpr(expr) && expr.function.ref?.name === 'auth' && isFromStdlib(expr.function.ref); } + +export function isEnumFieldReference(expr: Expression) { + return isReferenceExpr(expr) && isEnumField(expr.target.ref); +} + +export function isDataModelFieldReference(expr: Expression): boolean { + if (isReferenceExpr(expr)) { + return isDataModelField(expr.target.ref); + } else if (isMemberAccessExpr(expr)) { + return true; + } else { + return false; + } +} diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index 79e22174f..8476418b0 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -322,10 +322,8 @@ describe('Expression Writer Tests', () => { (model) => model.attributes[0].args[1].value, `{ foo: { - is: { - x : { - lte: 0 - } + x : { + lte: 0 } } }` @@ -351,10 +349,8 @@ describe('Expression Writer Tests', () => { NOT: { foo: { - is: { - x : { - gt: 0 - } + x : { + gt: 0 } } } @@ -380,9 +376,7 @@ describe('Expression Writer Tests', () => { `{ NOT: { foo: { - is: { - x: true - } + x: true } } }` @@ -413,13 +407,9 @@ describe('Expression Writer Tests', () => { (model) => model.attributes[0].args[1].value, `{ foo: { - is: { - bar: { - is: { - x : { - lte: 0 - } - } + bar: { + x : { + lte: 0 } } } @@ -534,12 +524,10 @@ describe('Expression Writer Tests', () => { (model) => model.attributes[0].args[1].value, `{ foo: { - is: { - bars: { - some: { - x: { - lte: 0 - } + bars: { + some: { + x: { + lte: 0 } } } @@ -594,10 +582,8 @@ describe('Expression Writer Tests', () => { { zenstack_guard : false } : { owner: { - is: { - id: { - equals: (user ? user.id : null) - } + id: { + equals: (user ? user.id : null) } } } @@ -623,11 +609,9 @@ describe('Expression Writer Tests', () => { { zenstack_guard : false } : { owner: { - is: { - id: { - not: { - equals: (user ? user.id : null) - } + id: { + not: { + equals: (user ? user.id : null) } } } @@ -653,15 +637,203 @@ describe('Expression Writer Tests', () => { { zenstack_guard : false } : { owner: { - is: { - id: { - equals: (user ? user.id : null) - } + id: { + equals: (user ? user.id : null) } } }` ); }); + + it('filter operators', async () => { + await check( + ` + enum Role { + USER + ADMIN + } + model Test { + id String @id + role Role + @@allow('all', role in [USER, ADMIN]) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + role: { in: [Role.USER, Role.ADMIN] } + } + ` + ); + + await check( + ` + model Test { + id String @id + value String + @@allow('all', contains(value, 'foo')) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + value: { contains: 'foo' } + } + ` + ); + + await check( + ` + model Test { + id String @id + value String + @@allow('all', contains(value, 'foo', true)) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + value: { contains: 'foo', mode: 'insensitive' } + } + ` + ); + + await check( + ` + model Test { + id String @id + value String + @@allow('all', contains(value, 'foo', false)) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + value: { contains: 'foo' } + } + ` + ); + + await check( + ` + model Foo { + id String @id + value String + test Test @relation(fields: [testId], references: [id]) + testId String @unique + } + model Test { + id String @id + foo Foo? + @@allow('all', search(foo.value, 'foo')) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + foo: { + value: { search: 'foo' } + } + } + ` + ); + + await check( + ` + model Test { + id String @id + value String + @@allow('all', startsWith(value, 'foo') && endsWith(value, 'bar')) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + AND: [ { value: { startsWith: 'foo' } }, { value: { endsWith: 'bar' } } ] + } + ` + ); + + await check( + ` + model Test { + id String @id + value String + @@allow('all', !startsWith(value, 'foo')) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + NOT: { value: { startsWith: 'foo' } } + } + ` + ); + + await check( + ` + model Test { + id String @id + values Int[] + @@allow('all', has(values, 1)) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + values: { has: 1 } + } + ` + ); + + await check( + ` + model Test { + id String @id + values Int[] + @@allow('all', hasSome(values, [1, 2])) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + values: { hasSome: [1, 2] } + } + ` + ); + + await check( + ` + model Test { + id String @id + values Int[] + @@allow('all', hasEvery(values, [1, 2])) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + values: { hasEvery: [1, 2] } + } + ` + ); + + await check( + ` + model Test { + id String @id + values Int[] + @@allow('all', isEmpty(values)) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + values: { isEmpty: true } + } + ` + ); + }); }); async function check(schema: string, getExpr: (model: DataModel) => Expression, expected: string) { diff --git a/packages/schema/tests/generator/code-generator.test.ts b/packages/schema/tests/generator/zmodel-generator.test.ts similarity index 98% rename from packages/schema/tests/generator/code-generator.test.ts rename to packages/schema/tests/generator/zmodel-generator.test.ts index 6df2144bb..91ddacca2 100644 --- a/packages/schema/tests/generator/code-generator.test.ts +++ b/packages/schema/tests/generator/zmodel-generator.test.ts @@ -2,7 +2,7 @@ import { loadModel } from '../utils'; import ZModelCodeGenerator from '../../src/plugins/prisma/zmodel-code-generator'; import { DataModel, DataModelAttribute, DataModelFieldAttribute } from '@zenstackhq/language/ast'; -describe('Code Generator Tests', () => { +describe('ZModel Generator Tests', () => { const generator = new ZModelCodeGenerator(); function checkAttribute(ast: DataModelAttribute | DataModelFieldAttribute, expected: string) { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index f93cccec1..d541d43a9 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -366,6 +366,162 @@ describe('Attribute tests', () => { ).toContain(`Value is not assignable to parameter`); }); + it('filter function check', async () => { + await loadModel(` + ${prelude} + enum E { + E1 + E2 + } + + model N { + id String @id + e E + es E[] + s String + i Int + m M @relation(fields: [mId], references: [id]) + mId String @unique + } + + model M { + id String @id + s String + e E + es E[] + n N? + + @@allow('all', e in [E1, E2]) + @@allow('all', contains(s, 'a')) + @@allow('all', contains(s, 'a', true)) + @@allow('all', search(s, 'a')) + @@allow('all', startsWith(s, 'a')) + @@allow('all', endsWith(s, 'a')) + @@allow('all', has(es, E1)) + @@allow('all', hasSome(es, [E1])) + @@allow('all', hasEvery(es, [E1])) + @@allow('all', isEmpty(es)) + + @@allow('all', n.e in [E1, E2]) + @@allow('all', n.i in [1, 2]) + @@allow('all', contains(n.s, 'a')) + @@allow('all', contains(n.s, 'a', true)) + @@allow('all', search(n.s, 'a')) + @@allow('all', startsWith(n.s, 'a')) + @@allow('all', endsWith(n.s, 'a')) + @@allow('all', has(n.es, E1)) + @@allow('all', hasSome(n.es, [E1])) + @@allow('all', hasEvery(n.es, [E1])) + @@allow('all', isEmpty(n.es)) + } + `); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + s String + @@allow('all', contains(s)) + } + `) + ).toContain('missing argument for parameter "search"'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + s String + @@allow('all', contains('a', s)) + } + `) + ).toContain('first argument must be a field reference'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + s String + s1 String + @@allow('all', contains(s, s1)) + } + `) + ).toContain('second argument must be a literal, an enum, or an array of them'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + i Int + @@allow('all', contains(i, 1)) + } + `) + ).toContain('argument is not assignable to parameter'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + i Int[] + @@allow('all', 1 in i) + } + `) + ).toContain('left operand of "in" must be a field reference'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + i Int + @@allow('all', i in 1) + } + `) + ).toContain('right operand of "in" must be an array of literals or enum values'); + + 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? + @@allow('all', n in [1]) + } + `) + ).toContain('left operand of "in" must be of scalar type'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + x Int + @@allow('all', has(x, 1)) + } + `) + ).toContain('argument is not assignable to parameter'); + + expect( + await loadModelWithError(` + ${prelude} + model M { + id String @id + x Int[] + @@allow('all', hasSome(x, 1)) + } + `) + ).toContain('argument is not assignable to parameter'); + }); + it('auth function check', async () => { expect( await loadModelWithError(` diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3d718bd59..0881a9408 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 5053e2c9c..066821294 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "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 7e2b6e85a..6202899b1 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 65febe9d1..3f644e59b 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -158,7 +158,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.81", + "version": "1.0.0-alpha.82", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/tests/integration/tests/e2e/filter-function-coverage.test.ts b/tests/integration/tests/e2e/filter-function-coverage.test.ts new file mode 100644 index 000000000..049c45e3b --- /dev/null +++ b/tests/integration/tests/e2e/filter-function-coverage.test.ts @@ -0,0 +1,63 @@ +import { loadSchema, run, type WeakDbClientContract } from '@zenstackhq/testtools'; + +describe('Filter Function Coverage Tests', () => { + it('contains case-sensitive', async () => { + const { withPresets } = await loadSchema( + ` + model Foo { + id String @id @default(cuid()) + string String + @@allow('all', contains(string, 'a')) + } + ` + ); + + await expect(withPresets().foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); + await expect(withPresets().foo.create({ data: { string: 'bac' } })).toResolveTruthy(); + }); + + it('startsWith', async () => { + const { withPresets } = await loadSchema( + ` + model Foo { + id String @id @default(cuid()) + string String + @@allow('all', startsWith(string, 'a')) + } + ` + ); + + await expect(withPresets().foo.create({ data: { string: 'bac' } })).toBeRejectedByPolicy(); + await expect(withPresets().foo.create({ data: { string: 'abc' } })).toResolveTruthy(); + }); + + it('endsWith', async () => { + const { withPresets } = await loadSchema( + ` + model Foo { + id String @id @default(cuid()) + string String + @@allow('all', endsWith(string, 'a')) + } + ` + ); + + await expect(withPresets().foo.create({ data: { string: 'bac' } })).toBeRejectedByPolicy(); + await expect(withPresets().foo.create({ data: { string: 'bca' } })).toResolveTruthy(); + }); + + it('in', async () => { + const { withPresets } = await loadSchema( + ` + model Foo { + id String @id @default(cuid()) + string String + @@allow('all', string in ['a', 'b']) + } + ` + ); + + await expect(withPresets().foo.create({ data: { string: 'c' } })).toBeRejectedByPolicy(); + await expect(withPresets().foo.create({ data: { string: 'b' } })).toResolveTruthy(); + }); +}); From 933012f046c4b48d008e4f5d8855b443a0c02703 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 27 Mar 2023 11:29:34 +0800 Subject: [PATCH 3/7] merge main to dev (#291) --- README.md | 4 +- package.json | 2 +- packages/language/package.json | 6 +- packages/language/src/generated/ast.ts | 12 +- packages/language/src/generated/grammar.ts | 84 ++- packages/language/src/generated/module.ts | 2 +- .../language/syntaxes/zmodel.tmLanguage.json | 22 +- packages/next/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/react/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 9 +- .../schema/tests/schema/formatter.test.ts | 5 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- pnpm-lock.yaml | 661 +++++++++++++++++- tests/integration/test-run/package-lock.json | 4 +- 19 files changed, 750 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 53f509e5a..241039dc9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@
+ +
@@ -99,7 +101,7 @@ The following diagram gives a high-level overview of how it works. - [Documentation](https://zenstack.dev/docs) - [Community chat](https://go.zenstack.dev/chat) - [Twitter](https://twitter.com/zenstackhq) -- [Blog](https://dev.to/zenstack) +- [Blog](https://zenstack.dev/blog) ## Features diff --git a/package.json b/package.json index b89c20425..f6b3dd555 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 958842c96..095a4de9f 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", @@ -22,11 +22,11 @@ "devDependencies": { "concurrently": "^7.4.0", "copyfiles": "^2.4.1", - "langium-cli": "1.0.0", + "langium-cli": "1.1.0", "rimraf": "^3.0.2", "typescript": "^4.9.4" }, "dependencies": { - "langium": "1.0.1" + "langium": "1.1.0" } } diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 711ecadae..8e00e671e 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -1,5 +1,5 @@ /****************************************************************************** - * This file was generated by langium-cli 1.0.0. + * This file was generated by langium-cli 1.1.0. * DO NOT EDIT MANUALLY! ******************************************************************************/ @@ -16,7 +16,7 @@ export function isAbstractDeclaration(item: unknown): item is AbstractDeclaratio export type AttributeAttributeName = string; -export type AttributeName = string; +export type AttributeName = AttributeAttributeName | DataModelAttributeName | DataModelFieldAttributeName; export type BuiltinType = 'BigInt' | 'Boolean' | 'Bytes' | 'DateTime' | 'Decimal' | 'Float' | 'Int' | 'Json' | 'String'; @@ -785,6 +785,14 @@ export class ZModelAstReflection extends AbstractAstReflection { ] }; } + case 'LiteralExpr': { + return { + name: 'LiteralExpr', + mandatory: [ + { name: 'value', type: 'boolean' } + ] + }; + } case 'Model': { return { name: 'Model', diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 0292c7c88..14d2c86d4 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -1,5 +1,5 @@ /****************************************************************************** - * This file was generated by langium-cli 1.0.0. + * This file was generated by langium-cli 1.1.0. * DO NOT EDIT MANUALLY! ******************************************************************************/ @@ -3130,55 +3130,51 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "types": [ { "$type": "Type", - "typeAlternatives": [ - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@35" - }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@30" + "name": "ReferenceTarget", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@35" + } }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@33" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@30" + } }, - "isArray": false, - "isRef": false - } - ], - "name": "ReferenceTarget" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@33" + } + } + ] + } }, { "$type": "Type", - "typeAlternatives": [ - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@29" - }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@32" + "name": "TypeDeclaration", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@29" + } }, - "isArray": false, - "isRef": false - } - ], - "name": "TypeDeclaration" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@32" + } + } + ] + } } ], "definesHiddenTokens": false, diff --git a/packages/language/src/generated/module.ts b/packages/language/src/generated/module.ts index e2111b145..95219bee8 100644 --- a/packages/language/src/generated/module.ts +++ b/packages/language/src/generated/module.ts @@ -1,5 +1,5 @@ /****************************************************************************** - * This file was generated by langium-cli 1.0.0. + * This file was generated by langium-cli 1.1.0. * DO NOT EDIT MANUALLY! ******************************************************************************/ diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index f33d54cf5..a98fc252e 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage.json +++ b/packages/language/syntaxes/zmodel.tmLanguage.json @@ -10,17 +10,27 @@ }, { "name": "keyword.control.zmodel", - "match": "\\b(Any|Asc|attribute|BigInt|Boolean|Bytes|ContextType|datasource|DateTime|Decimal|Desc|enum|FieldReference|Float|function|generator|in|Int|Json|model|Null|Object|plugin|sort|String|TransitiveFieldReference)\\b" + "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|attribute|datasource|enum|function|generator|in|model|plugin|sort)\\b" }, { "name": "string.quoted.double.zmodel", "begin": "\"", - "end": "\"" + "end": "\"", + "patterns": [ + { + "include": "#string-character-escape" + } + ] }, { "name": "string.quoted.single.zmodel", "begin": "'", - "end": "'" + "end": "'", + "patterns": [ + { + "include": "#string-character-escape" + } + ] } ], "repository": { @@ -52,6 +62,10 @@ "name": "comment.line.zmodel" } ] + }, + "string-character-escape": { + "name": "constant.character.escape.zmodel", + "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" } } -} \ No newline at end of file +} diff --git a/packages/next/package.json b/packages/next/package.json index 92acef8fb..5ec18fe8b 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 6e97fa57c..fd6a2ed2b 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.0.0-alpha.82", + "version": "1.0.0-alpha.85", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index 36bc89aaf..631f16fcc 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 362681501..43c5b7f33 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.0.0-alpha.82", + "version": "1.0.0-alpha.85", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 37586726a..1686bcce9 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "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 0d57272d3..c027acc4f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "author": { "name": "ZenStack Team" }, @@ -90,7 +90,7 @@ "colors": "1.4.0", "commander": "^8.3.0", "cuid": "^2.1.8", - "langium": "1.0.1", + "langium": "1.1.0", "mixpanel": "^0.17.0", "node-machine-id": "^1.1.12", "ora": "^5.4.1", @@ -119,6 +119,7 @@ "@types/vscode": "^1.56.0", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", + "@zenstackhq/testtools": "workspace:*", "concurrently": "^7.4.0", "copyfiles": "^2.4.1", "dotenv": "^16.0.3", @@ -134,7 +135,7 @@ "ts-node": "^10.9.1", "tsc-alias": "^1.7.0", "typescript": "^4.8.4", - "vsce": "^2.13.0", - "@zenstackhq/testtools": "workspace:*" + "vitest": "^0.29.7", + "vsce": "^2.13.0" } } diff --git a/packages/schema/tests/schema/formatter.test.ts b/packages/schema/tests/schema/formatter.test.ts index 4de78fc4e..413fbf369 100644 --- a/packages/schema/tests/schema/formatter.test.ts +++ b/packages/schema/tests/schema/formatter.test.ts @@ -1,3 +1,5 @@ +/// + import { EmptyFileSystem } from 'langium'; import { expectFormatting } from 'langium/test'; import { createZModelServices } from '../../src/language-server/zmodel-module'; @@ -5,7 +7,8 @@ const services = createZModelServices({ ...EmptyFileSystem }).ZModel; const formatting = expectFormatting(services); describe('ZModelFormatter', () => { - it('declaration formatting', async () => { + // eslint-disable-next-line jest/no-disabled-tests + test.skip('declaration formatting', async () => { await formatting({ before: `datasource db { provider = 'postgresql' url = env('DATABASE_URL')} generator js {provider = 'prisma-client-js'} plugin reactHooks {provider = '@zenstackhq/react'output = 'lib/hooks'} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 0881a9408..2e29cb334 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 066821294..96671cd2d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "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 6202899b1..0abfdde7a 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e2c38efc..9aef7549d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,16 +12,16 @@ importers: specifiers: concurrently: ^7.4.0 copyfiles: ^2.4.1 - langium: 1.0.1 - langium-cli: 1.0.0 + langium: 1.1.0 + langium-cli: 1.1.0 rimraf: ^3.0.2 typescript: ^4.9.4 dependencies: - langium: 1.0.1 + langium: 1.1.0 devDependencies: concurrently: 7.4.0 copyfiles: 2.4.1 - langium-cli: 1.0.0 + langium-cli: 1.1.0 rimraf: 3.0.2 typescript: 4.9.4 publishDirectory: dist @@ -269,7 +269,7 @@ importers: eslint: ^8.27.0 eslint-plugin-jest: ^27.1.7 jest: ^29.2.1 - langium: 1.0.1 + langium: 1.1.0 langium-cli: ^1.0.0 mixpanel: ^0.17.0 node-machine-id: ^1.1.12 @@ -288,6 +288,7 @@ importers: tsc-alias: ^1.7.0 typescript: ^4.8.4 uuid: ^9.0.0 + vitest: ^0.29.7 vsce: ^2.13.0 vscode-jsonrpc: ^8.0.2 vscode-languageclient: ^8.0.2 @@ -307,7 +308,7 @@ importers: colors: 1.4.0 commander: 8.3.0 cuid: 2.1.8 - langium: 1.0.1 + langium: 1.1.0 mixpanel: 0.17.0 node-machine-id: 1.1.12 ora: 5.4.1 @@ -351,6 +352,7 @@ importers: ts-node: 10.9.1_jcmx33t3olsvcxopqdljsohpme tsc-alias: 1.7.0 typescript: 4.8.4 + vitest: 0.29.7 vsce: 2.15.0 publishDirectory: dist @@ -1379,6 +1381,96 @@ packages: dev: true optional: true + /@esbuild/android-arm/0.17.14: + resolution: {integrity: sha512-0CnlwnjDU8cks0yJLXfkaU/uoLyRf9VZJs4p1PskBr2AlAHeEsFEwJEo0of/Z3g+ilw5mpyDwThlxzNEIxOE4g==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64/0.17.14: + resolution: {integrity: sha512-eLOpPO1RvtsP71afiFTvS7tVFShJBCT0txiv/xjFBo5a7R7Gjw7X0IgIaFoLKhqXYAXhahoXm7qAmRXhY4guJg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64/0.17.14: + resolution: {integrity: sha512-nrfQYWBfLGfSGLvRVlt6xi63B5IbfHm3tZCdu/82zuFPQ7zez4XjmRtF/wIRYbJQ/DsZrxJdEvYFE67avYXyng==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64/0.17.14: + resolution: {integrity: sha512-eoSjEuDsU1ROwgBH/c+fZzuSyJUVXQTOIN9xuLs9dE/9HbV/A5IqdXHU1p2OfIMwBwOYJ9SFVGGldxeRCUJFyw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64/0.17.14: + resolution: {integrity: sha512-zN0U8RWfrDttdFNkHqFYZtOH8hdi22z0pFm0aIJPsNC4QQZv7je8DWCX5iA4Zx6tRhS0CCc0XC2m7wKsbWEo5g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64/0.17.14: + resolution: {integrity: sha512-z0VcD4ibeZWVQCW1O7szaLxGsx54gcCnajEJMdYoYjLiq4g1jrP2lMq6pk71dbS5+7op/L2Aod+erw+EUr28/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64/0.17.14: + resolution: {integrity: sha512-hd9mPcxfTgJlolrPlcXkQk9BMwNBvNBsVaUe5eNUqXut6weDQH8whcNaKNF2RO8NbpT6GY8rHOK2A9y++s+ehw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm/0.17.14: + resolution: {integrity: sha512-BNTl+wSJ1omsH8s3TkQmIIIQHwvwJrU9u1ggb9XU2KTVM4TmthRIVyxSp2qxROJHhZuW/r8fht46/QE8hU8Qvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64/0.17.14: + resolution: {integrity: sha512-FhAMNYOq3Iblcj9i+K0l1Fp/MHt+zBeRu/Qkf0LtrcFu3T45jcwB6A1iMsemQ42vR3GBhjNZJZTaCe3VFPbn9g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32/0.17.14: + resolution: {integrity: sha512-91OK/lQ5y2v7AsmnFT+0EyxdPTNhov3y2CWMdizyMfxSxRqHazXdzgBKtlmkU2KYIc+9ZK3Vwp2KyXogEATYxQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64/0.15.12: resolution: {integrity: sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==} engines: {node: '>=12'} @@ -1388,6 +1480,114 @@ packages: dev: true optional: true + /@esbuild/linux-loong64/0.17.14: + resolution: {integrity: sha512-vp15H+5NR6hubNgMluqqKza85HcGJgq7t6rMH7O3Y6ApiOWPkvW2AJfNojUQimfTp6OUrACUXfR4hmpcENXoMQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el/0.17.14: + resolution: {integrity: sha512-90TOdFV7N+fgi6c2+GO9ochEkmm9kBAKnuD5e08GQMgMINOdOFHuYLPQ91RYVrnWwQ5683sJKuLi9l4SsbJ7Hg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64/0.17.14: + resolution: {integrity: sha512-NnBGeoqKkTugpBOBZZoktQQ1Yqb7aHKmHxsw43NddPB2YWLAlpb7THZIzsRsTr0Xw3nqiPxbA1H31ZMOG+VVPQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64/0.17.14: + resolution: {integrity: sha512-0qdlKScLXA8MGVy21JUKvMzCYWovctuP8KKqhtE5A6IVPq4onxXhSuhwDd2g5sRCzNDlDjitc5sX31BzDoL5Fw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x/0.17.14: + resolution: {integrity: sha512-Hdm2Jo1yaaOro4v3+6/zJk6ygCqIZuSDJHdHaf8nVH/tfOuoEX5Riv03Ka15LmQBYJObUTNS1UdyoMk0WUn9Ww==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64/0.17.14: + resolution: {integrity: sha512-8KHF17OstlK4DuzeF/KmSgzrTWQrkWj5boluiiq7kvJCiQVzUrmSkaBvcLB2UgHpKENO2i6BthPkmUhNDaJsVw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64/0.17.14: + resolution: {integrity: sha512-nVwpqvb3yyXztxIT2+VsxJhB5GCgzPdk1n0HHSnchRAcxqKO6ghXwHhJnr0j/B+5FSyEqSxF4q03rbA2fKXtUQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64/0.17.14: + resolution: {integrity: sha512-1RZ7uQQ9zcy/GSAJL1xPdN7NDdOOtNEGiJalg/MOzeakZeTrgH/DoCkbq7TaPDiPhWqnDF+4bnydxRqQD7il6g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64/0.17.14: + resolution: {integrity: sha512-nqMjDsFwv7vp7msrwWRysnM38Sd44PKmW8EzV01YzDBTcTWUpczQg6mGao9VLicXSgW/iookNK6AxeogNVNDZA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64/0.17.14: + resolution: {integrity: sha512-xrD0mccTKRBBIotrITV7WVQAwNJ5+1va6L0H9zN92v2yEdjfAN7864cUaZwJS7JPEs53bDTzKFbfqVlG2HhyKQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32/0.17.14: + resolution: {integrity: sha512-nXpkz9bbJrLLyUTYtRotSS3t5b+FOuljg8LgLdINWFs3FfqZMtbnBCZFUmBzQPyxqU87F8Av+3Nco/M3hEcu1w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64/0.17.14: + resolution: {integrity: sha512-gPQmsi2DKTaEgG14hc3CHXHp62k8g6qr0Pas+I4lUxRMugGSATh/Bi8Dgusoz9IQ0IfdrvLpco6kujEIBoaogA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint/eslintrc/1.3.3: resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2975,6 +3175,16 @@ packages: '@types/node': 18.14.2 dev: true + /@types/chai-subset/1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.4 + dev: true + + /@types/chai/4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: true + /@types/connect/3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: @@ -3516,6 +3726,37 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /@vitest/expect/0.29.7: + resolution: {integrity: sha512-UtG0tW0DP6b3N8aw7PHmweKDsvPv4wjGvrVZW7OSxaFg76ShtVdMiMcUkZJgCE8QWUmhwaM0aQhbbVLo4F4pkA==} + dependencies: + '@vitest/spy': 0.29.7 + '@vitest/utils': 0.29.7 + chai: 4.3.7 + dev: true + + /@vitest/runner/0.29.7: + resolution: {integrity: sha512-Yt0+csM945+odOx4rjZSjibQfl2ymxqVsmYz6sO2fiO5RGPYDFCo60JF6tLL9pz4G/kjY4irUxadeB1XT+H1jg==} + dependencies: + '@vitest/utils': 0.29.7 + p-limit: 4.0.0 + pathe: 1.1.0 + dev: true + + /@vitest/spy/0.29.7: + resolution: {integrity: sha512-IalL0iO6A6Xz8hthR8sctk6ZS//zVBX48EiNwQguYACdgdei9ZhwMaBFV70mpmeYAFCRAm+DpoFHM5470Im78A==} + dependencies: + tinyspy: 1.1.1 + dev: true + + /@vitest/utils/0.29.7: + resolution: {integrity: sha512-vNgGadp2eE5XKCXtZXL5UyNEDn68npSct75OC9AlELenSK0DiV1Mb9tfkwJHKjRb69iek+e79iipoJx8+s3SdA==} + dependencies: + cli-truncate: 3.1.0 + diff: 5.1.0 + loupe: 2.3.6 + pretty-format: 27.5.1 + dev: true + /abort-controller/3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -3554,6 +3795,12 @@ packages: hasBin: true dev: true + /acorn/8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /agent-base/6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3624,6 +3871,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex/6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -3641,6 +3893,11 @@ packages: engines: {node: '>=10'} dev: true + /ansi-styles/6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /anymatch/3.1.2: resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} engines: {node: '>= 8'} @@ -3739,6 +3996,10 @@ packages: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} dev: true + /assertion-error/1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /astral-regex/2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -4146,6 +4407,11 @@ packages: engines: {node: '>= 0.8'} dev: true + /cac/6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -4199,6 +4465,19 @@ packages: upper-case-first: 2.0.2 dev: false + /chai/4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk/2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -4240,6 +4519,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /check-error/1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + /checkpoint-client/1.1.21: resolution: {integrity: sha512-bcrcnJncn6uGhj06IIsWvUBPyJWK1ZezDbLCJ//IQEYXkUobhGvOOBlHe9K5x0ZMkAZGinPB4T+lTUmFz/acWQ==} dependencies: @@ -4361,6 +4644,14 @@ packages: slice-ansi: 3.0.0 string-width: 4.2.3 + /cli-truncate/3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + /cliui/6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} dependencies: @@ -4457,6 +4748,11 @@ packages: typical: 5.2.0 dev: true + /commander/10.0.0: + resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} + engines: {node: '>=14'} + dev: true + /commander/6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -4717,6 +5013,13 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /deep-eql/4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /deep-extend/0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4825,6 +5128,11 @@ packages: engines: {node: '>=0.3.1'} dev: true + /diff/5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4876,6 +5184,10 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + /eastasianwidth/0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ee-first/1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} dev: true @@ -4896,6 +5208,10 @@ packages: /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex/9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /encodeurl/1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -5204,6 +5520,36 @@ packages: esbuild-windows-arm64: 0.15.12 dev: true + /esbuild/0.17.14: + resolution: {integrity: sha512-vOO5XhmVj/1XQR9NQ1UPq6qvMYL7QFJU57J5fKBKBKxp17uDt5PgxFDb4A2nEiXhr1qQs4x0F5+66hVVw4ruNw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.14 + '@esbuild/android-arm64': 0.17.14 + '@esbuild/android-x64': 0.17.14 + '@esbuild/darwin-arm64': 0.17.14 + '@esbuild/darwin-x64': 0.17.14 + '@esbuild/freebsd-arm64': 0.17.14 + '@esbuild/freebsd-x64': 0.17.14 + '@esbuild/linux-arm': 0.17.14 + '@esbuild/linux-arm64': 0.17.14 + '@esbuild/linux-ia32': 0.17.14 + '@esbuild/linux-loong64': 0.17.14 + '@esbuild/linux-mips64el': 0.17.14 + '@esbuild/linux-ppc64': 0.17.14 + '@esbuild/linux-riscv64': 0.17.14 + '@esbuild/linux-s390x': 0.17.14 + '@esbuild/linux-x64': 0.17.14 + '@esbuild/netbsd-x64': 0.17.14 + '@esbuild/openbsd-x64': 0.17.14 + '@esbuild/sunos-x64': 0.17.14 + '@esbuild/win32-arm64': 0.17.14 + '@esbuild/win32-ia32': 0.17.14 + '@esbuild/win32-x64': 0.17.14 + dev: true + /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -5996,6 +6342,10 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true + /get-func-name/2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + /get-intrinsic/1.1.3: resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} dependencies: @@ -6388,6 +6738,11 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + /is-fullwidth-code-point/4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + /is-generator-fn/2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -8537,6 +8892,10 @@ packages: engines: {node: '>=6'} hasBin: true + /jsonc-parser/3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + /jsonfile/4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: @@ -8594,6 +8953,19 @@ packages: lodash: 4.17.21 dev: true + /langium-cli/1.1.0: + resolution: {integrity: sha512-vnv037FHqXqMeNiNF90v47VrJGiJPzH721UIbbHcu6Nfx0C1UC6SmQhGHtZIDRovT5qJsiXRIPDTZYrIkm4KJQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + commander: 10.0.0 + fs-extra: 11.1.0 + jsonschema: 1.4.1 + langium: 1.1.0 + lodash: 4.17.21 + dev: true + /langium/1.0.1: resolution: {integrity: sha512-9w5NRsspYOOXV56q1EhXeFJWkboAVKZDzIEqPLraMQPQy6fvq104wlVwgvF6w9H4IcCpDHCsHJLsfzQMWBsjAA==} engines: {node: '>=14.0.0'} @@ -8603,6 +8975,17 @@ packages: vscode-languageserver: 8.0.2 vscode-languageserver-textdocument: 1.0.7 vscode-uri: 3.0.7 + dev: true + + /langium/1.1.0: + resolution: {integrity: sha512-TsWY/DIOR73se9/YaMQZpvfFWWrhWP0FQS9MrpxWEnMJR0FoKVpMF1thPWXZexLSfyEm1pn2oYzCdW4KUBqXxA==} + engines: {node: '>=14.0.0'} + dependencies: + chevrotain: 10.4.2 + chevrotain-allstar: 0.1.4 + vscode-languageserver: 8.0.2 + vscode-languageserver-textdocument: 1.0.8 + vscode-uri: 3.0.7 /lazystream/1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} @@ -8657,6 +9040,11 @@ packages: strip-bom: 3.0.0 dev: true + /local-pkg/0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + /locate-path/5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -8716,6 +9104,12 @@ packages: dependencies: js-tokens: 4.0.0 + /loupe/2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + /lower-case/2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -8905,6 +9299,15 @@ packages: engines: {node: '>=10'} hasBin: true + /mlly/1.2.0: + resolution: {integrity: sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==} + dependencies: + acorn: 8.8.2 + pathe: 1.1.0 + pkg-types: 1.0.2 + ufo: 1.1.1 + dev: true + /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -9169,6 +9572,13 @@ packages: dependencies: yocto-queue: 0.1.0 + /p-limit/4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate/4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -9290,6 +9700,14 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /pathe/1.1.0: + resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true + + /pathval/1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /pend/1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -9345,6 +9763,14 @@ packages: dependencies: find-up: 4.1.0 + /pkg-types/1.0.2: + resolution: {integrity: sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.2.0 + pathe: 1.1.0 + dev: true + /plimit-lit/1.4.1: resolution: {integrity: sha512-bK14ePAod0XWhXwjT6XvYfjcQ9PbCUkZXnDCAKRMZTJCaDIV9VFya1S/I+3WSbpdR8uBhCDh8TS4lQ/JQvhNFA==} dependencies: @@ -9364,6 +9790,15 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /prebuild-install/7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} engines: {node: '>=10'} @@ -9403,6 +9838,15 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + /pretty-format/27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /pretty-format/29.0.3: resolution: {integrity: sha512-cHudsvQr1K5vNVLbvYF/nv3Qy/F/BcEKxGuIeMiVMRHxPOO1RxXooP8g/ZrwAp7Dx+KdMZoOc7NxLHhMrP2f9Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9590,6 +10034,10 @@ packages: react: 18.2.0 scheduler: 0.23.0 + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true @@ -9825,6 +10273,14 @@ packages: dependencies: glob: 7.2.3 + /rollup/3.20.2: + resolution: {integrity: sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -9980,6 +10436,10 @@ packages: object-inspect: 1.12.3 dev: true + /siginfo/2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -10014,6 +10474,14 @@ packages: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + /slice-ansi/5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + dev: true + /smartwrap/2.0.2: resolution: {integrity: sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==} engines: {node: '>=6'} @@ -10101,11 +10569,19 @@ packages: escape-string-regexp: 2.0.0 dev: true + /stackback/0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} dev: true + /std-env/3.3.2: + resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + dev: true + /stream-read-all/3.0.1: resolution: {integrity: sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==} engines: {node: '>=10'} @@ -10137,6 +10613,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width/5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: true + /string.prototype.trimend/1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: @@ -10173,6 +10658,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi/7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-bom/3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -10203,6 +10695,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal/1.0.1: + resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + dependencies: + acorn: 8.8.2 + dev: true + /styled-jsx/5.0.7_zavbqmrropwrojvx6ojaa4s7im: resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} @@ -10404,6 +10902,20 @@ packages: engines: {node: '>=6'} dev: true + /tinybench/2.4.0: + resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} + dev: true + + /tinypool/0.4.0: + resolution: {integrity: sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy/1.1.1: + resolution: {integrity: sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==} + engines: {node: '>=14.0.0'} + dev: true + /tmp/0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -10876,6 +11388,10 @@ packages: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: true + /ufo/1.1.1: + resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} + dev: true + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -11004,6 +11520,122 @@ packages: engines: {node: '>= 0.8'} dev: true + /vite-node/0.29.7_@types+node@18.14.2: + resolution: {integrity: sha512-PakCZLvz37yFfUPWBnLa1OYHPCGm5v4pmRrTcFN4V/N/T3I6tyP3z07S//9w+DdeL7vVd0VSeyMZuAh+449ZWw==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.2.0 + pathe: 1.1.0 + picocolors: 1.0.0 + vite: 4.2.1_@types+node@18.14.2 + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite/4.2.1_@types+node@18.14.2: + resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.14.2 + esbuild: 0.17.14 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 3.20.2 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitest/0.29.7: + resolution: {integrity: sha512-aWinOSOu4jwTuZHkb+cCyrqQ116Q9TXaJrNKTHudKBknIpR0VplzeaOUuDF9jeZcrbtQKZQt6yrtd+eakbaxHg==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.4 + '@types/chai-subset': 1.3.3 + '@types/node': 18.14.2 + '@vitest/expect': 0.29.7 + '@vitest/runner': 0.29.7 + '@vitest/spy': 0.29.7 + '@vitest/utils': 0.29.7 + acorn: 8.8.2 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + debug: 4.3.4 + local-pkg: 0.4.3 + pathe: 1.1.0 + picocolors: 1.0.0 + source-map: 0.6.1 + std-env: 3.3.2 + strip-literal: 1.0.1 + tinybench: 2.4.0 + tinypool: 0.4.0 + tinyspy: 1.1.1 + vite: 4.2.1_@types+node@18.14.2 + vite-node: 0.29.7_@types+node@18.14.2 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vsce/2.15.0: resolution: {integrity: sha512-P8E9LAZvBCQnoGoizw65JfGvyMqNGlHdlUXD1VAuxtvYAaHBKLBdKPnpy60XKVDAkQCfmMu53g+gq9FM+ydepw==} engines: {node: '>= 14'} @@ -11054,6 +11686,9 @@ packages: /vscode-languageserver-textdocument/1.0.7: resolution: {integrity: sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg==} + /vscode-languageserver-textdocument/1.0.8: + resolution: {integrity: sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==} + /vscode-languageserver-types/3.17.2: resolution: {integrity: sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==} @@ -11142,6 +11777,15 @@ packages: dependencies: isexe: 2.0.0 + /why-is-node-running/2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -11332,6 +11976,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + /yocto-queue/1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + /zip-stream/4.1.0: resolution: {integrity: sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==} engines: {node: '>= 10'} diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 3f644e59b..14f918130 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -158,7 +158,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.82", + "version": "1.0.0-alpha.85", "hasInstallScript": true, "license": "MIT", "dependencies": { From c3b456a3b6e841d7eedc7565ef87cafd90fca2d6 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 28 Mar 2023 13:38:04 +0800 Subject: [PATCH 4/7] fix: improve clarity of dealing with `auth()` during policy generation (#293) --- packages/language/src/ast.ts | 1 + packages/runtime/src/validation.ts | 14 + .../validator/expression-validator.ts | 4 +- .../function-invocation-validator.ts | 4 +- .../src/language-server/zmodel-linker.ts | 14 +- .../access-policy/expression-writer.ts | 186 +++++--- .../access-policy/policy-guard-generator.ts | 15 +- .../typescript-expression-transformer.ts | 18 +- packages/schema/src/utils/ast-utils.ts | 36 +- .../tests/generator/expression-writer.test.ts | 407 +++++++++++++++--- tests/integration/test-run/package-lock.json | 6 +- .../tests/with-policy/auth.test.ts | 9 +- .../tests/with-policy/multi-id-fields.test.ts | 85 ++++ 13 files changed, 659 insertions(+), 140 deletions(-) diff --git a/packages/language/src/ast.ts b/packages/language/src/ast.ts index e5abfb58c..b9888eb9d 100644 --- a/packages/language/src/ast.ts +++ b/packages/language/src/ast.ts @@ -14,6 +14,7 @@ export type ResolvedShape = ExpressionType | AbstractDeclaration; export type ResolvedType = { decl?: ResolvedShape; array?: boolean; + nullable?: boolean; }; export const BinaryExprOperatorPriority: Record = { diff --git a/packages/runtime/src/validation.ts b/packages/runtime/src/validation.ts index ed0ddbfb7..33115f8e9 100644 --- a/packages/runtime/src/validation.ts +++ b/packages/runtime/src/validation.ts @@ -18,3 +18,17 @@ export function validate(validator: z.ZodType, data: unknown) { throw new ValidationError(fromZodError(err as z.ZodError).message); } } + +/** + * Check if the given object has all the given fields, not null or undefined + * @param obj + * @param fields + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function hasAllFields(obj: any, fields: string[]) { + if (typeof obj !== 'object' || !obj) { + return false; + } + return fields.every((f) => obj[f] !== undefined && obj[f] !== null); +} diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index ea15766db..64cdec539 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -1,6 +1,6 @@ import { BinaryExpr, Expression, isArrayExpr, isBinaryExpr, isEnum, isLiteralExpr } from '@zenstackhq/language/ast'; import { ValidationAcceptor } from 'langium'; -import { isAuthInvocation, isDataModelFieldReference, isEnumFieldReference } from '../../utils/ast-utils'; +import { getDataModelFieldReference, isAuthInvocation, isEnumFieldReference } from '../../utils/ast-utils'; import { AstValidator } from '../types'; /** @@ -33,7 +33,7 @@ export default class ExpressionValidator implements AstValidator { private validateBinaryExpr(expr: BinaryExpr, accept: ValidationAcceptor) { switch (expr.operator) { case 'in': { - if (!isDataModelFieldReference(expr.left)) { + if (!getDataModelFieldReference(expr.left)) { accept('error', 'left operand of "in" must be a field reference', { node: expr.left }); } 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 e5a5c76f0..d02be18f4 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -8,7 +8,7 @@ import { isLiteralExpr, } from '@zenstackhq/language/ast'; import { ValidationAcceptor } from 'langium'; -import { isDataModelFieldReference, isEnumFieldReference } from '../../utils/ast-utils'; +import { getDataModelFieldReference, isEnumFieldReference } from '../../utils/ast-utils'; import { FILTER_OPERATOR_FUNCTIONS } from '../constants'; import { AstValidator } from '../types'; import { isFromStdlib } from '../utils'; @@ -38,7 +38,7 @@ export default class FunctionInvocationValidator implements AstValidator isDataModel(d) && d.name === 'User'); if (userModel) { - node.$resolvedType = { decl: userModel }; + node.$resolvedType = { decl: userModel, nullable: true }; } } else if (funcDecl.name === 'future' && isFromStdlib(funcDecl)) { // future() function is resolved to current model @@ -447,19 +448,24 @@ export class ZModelLinker extends DefaultLinker { //#region Utils private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType) { + let nullable = false; + if (isDataModelFieldType(type)) { + nullable = type.optional; + } if (type.type) { const mappedType = mapBuiltinTypeToExpressionType(type.type); - node.$resolvedType = { decl: mappedType, array: type.array }; + node.$resolvedType = { decl: mappedType, array: type.array, nullable: nullable }; } else if (type.reference) { node.$resolvedType = { decl: type.reference.ref, array: type.array, + nullable: nullable, }; } } - private resolveToBuiltinTypeOrDecl(node: AstNode, type: ResolvedShape, array = false) { - node.$resolvedType = { decl: type, array }; + private resolveToBuiltinTypeOrDecl(node: AstNode, type: ResolvedShape, array = false, nullable = false) { + node.$resolvedType = { decl: type, array, nullable }; } //#endregion diff --git a/packages/schema/src/plugins/access-policy/expression-writer.ts b/packages/schema/src/plugins/access-policy/expression-writer.ts index e80fcf43c..03fe11fb1 100644 --- a/packages/schema/src/plugins/access-policy/expression-writer.ts +++ b/packages/schema/src/plugins/access-policy/expression-writer.ts @@ -17,7 +17,7 @@ import { import { getLiteral, GUARD_FIELD_NAME, PluginError } from '@zenstackhq/sdk'; import { CodeBlockWriter } from 'ts-morph'; import { FILTER_OPERATOR_FUNCTIONS } from '../../language-server/constants'; -import { getIdField, isAuthInvocation } from '../../utils/ast-utils'; +import { getIdFields, isAuthInvocation } from '../../utils/ast-utils'; import TypeScriptExpressionTransformer from './typescript-expression-transformer'; import { isFutureExpr } from './utils'; @@ -99,12 +99,17 @@ export class ExpressionWriter { private writeMemberAccess(expr: MemberAccessExpr) { this.block(() => { - // must be a boolean member - this.writeFieldCondition(expr.operand, () => { - this.block(() => { - this.writer.write(`${expr.member.ref?.name}: true`); + if (this.isAuthOrAuthMemberAccess(expr)) { + // member access of `auth()`, generate plain expression + this.guard(() => this.plain(expr), true); + } else { + // must be a boolean member + this.writeFieldCondition(expr.operand, () => { + this.block(() => { + this.writer.write(`${expr.member.ref?.name}: true`); + }); }); - }); + } }); } @@ -190,9 +195,14 @@ export class ExpressionWriter { return false; } - private guard(write: () => void) { + private guard(write: () => void, cast = false) { this.writer.write(`${GUARD_FIELD_NAME}: `); - write(); + if (cast) { + this.writer.write('!!'); + write(); + } else { + write(); + } } private plain(expr: Expression) { @@ -211,12 +221,9 @@ export class ExpressionWriter { // compile down to a plain expression this.block(() => { this.guard(() => { - this.plain(expr.left); - this.writer.write(' ' + operator + ' '); - this.plain(expr.right); + this.plain(expr); }); }); - return; } @@ -242,65 +249,105 @@ export class ExpressionWriter { } as ReferenceExpr; } - // if the operand refers to auth(), need to build a guard to avoid - // using undefined user as filter (which means no filter to Prisma) - // if auth() evaluates falsy, just treat the condition as false - if (this.isAuthOrAuthMemberAccess(operand)) { - this.writer.write(`!user ? { ${GUARD_FIELD_NAME}: false } : `); + // guard member access of `auth()` with null check + if (this.isAuthOrAuthMemberAccess(operand) && !fieldAccess.$resolvedType?.nullable) { + this.writer.write( + `(${this.plainExprBuilder.transform(operand)} == null) ? { ${GUARD_FIELD_NAME}: ${ + // auth().x != user.x is true when auth().x is null and user is not nullable + // other expressions are evaluated to false when null is involved + operator === '!=' ? 'true' : 'false' + } } : ` + ); } - this.block(() => { - this.writeFieldCondition(fieldAccess, () => { - this.block( - () => { + this.block( + () => { + this.writeFieldCondition(fieldAccess, () => { + this.block(() => { const dataModel = this.isModelTyped(fieldAccess); - if (dataModel) { - const idField = getIdField(dataModel); - if (!idField) { + if (dataModel && isAuthInvocation(operand)) { + // right now this branch only serves comparison with `auth`, like + // @@allow('all', owner == auth()) + + const idFields = getIdFields(dataModel); + if (!idFields || idFields.length === 0) { throw new PluginError(`Data model ${dataModel.name} does not have an id field`); } - // comparing with an object, convert to "id" comparison instead - this.writer.write(`${idField.name}: `); + + if (operator !== '==' && operator !== '!=') { + throw new PluginError('Only == and != operators are allowed'); + } + + if (!isThisExpr(fieldAccess)) { + this.writer.writeLine(operator === '==' ? 'is:' : 'isNot:'); + const fieldIsNullable = !!fieldAccess.$resolvedType?.nullable; + if (fieldIsNullable) { + // if field is nullable, we can generate "null" check condition + this.writer.write(`(user == null) ? null : `); + } + } + this.block(() => { - this.writeOperator(operator, () => { - // operand ? operand.field : null - this.writer.write('('); - this.plain(operand); - this.writer.write(' ? '); - this.plain(operand); - this.writer.write(`.${idField.name}`); - this.writer.write(' : null'); - this.writer.write(')'); + idFields.forEach((idField, idx) => { + const writeIdsCheck = () => { + // id: user.id + this.writer.write(`${idField.name}:`); + this.plain(operand); + this.writer.write(`.${idField.name}`); + if (idx !== idFields.length - 1) { + this.writer.write(','); + } + }; + + if (isThisExpr(fieldAccess) && operator === '!=') { + // wrap a not + this.writer.writeLine('NOT:'); + this.block(() => writeIdsCheck()); + } else { + writeIdsCheck(); + } }); }); } else { - this.writeOperator(operator, () => { + this.writeOperator(operator, fieldAccess, () => { this.plain(operand); }); } - }, - // "this" expression is compiled away (to .id access), so we should - // avoid generating a new layer - !isThisExpr(fieldAccess) - ); - }); - }); + }, !isThisExpr(fieldAccess)); + }); + }, + // "this" expression is compiled away (to .id access), so we should + // avoid generating a new layer + !isThisExpr(fieldAccess) + ); } private isAuthOrAuthMemberAccess(expr: Expression) { return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthInvocation(expr.operand)); } - private writeOperator(operator: ComparisonOperator, writeOperand: () => void) { - if (operator === '!=') { - // wrap a 'not' - this.writer.write('not: '); - this.block(() => { - this.writeOperator('==', writeOperand); - }); - } else { - this.writer.write(`${this.mapOperator(operator)}: `); + private writeOperator(operator: ComparisonOperator, fieldAccess: Expression, writeOperand: () => void) { + if (isDataModel(fieldAccess.$resolvedType?.decl)) { + if (operator === '==') { + this.writer.write('is: '); + } else if (operator === '!=') { + this.writer.write('isNot: '); + } else { + throw new PluginError('Only == and != operators are allowed for data model comparison'); + } writeOperand(); + } else { + if (operator === '!=') { + // wrap a 'not' + this.writer.write('not: '); + this.block(() => { + this.writer.write(`${this.mapOperator('==')}: `); + writeOperand(); + }); + } else { + this.writer.write(`${this.mapOperator(operator)}: `); + writeOperand(); + } } } @@ -414,10 +461,37 @@ export class ExpressionWriter { } private writeLogical(expr: BinaryExpr, operator: '&&' | '||') { - this.block(() => { - this.writer.write(`${operator === '&&' ? 'AND' : 'OR'}: `); - this.writeExprList([expr.left, expr.right]); - }); + // TODO: do we need short-circuit for logical operators? + + if (operator === '&&') { + // // && short-circuit: left && right -> left ? right : { zenstack_guard: false } + // if (!this.hasFieldAccess(expr.left)) { + // this.plain(expr.left); + // this.writer.write(' ? '); + // this.write(expr.right); + // this.writer.write(' : '); + // this.block(() => this.guard(() => this.writer.write('false'))); + // } else { + this.block(() => { + this.writer.write('AND:'); + this.writeExprList([expr.left, expr.right]); + }); + // } + } else { + // // || short-circuit: left || right -> left ? { zenstack_guard: true } : right + // if (!this.hasFieldAccess(expr.left)) { + // this.plain(expr.left); + // this.writer.write(' ? '); + // this.block(() => this.guard(() => this.writer.write('true'))); + // this.writer.write(' : '); + // this.write(expr.right); + // } else { + this.block(() => { + this.writer.write('OR:'); + this.writeExprList([expr.left, expr.right]); + }); + // } + } } private writeUnary(expr: UnaryExpr) { diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts index 9f0fcd5af..06a70336b 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -21,7 +21,7 @@ import path from 'path'; import { FunctionDeclaration, Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; import { name } from '.'; import { isFromStdlib } from '../../language-server/utils'; -import { analyzePolicies, getIdField } from '../../utils/ast-utils'; +import { analyzePolicies, getIdFields } from '../../utils/ast-utils'; import { ALL_OPERATION_KINDS, getDefaultOutputFolder, RUNTIME_PACKAGE } from '../plugin-utils'; import { ExpressionWriter } from './expression-writer'; import { isFutureExpr } from './utils'; @@ -42,9 +42,8 @@ export default class PolicyGenerator { const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); sf.addImportDeclaration({ - namedImports: [{ name: 'QueryContext' }], + namedImports: [{ name: 'type QueryContext' }, { name: 'hasAllFields' }], moduleSpecifier: `${RUNTIME_PACKAGE}`, - isTypeOnly: true, }); sf.addImportDeclaration({ @@ -329,13 +328,17 @@ export default class PolicyGenerator { if (!userModel) { throw new PluginError('User model not found'); } - const userIdField = getIdField(userModel); - if (!userIdField) { + const userIdFields = getIdFields(userModel); + if (!userIdFields || userIdFields.length === 0) { throw new PluginError('User model does not have an id field'); } // normalize user to null to avoid accidentally use undefined in filter - func.addStatements(`const user = context.user ?? null;`); + func.addStatements( + `const user = hasAllFields(context.user, [${userIdFields + .map((f) => "'" + f.name + "'") + .join(', ')}]) ? context.user : null;` + ); } // r = ; diff --git a/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts b/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts index 961b3028f..cb6dfba5e 100644 --- a/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts +++ b/packages/schema/src/plugins/access-policy/typescript-expression-transformer.ts @@ -1,5 +1,6 @@ import { ArrayExpr, + BinaryExpr, Expression, InvocationExpr, isEnumField, @@ -9,6 +10,7 @@ import { NullExpr, ReferenceExpr, ThisExpr, + UnaryExpr, } from '@zenstackhq/language/ast'; import { PluginError } from '@zenstackhq/sdk'; import { isAuthInvocation } from '../../utils/ast-utils'; @@ -53,6 +55,12 @@ export default class TypeScriptExpressionTransformer { case MemberAccessExpr: return this.memberAccess(expr as MemberAccessExpr); + case UnaryExpr: + return this.unary(expr as UnaryExpr); + + case BinaryExpr: + return this.binary(expr as BinaryExpr); + default: throw new PluginError(`Unsupported expression type: ${expr.$type}`); } @@ -78,7 +86,7 @@ export default class TypeScriptExpressionTransformer { return expr.member.ref.name; } else { // normalize field access to null instead of undefined to avoid accidentally use undefined in filter - return `(${this.transform(expr.operand)} ? ${this.transform(expr.operand)}.${expr.member.ref.name} : null)`; + return `(${this.transform(expr.operand)}?.${expr.member.ref.name} ?? null)`; } } @@ -124,4 +132,12 @@ export default class TypeScriptExpressionTransformer { return expr.value.toString(); } } + + private unary(expr: UnaryExpr): string { + return `(${expr.operator} ${this.transform(expr.operand)})`; + } + + private binary(expr: BinaryExpr): string { + return `(${this.transform(expr.left)} ${expr.operator} ${this.transform(expr.right)})`; + } } diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 7452d50fa..5456a2670 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -1,7 +1,9 @@ import { DataModel, DataModelAttribute, + DataModelField, Expression, + isArrayExpr, isDataModel, isDataModelField, isEnumField, @@ -9,6 +11,7 @@ import { isMemberAccessExpr, isReferenceExpr, Model, + ReferenceExpr, } from '@zenstackhq/language/ast'; import { PolicyOperationKind } from '@zenstackhq/runtime'; import { getLiteral } from '@zenstackhq/sdk'; @@ -100,8 +103,25 @@ export const VALIDATION_ATTRIBUTES = [ '@lte', ]; -export function getIdField(dataModel: DataModel) { - return dataModel.fields.find((f) => f.attributes.some((attr) => attr.decl.$refText === '@id')); +export function getIdFields(dataModel: DataModel) { + const fieldLevelId = dataModel.fields.find((f) => f.attributes.some((attr) => attr.decl.$refText === '@id')); + if (fieldLevelId) { + return [fieldLevelId]; + } else { + // get model level @@id attribute + const modelIdAttr = dataModel.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); + if (modelIdAttr) { + // get fields referenced in the attribute: @@id([field1, field2]]) + if (!isArrayExpr(modelIdAttr.args[0].value)) { + return []; + } + const argValue = modelIdAttr.args[0].value; + return argValue.items + .filter((expr): expr is ReferenceExpr => isReferenceExpr(expr) && !!getDataModelFieldReference(expr)) + .map((expr) => expr.target.ref as DataModelField); + } + } + return []; } export function isAuthInvocation(expr: Expression) { @@ -112,12 +132,12 @@ export function isEnumFieldReference(expr: Expression) { return isReferenceExpr(expr) && isEnumField(expr.target.ref); } -export function isDataModelFieldReference(expr: Expression): boolean { - if (isReferenceExpr(expr)) { - return isDataModelField(expr.target.ref); - } else if (isMemberAccessExpr(expr)) { - return true; +export function getDataModelFieldReference(expr: Expression): DataModelField | undefined { + if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) { + return expr.target.ref; + } else if (isMemberAccessExpr(expr) && isDataModelField(expr.member.ref)) { + return expr.member.ref; } else { - return false; + return undefined; } } diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index 8476418b0..157d4916e 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -119,13 +119,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `!user ? - { zenstack_guard: false } : - { - id: { - equals: (user ? user.id: null) - } - }` + `(user == null) ? { zenstack_guard: false } : { id: user.id }` ); await check( @@ -137,15 +131,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `!user ? - { zenstack_guard: false } : - { - id: { - not: { - equals: (user ? user.id: null) - } - } - }` + `(user == null) ? { zenstack_guard: true } : { NOT: { id: user.id } }` ); await check( @@ -536,33 +522,113 @@ describe('Expression Writer Tests', () => { ); }); - it('auth check', async () => { + it('auth null check', async () => { await check( ` - model User { id String @id } + model User { + id String @id + } + model Test { id String @id - @@deny('all', auth() == null) + @@allow('all', auth() == null) } `, (model) => model.attributes[0].args[1].value, - `{ ${GUARD_FIELD_NAME}: user == null }` + `{ zenstack_guard: (user == null) }`, + '{ id: "1" }' ); await check( ` - model User { id String @id } + model User { + x String + y String + @@id([x, y]) + } + + model Test { + id String @id + @@allow('all', auth() == null) + } + `, + (model) => model.attributes[0].args[1].value, + `{ zenstack_guard: (user == null) }`, + '{ x: "1", y: "2" }' + ); + + await check( + ` + model User { + id String @id + } + model Test { id String @id @@allow('all', auth() != null) } `, (model) => model.attributes[0].args[1].value, - `{ ${GUARD_FIELD_NAME}: user != null }` + `{ zenstack_guard: (user != null) }`, + '{ id: "1" }' + ); + + await check( + ` + model User { + x String + y String + @@id([x, y]) + } + + model Test { + id String @id + @@allow('all', auth() != null) + } + `, + (model) => model.attributes[0].args[1].value, + `{ zenstack_guard: (user != null) }`, + '{ x: "1", y: "2" }' + ); + }); + + it('auth boolean field check', async () => { + await check( + ` + model User { + id String @id + admin Boolean + } + + model Test { + id String @id + @@allow('all', auth().admin) + } + `, + (model) => model.attributes[0].args[1].value, + `{ zenstack_guard: !!(user?.admin ?? null) }`, + '{ id: "1", admin: true }' + ); + + await check( + ` + model User { + id String @id + admin Boolean + } + + model Test { + id String @id + @@deny('all', !auth().admin) + } + `, + (model) => model.attributes[0].args[1].value, + `{ NOT: { zenstack_guard: !!(user?.admin ?? null) } }`, + '{ id: "1", admin: true }' ); }); - it('auth check against field', async () => { + it('auth check against field single id', async () => { await check( ` model User { @@ -578,16 +644,7 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `!user ? - { zenstack_guard : false } : - { - owner: { - id: { - equals: (user ? user.id : null) - } - } - } - ` + `(user==null) ? { zenstack_guard: false } : { owner: { is: { id : user.id } } }` ); await check( @@ -605,17 +662,12 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `!user ? - { zenstack_guard : false } : - { - owner: { - id: { - not: { - equals: (user ? user.id : null) - } - } - } - }` + `(user==null) ? { zenstack_guard: true } : + { + owner: { + isNot: { id: user.id } + } + }` ); await check( @@ -633,15 +685,261 @@ describe('Expression Writer Tests', () => { } `, (model) => model.attributes[0].args[1].value, - `!user ? + `((user?.id??null)==null) ? { zenstack_guard : false } : - { - owner: { - id: { - equals: (user ? user.id : null) - } - } - }` + { owner: { id: { equals: (user?.id ?? null) } } }` + ); + }); + + it('auth check against field multi-id', async () => { + await check( + ` + model User { + x String + y String + t Test? + @@id([x, y]) + } + + model Test { + id String @id + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() == owner) + } + `, + (model) => model.attributes[1].args[1].value, + `(user==null) ? + { zenstack_guard: false } : + { owner: { is: { x: user.x, y: user.y } } }`, + '{ x: "1", y: "2" }' + ); + + await check( + ` + model User { + x String + y String + t Test? + @@id([x, y]) + } + + model Test { + id String @id + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() != owner) + } + `, + (model) => model.attributes[1].args[1].value, + `(user==null) ? + { zenstack_guard: true } : + { owner: { isNot: { x: user.x, y: user.y } } }`, + '{ x: "1", y: "2" }' + ); + + await check( + ` + model User { + x String + y String + t Test? + @@id([x, y]) + } + + model Test { + id String @id + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth().x == owner.x && auth().y == owner.y) + } + `, + (model) => model.attributes[1].args[1].value, + `{ + AND: [ + ((user?.x??null)==null) ? { zenstack_guard: false } : { owner: { x: { equals: (user?.x ?? null) } } }, + ((user?.y??null)==null) ? { zenstack_guard: false } : { owner: { y: { equals: (user?.y ?? null) } } } + ] + }`, + '{ x: "1", y: "2" }' + ); + }); + + it('auth check against nullable field', async () => { + await check( + ` + model User { + id String @id + t Test? + } + + model Test { + id String @id + owner User? @relation(fields: [ownerId], references: [id]) + ownerId String? @unique + @@allow('all', auth() == owner) + } + `, + (model) => model.attributes[0].args[1].value, + `{ + owner: { + is: (user == null) ? null : { id: user.id } + } + }` + ); + + await check( + ` + model User { + id String @id + t Test? + } + + model Test { + id String @id + owner User? @relation(fields: [ownerId], references: [id]) + ownerId String? @unique + @@deny('all', auth() != owner) + } + `, + (model) => model.attributes[0].args[1].value, + `{ + owner: { + isNot: (user == null) ? null : { id: user.id } + } + }` + ); + + await check( + ` + model User { + id String @id + t Test? + } + + model Test { + id String @id + owner User? @relation(fields: [ownerId], references: [id]) + ownerId String? @unique + @@allow('all', auth().id == owner.id) + } + `, + (model) => model.attributes[0].args[1].value, + `((user?.id??null)==null) ? { zenstack_guard: false } : { owner: { id: { equals: (user?.id ?? null) } } }` + ); + }); + + it('auth check short-circuit [TBD]', async () => { + await check( + ` + model User { + id String @id + t Test? + } + + model Test { + id String @id + owner User @relation(fields: [ownerId], references: [id]) + ownerId String @unique + value Int + @@allow('all', auth() != null && auth().id == owner.id && value > 0) + } + `, + (model) => model.attributes[0].args[1].value, + `{ + AND: [ + { + AND: [ + { zenstack_guard: (user!=null) }, + ((user?.id??null)==null) ? {zenstack_guard:false} : { owner: { id: { equals: (user?.id??null) } } } + ] + }, + { value: { gt: 0 } } + ] + }` + ); + + await check( + ` + model User { + id String @id + t Test? + } + + model Test { + id String @id + owner User @relation(fields: [ownerId], references: [id]) + ownerId String @unique + value Int + @@deny('all', auth() == null || auth().id != owner.id || value <= 0) + } + `, + (model) => model.attributes[0].args[1].value, + `{ + OR: [ + { + OR: [ + { zenstack_guard:(user==null) }, + ((user?.id??null)==null) ? {zenstack_guard:true} : { owner : { id: { not: { equals: (user?.id??null) } } } } + ] + }, + { value: { lte: 0 } } + ] + }` + ); + }); + + it('relation field null check', async () => { + await check( + ` + model M { + id String @id + s String? + t Test @relation(fields: [tId], references: [id]) + tId String @unique + } + + model Test { + id String @id + m M? + @@allow('all', m == null || m.s == null) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + OR: [{ m: { is: null } }, { m: { s: { equals: null } } }] + } + ` + ); + + await check( + ` + model M { + id String @id + s String? + t Test @relation(fields: [tId], references: [id]) + tId String @unique + } + + model Test { + id String @id + m M? + @@deny('all', m != null || m.s != null) + } + `, + (model) => model.attributes[0].args[1].value, + ` + { + OR: [{ m: { isNot: null } }, { m: { s: { not: { equals: null } } } }] + } + ` ); }); @@ -836,7 +1134,7 @@ describe('Expression Writer Tests', () => { }); }); -async function check(schema: string, getExpr: (model: DataModel) => Expression, expected: string) { +async function check(schema: string, getExpr: (model: DataModel) => Expression, expected: string, userInit?: string) { if (!schema.includes('datasource ')) { schema = ` @@ -860,7 +1158,7 @@ async function check(schema: string, getExpr: (model: DataModel) => Expression, // inject user variable sf.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, - declarations: [{ name: 'user', initializer: '{ id: "user1" }' }], + declarations: [{ name: 'user', initializer: userInit ?? '{ id: "user1" }' }], }); // inject enums @@ -894,6 +1192,7 @@ async function check(schema: string, getExpr: (model: DataModel) => Expression, sf.formatText(); await project.save(); + console.log('Source saved:', sourcePath); if (project.getPreEmitDiagnostics().length > 0) { for (const d of project.getPreEmitDiagnostics()) { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 14f918130..2cbb0df60 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -173,7 +173,7 @@ "colors": "1.4.0", "commander": "^8.3.0", "cuid": "^2.1.8", - "langium": "1.0.1", + "langium": "1.1.0", "mixpanel": "^0.17.0", "node-machine-id": "^1.1.12", "ora": "^5.4.1", @@ -221,6 +221,7 @@ "ts-node": "^10.9.1", "tsc-alias": "^1.7.0", "typescript": "^4.8.4", + "vitest": "^0.29.7", "vsce": "^2.13.0" }, "engines": { @@ -425,7 +426,7 @@ "eslint": "^8.27.0", "eslint-plugin-jest": "^27.1.7", "jest": "^29.2.1", - "langium": "1.0.1", + "langium": "1.1.0", "langium-cli": "^1.0.0", "mixpanel": "^0.17.0", "node-machine-id": "^1.1.12", @@ -444,6 +445,7 @@ "tsc-alias": "^1.7.0", "typescript": "^4.8.4", "uuid": "^9.0.0", + "vitest": "^0.29.7", "vsce": "^2.13.0", "vscode-jsonrpc": "^8.0.2", "vscode-languageclient": "^8.0.2", diff --git a/tests/integration/tests/with-policy/auth.test.ts b/tests/integration/tests/with-policy/auth.test.ts index ac74f451b..57c9fff08 100644 --- a/tests/integration/tests/with-policy/auth.test.ts +++ b/tests/integration/tests/with-policy/auth.test.ts @@ -1,7 +1,7 @@ import { loadSchema } from '@zenstackhq/testtools'; import path from 'path'; -describe('With Policy:undefined user', () => { +describe('With Policy: auth() test', () => { let origDir: string; const suite = 'undefined-user'; @@ -182,13 +182,12 @@ describe('With Policy:undefined user', () => { const db = withPolicy(); await expect(db.user.create({ data: { id: 'user1', role: 'USER' } })).toResolveTruthy(); await expect(db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' } })).toResolveTruthy(); - await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb = withPolicy({ role: 'USER' }); - await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); + const authDb = withPolicy({ id: 'user1', role: 'USER' }); + await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb1 = withPolicy({ role: 'ADMIN' }); + const authDb1 = withPolicy({ id: 'user2', role: 'ADMIN' }); await expect(authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); }); diff --git a/tests/integration/tests/with-policy/multi-id-fields.test.ts b/tests/integration/tests/with-policy/multi-id-fields.test.ts index f9984f98f..156b9e2ac 100644 --- a/tests/integration/tests/with-policy/multi-id-fields.test.ts +++ b/tests/integration/tests/with-policy/multi-id-fields.test.ts @@ -68,4 +68,89 @@ describe('With Policy: multiple id fields', () => { }) ).toResolveTruthy(); }); + + it('multi-id auth', async () => { + const { prisma, withPolicy } = await loadSchema( + ` + model User { + x String + y String + m M? + n N? + p P? + q Q? + @@id([x, y]) + @@allow('all', true) + } + + model M { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() == owner) + } + + model N { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth().x == owner.x && auth().y == owner.y) + } + + model P { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() != owner) + } + + model Q { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() != null) + } + ` + ); + + await prisma.user.create({ data: { x: '1', y: '1' } }); + await prisma.user.create({ data: { x: '1', y: '2' } }); + + const anonDb = withPolicy({}); + + await expect( + anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) + ).toBeRejectedByPolicy(); + await expect( + anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) + ).toBeRejectedByPolicy(); + await expect( + anonDb.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) + ).toBeRejectedByPolicy(); + await expect( + anonDb.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) + ).toBeRejectedByPolicy(); + + const db = withPolicy({ x: '1', y: '1' }); + + await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); + await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); + await expect(db.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); + await expect(db.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); + await expect(db.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toBeRejectedByPolicy(); + await expect(db.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); + + await expect( + withPolicy(undefined).q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) + ).toBeRejectedByPolicy(); + await expect(db.q.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); + }); }); From 8b53ce710fbb887262373d2b317716efe84bb8cc Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 28 Mar 2023 14:09:46 +0800 Subject: [PATCH 5/7] merge from main (#294) Co-authored-by: JG --- .../language/syntaxes/zmodel.tmLanguage.json | 122 +++++++++--------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index a98fc252e..58448e539 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage.json +++ b/packages/language/syntaxes/zmodel.tmLanguage.json @@ -1,71 +1,69 @@ { - "name": "zmodel", - "scopeName": "source.zmodel", - "fileTypes": [ - ".zmodel" - ], - "patterns": [ - { - "include": "#comments" - }, - { - "name": "keyword.control.zmodel", - "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|attribute|datasource|enum|function|generator|in|model|plugin|sort)\\b" - }, - { - "name": "string.quoted.double.zmodel", - "begin": "\"", - "end": "\"", - "patterns": [ + "name": "zmodel", + "scopeName": "source.zmodel", + "fileTypes": [".zmodel"], + "patterns": [ { - "include": "#string-character-escape" - } - ] - }, - { - "name": "string.quoted.single.zmodel", - "begin": "'", - "end": "'", - "patterns": [ + "include": "#comments" + }, { - "include": "#string-character-escape" - } - ] - } - ], - "repository": { - "comments": { - "patterns": [ + "name": "keyword.control.zmodel", + "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|attribute|datasource|enum|function|generator|in|model|plugin|sort)\\b" + }, { - "name": "comment.block.zmodel", - "begin": "/\\*", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.zmodel" - } - }, - "end": "\\*/", - "endCaptures": { - "0": { - "name": "punctuation.definition.comment.zmodel" - } - } + "name": "string.quoted.double.zmodel", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "include": "#string-character-escape" + } + ] }, { - "begin": "//", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.zmodel" - } - }, - "end": "(?=$)", - "name": "comment.line.zmodel" + "name": "string.quoted.single.zmodel", + "begin": "'", + "end": "'", + "patterns": [ + { + "include": "#string-character-escape" + } + ] + } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.block.zmodel", + "begin": "/\\*", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.zmodel" + } + }, + "end": "\\*/", + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.zmodel" + } + } + }, + { + "begin": "//", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.zmodel" + } + }, + "end": "(?=$)", + "name": "comment.line.zmodel" + } + ] + }, + "string-character-escape": { + "name": "constant.character.escape.zmodel", + "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" } - ] - }, - "string-character-escape": { - "name": "constant.character.escape.zmodel", - "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" } - } } From bab179ec80812774c14a2aa20a418f3abfa77293 Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 28 Mar 2023 14:41:48 +0800 Subject: [PATCH 6/7] Chore/main2dev (#295) Co-authored-by: JG From 5378d515b6fbcac64b1b554c5b3f04a098e5641e Mon Sep 17 00:00:00 2001 From: Yiming Date: Tue, 28 Mar 2023 17:23:18 +0800 Subject: [PATCH 7/7] chore: bump version (#296) --- package.json | 2 +- packages/language/package.json | 2 +- .../language/syntaxes/zmodel.tmLanguage.json | 122 +++++++++--------- packages/next/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/react/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 +- tests/integration/test-run/package-lock.json | 4 +- 13 files changed, 75 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index f6b3dd555..99d47e57b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 095a4de9f..aa64b9810 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index 58448e539..a98fc252e 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage.json +++ b/packages/language/syntaxes/zmodel.tmLanguage.json @@ -1,69 +1,71 @@ { - "name": "zmodel", - "scopeName": "source.zmodel", - "fileTypes": [".zmodel"], - "patterns": [ + "name": "zmodel", + "scopeName": "source.zmodel", + "fileTypes": [ + ".zmodel" + ], + "patterns": [ + { + "include": "#comments" + }, + { + "name": "keyword.control.zmodel", + "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|attribute|datasource|enum|function|generator|in|model|plugin|sort)\\b" + }, + { + "name": "string.quoted.double.zmodel", + "begin": "\"", + "end": "\"", + "patterns": [ { - "include": "#comments" - }, + "include": "#string-character-escape" + } + ] + }, + { + "name": "string.quoted.single.zmodel", + "begin": "'", + "end": "'", + "patterns": [ { - "name": "keyword.control.zmodel", - "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|attribute|datasource|enum|function|generator|in|model|plugin|sort)\\b" - }, + "include": "#string-character-escape" + } + ] + } + ], + "repository": { + "comments": { + "patterns": [ { - "name": "string.quoted.double.zmodel", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "include": "#string-character-escape" - } - ] + "name": "comment.block.zmodel", + "begin": "/\\*", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.zmodel" + } + }, + "end": "\\*/", + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.zmodel" + } + } }, { - "name": "string.quoted.single.zmodel", - "begin": "'", - "end": "'", - "patterns": [ - { - "include": "#string-character-escape" - } - ] - } - ], - "repository": { - "comments": { - "patterns": [ - { - "name": "comment.block.zmodel", - "begin": "/\\*", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.zmodel" - } - }, - "end": "\\*/", - "endCaptures": { - "0": { - "name": "punctuation.definition.comment.zmodel" - } - } - }, - { - "begin": "//", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.zmodel" - } - }, - "end": "(?=$)", - "name": "comment.line.zmodel" - } - ] - }, - "string-character-escape": { - "name": "constant.character.escape.zmodel", - "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" + "begin": "//", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.zmodel" + } + }, + "end": "(?=$)", + "name": "comment.line.zmodel" } + ] + }, + "string-character-escape": { + "name": "constant.character.escape.zmodel", + "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" } + } } diff --git a/packages/next/package.json b/packages/next/package.json index 5ec18fe8b..5481df120 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index fd6a2ed2b..c9d696b18 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.0.0-alpha.85", + "version": "1.0.0-alpha.87", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index 631f16fcc..21670fdcd 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 43c5b7f33..50a05903c 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.0.0-alpha.85", + "version": "1.0.0-alpha.87", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1686bcce9..770391323 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "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 c027acc4f..f54bd8a8d 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2e29cb334..aa93e7bac 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 96671cd2d..813a99f41 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "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 0abfdde7a..4fb2eadae 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 2cbb0df60..3f3d059d9 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -158,7 +158,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.85", + "version": "1.0.0-alpha.87", "hasInstallScript": true, "license": "MIT", "dependencies": {