From c017cce43b2086ad68366c993a6a6bd6eb8280c1 Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:51:34 +0800 Subject: [PATCH 1/9] fix: generate react hooks for all models regardless policies (#191) --- package.json | 2 +- packages/language/package.json | 2 +- packages/next/package.json | 2 +- packages/plugins/react/package.json | 2 +- .../plugins/react/src/react-hooks-generator.ts | 15 ++++----------- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- tests/integration/test-run/package-lock.json | 4 ++-- 10 files changed, 14 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 2a6d99669..de23b0d01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.29", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index b499fd6a1..37490b7db 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.29", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/next/package.json b/packages/next/package.json index 0351fda53..681a274e4 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.29", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index da98367b8..e74a86388 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.28", + "version": "1.0.0-alpha.29", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/react/src/react-hooks-generator.ts b/packages/plugins/react/src/react-hooks-generator.ts index d34f97a47..65c09e104 100644 --- a/packages/plugins/react/src/react-hooks-generator.ts +++ b/packages/plugins/react/src/react-hooks-generator.ts @@ -6,17 +6,6 @@ import * as path from 'path'; import { Project } from 'ts-morph'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - const project = new Project(); - const models: DataModel[] = []; - const warnings: string[] = []; - - for (const dm of model.declarations.filter((d): d is DataModel => isDataModel(d))) { - const hasAllowRule = dm.attributes.find((attr) => attr.decl.ref?.name === '@@allow'); - if (hasAllowRule) { - models.push(dm); - } - } - let outDir = options.output as string; if (!outDir) { throw new PluginError('"output" option is required'); @@ -27,6 +16,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. outDir = path.join(path.dirname(options.schemaPath), outDir); } + const project = new Project(); + const warnings: string[] = []; + const models = model.declarations.filter((d): d is DataModel => isDataModel(d)); + generateIndex(project, outDir, models); models.forEach((model) => { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 65f6ed6e5..fdd495ad9 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.28", + "version": "1.0.0-alpha.29", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2fcaca000..88969e328 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.28", + "version": "1.0.0-alpha.29", "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 d803e6250..ef4824a0e 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.28", + "version": "1.0.0-alpha.29", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2311c713c..32f8d7a66 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.29", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 17a748a13..615eb6e24 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.25", + "version": "1.0.0-alpha.29", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -160,7 +160,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.25", + "version": "1.0.0-alpha.29", "hasInstallScript": true, "license": "MIT", "dependencies": { From 4af0a02de05af412b45ea94fbb71b98f62f02d0e Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Thu, 2 Feb 2023 22:42:06 +0800 Subject: [PATCH 2/9] fix: zmodel grammar error when using triple slash comments (#194) --- package.json | 2 +- packages/language/package.json | 2 +- packages/language/src/generated/grammar.ts | 127 +++++++++++++++++-- packages/language/src/zmodel.langium | 26 ++-- packages/next/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 +- tests/integration/test-run/package-lock.json | 4 +- 11 files changed, 141 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index de23b0d01..5d20c64fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.29", + "version": "1.0.0-alpha.30", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 37490b7db..b18485323 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.29", + "version": "1.0.0-alpha.30", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index d82b57737..989c7f1da 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -104,6 +104,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "datasource" @@ -156,6 +164,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -211,6 +227,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "generator" @@ -263,6 +287,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -318,6 +350,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "plugin" @@ -370,6 +410,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -1589,6 +1637,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "enum" @@ -1639,16 +1695,29 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "ParserRule", "name": "EnumField", "definition": { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@53" + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" }, - "arguments": [] - } + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@53" + }, + "arguments": [] + } + } + ] }, "definesHiddenTokens": false, "entry": false, @@ -1663,6 +1732,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "function" @@ -1779,6 +1856,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -2034,6 +2119,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "attribute" @@ -2126,6 +2219,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "default", @@ -2323,6 +2424,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "decl", diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 681608f3c..d6feb0179 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -10,24 +10,24 @@ AbstractDeclaration: // datasource DataSource: - 'datasource' name=ID '{' (fields+=DataSourceField)* '}'; + TRIPLE_SLASH_COMMENT* 'datasource' name=ID '{' (fields+=DataSourceField)* '}'; DataSourceField: - name=ID '=' value=(LiteralExpr|InvocationExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr|InvocationExpr); // generator GeneratorDecl: - 'generator' name=ID '{' (fields+=GeneratorField)* '}'; + TRIPLE_SLASH_COMMENT* 'generator' name=ID '{' (fields+=GeneratorField)* '}'; GeneratorField: - name=ID '=' value=(LiteralExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | ArrayExpr); // plugin Plugin: - 'plugin' name=ID '{' (fields+=PluginField)* '}'; + TRIPLE_SLASH_COMMENT* 'plugin' name=ID '{' (fields+=PluginField)* '}'; PluginField: - name=ID '=' value=(LiteralExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | ArrayExpr); // expression Expression: @@ -149,17 +149,17 @@ DataModelFieldType: // enum Enum: - 'enum' name=ID '{' (fields+=EnumField)+ '}'; + TRIPLE_SLASH_COMMENT* 'enum' name=ID '{' (fields+=EnumField)+ '}'; EnumField: - name=ID; + TRIPLE_SLASH_COMMENT* name=ID; // function FunctionDecl: - 'function' name=ID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; + TRIPLE_SLASH_COMMENT* 'function' name=ID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; FunctionParam: - name=ID ':' type=FunctionParamType; + TRIPLE_SLASH_COMMENT* name=ID ':' type=FunctionParamType; FunctionParamType: (type=ExpressionType | reference=[TypeDeclaration]) (array?='[]')?; @@ -184,10 +184,10 @@ AttributeName returns string: // attribute Attribute: - 'attribute' name=AttributeName '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=AttributeAttribute)*; + TRIPLE_SLASH_COMMENT* 'attribute' name=AttributeName '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=AttributeAttribute)*; AttributeParam: - (default?='_')? name=ID ':' type=AttributeParamType; + TRIPLE_SLASH_COMMENT* (default?='_')? name=ID ':' type=AttributeParamType; // FieldReference refers to fields declared in the current model // TransitiveFieldReference refers to fields declared in the model type of the current field @@ -200,7 +200,7 @@ DataModelFieldAttribute: decl=[Attribute:DataModelFieldAttributeName] ('(' AttributeArgList? ')')?; DataModelAttribute: - decl=[Attribute:DataModelAttributeName] ('(' AttributeArgList? ')')?; + TRIPLE_SLASH_COMMENT* decl=[Attribute:DataModelAttributeName] ('(' AttributeArgList? ')')?; AttributeAttribute: decl=[Attribute:AttributeAttributeName] ('(' AttributeArgList? ')')?; diff --git a/packages/next/package.json b/packages/next/package.json index 681a274e4..34bd2e43e 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.29", + "version": "1.0.0-alpha.30", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index e74a86388..e5c1c2836 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.29", + "version": "1.0.0-alpha.30", "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 fdd495ad9..9d4f3f3c6 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.29", + "version": "1.0.0-alpha.30", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 88969e328..64e695bfd 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.29", + "version": "1.0.0-alpha.30", "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 ef4824a0e..3c3ae9a81 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.29", + "version": "1.0.0-alpha.30", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 32f8d7a66..554769994 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.29", + "version": "1.0.0-alpha.30", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 615eb6e24..b4e69b890 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.29", + "version": "1.0.0-alpha.30", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -160,7 +160,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.29", + "version": "1.0.0-alpha.30", "hasInstallScript": true, "license": "MIT", "dependencies": { From 63c3607cd1a32cc68a732538e99fa5fbde843637 Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Fri, 3 Feb 2023 21:18:04 +0800 Subject: [PATCH 3/9] fix: multi-field unique constraint support; react hooks error catching bug (#195) --- package.json | 2 +- packages/language/package.json | 2 +- packages/next/package.json | 2 +- packages/plugins/react/package.json | 2 +- .../react/src/react-hooks-generator.ts | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- .../src/enhancements/policy/policy-utils.ts | 47 ++++- packages/runtime/src/enhancements/types.ts | 11 +- packages/schema/package.json | 2 +- .../schema/src/plugins/model-meta/index.ts | 48 ++++- packages/sdk/package.json | 2 +- packages/sdk/src/utils.ts | 21 +- tests/integration/test-run/package-lock.json | 4 +- .../with-policy/multi-field-unique.test.ts | 193 ++++++++++++++++++ 15 files changed, 325 insertions(+), 17 deletions(-) create mode 100644 tests/integration/tests/with-policy/multi-field-unique.test.ts diff --git a/package.json b/package.json index 5d20c64fc..b8864a499 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index b18485323..94dcb52da 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/next/package.json b/packages/next/package.json index 34bd2e43e..334de8570 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index e5c1c2836..bac8594ee 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.30", + "version": "1.0.0-alpha.31", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/react/src/react-hooks-generator.ts b/packages/plugins/react/src/react-hooks-generator.ts index 65c09e104..58b9a33f9 100644 --- a/packages/plugins/react/src/react-hooks-generator.ts +++ b/packages/plugins/react/src/react-hooks-generator.ts @@ -35,7 +35,7 @@ function wrapReadbackErrorCheck(code: string) { return `try { ${code} } catch (err: any) { - if (err.prisma && err.code === 'P2004') { + if (err.info?.prisma && err.info?.code === 'P2004') { // unable to readback data return undefined; } else { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 9d4f3f3c6..3ef3b3492 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.30", + "version": "1.0.0-alpha.31", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 64e695bfd..b53331462 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.30", + "version": "1.0.0-alpha.31", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 4afee5791..7ca356aba 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -130,6 +130,14 @@ export class PolicyUtil { */ async readWithCheck(model: string, args: any): Promise { args = this.clone(args); + + if (args.where) { + // query args will be used with findMany, so we need to + // translate unique constraint filters into a flat filter + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + await this.flattenGeneratedUniqueField(model, args.where); + } + await this.injectAuthGuard(args, model, 'read'); // recursively inject read guard conditions into the query args @@ -143,6 +151,28 @@ export class PolicyUtil { return result; } + // flatten unique constraint filters + async flattenGeneratedUniqueField(model: string, args: any) { + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + const uniqueConstraints = this.modelMeta.uniqueConstraints?.[camelCase(model)]; + let flattened = false; + if (uniqueConstraints) { + for (const [field, value] of Object.entries(args)) { + if (uniqueConstraints[field] && typeof value === 'object') { + for (const [f, v] of Object.entries(value)) { + args[f] = v; + } + delete args[field]; + flattened = true; + } + } + } + + if (flattened) { + this.logger.info(`Filter flattened: ${JSON.stringify(args)}`); + } + } + private async injectNestedReadConditions(model: string, args: any) { const injectTarget = args.select ?? args.include; if (!injectTarget) { @@ -376,6 +406,12 @@ export class PolicyUtil { // fetch preValue selection (analyzed from the post-update rules) const preValueSelect = await this.getPreValueSelect(model); const filter = await buildReversedQuery(context); + + // query args will be used with findMany, so we need to + // translate unique constraint filters into a flat filter + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + await this.flattenGeneratedUniqueField(model, filter); + const idField = this.getIdField(model); const query = { where: filter, select: { ...preValueSelect, [idField.name]: true } }; this.logger.info(`fetching pre-update entities for ${model}: ${format(query)})}`); @@ -543,11 +579,18 @@ export class PolicyUtil { ) { this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`); - const count = (await db[model].count({ where: filter })) as number; + const queryFilter = deepcopy(filter); + + // query args will be used with findMany, so we need to + // translate unique constraint filters into a flat filter + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + await this.flattenGeneratedUniqueField(model, queryFilter); + + const count = (await db[model].count({ where: queryFilter })) as number; const guard = await this.getAuthGuard(model, operation); // build a query condition with policy injected - const guardedQuery = { where: this.and(filter, guard) }; + const guardedQuery = { where: this.and(queryFilter, guard) }; const schema = (operation === 'create' || operation === 'update') && (await this.getModelSchema(model)); diff --git a/packages/runtime/src/enhancements/types.ts b/packages/runtime/src/enhancements/types.ts index 73364e7cc..8f81a3887 100644 --- a/packages/runtime/src/enhancements/types.ts +++ b/packages/runtime/src/enhancements/types.ts @@ -1,10 +1,19 @@ import { z } from 'zod'; import { FieldInfo, PolicyOperationKind, QueryContext } from '../types'; +/** + * Metadata for a model-level unique constraint + * e.g.: @@unique([a, b]) + */ +export type UniqueConstraint = { name: string; fields: string[] }; + /** * ZModel data model metadata */ -export type ModelMeta = { fields: Record> }; +export type ModelMeta = { + fields: Record>; + uniqueConstraints: Record>; +}; /** * Function for getting policy guard with a given context diff --git a/packages/schema/package.json b/packages/schema/package.json index 3c3ae9a81..84f8be89c 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.30", + "version": "1.0.0-alpha.31", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts index ba708c951..13794805a 100644 --- a/packages/schema/src/plugins/model-meta/index.ts +++ b/packages/schema/src/plugins/model-meta/index.ts @@ -1,6 +1,14 @@ -import { DataModel, DataModelField, Model, isDataModel, isLiteralExpr } from '@zenstackhq/language/ast'; +import { + ArrayExpr, + DataModel, + DataModelField, + isDataModel, + isLiteralExpr, + Model, + ReferenceExpr, +} from '@zenstackhq/language/ast'; import { RuntimeAttribute } from '@zenstackhq/runtime'; -import { PluginOptions, getLiteral, resolved } from '@zenstackhq/sdk'; +import { getAttributeArgs, getLiteral, PluginOptions, resolved } from '@zenstackhq/sdk'; import { camelCase } from 'change-case'; import path from 'path'; import { CodeBlockWriter, Project, VariableDeclarationKind } from 'ts-morph'; @@ -66,6 +74,23 @@ function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter) } }); writer.write(','); + + writer.write('uniqueConstraints:'); + writer.block(() => { + for (const model of dataModels) { + writer.write(`${camelCase(model.name)}:`); + writer.block(() => { + for (const constraint of getUniqueConstraints(model)) { + writer.write(`${constraint.name}: { + name: "${constraint.name}", + fields: ${JSON.stringify(constraint.fields)} + },`); + } + }); + writer.write(','); + } + }); + writer.write(','); }); } @@ -119,3 +144,22 @@ function getFieldAttributes(field: DataModelField): RuntimeAttribute[] { function isIdField(field: DataModelField) { return field.attributes.some((attr) => attr.decl.ref?.name === '@id'); } + +function getUniqueConstraints(model: DataModel) { + const constraints: Array<{ name: string; fields: string[] }> = []; + for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@unique')) { + const argsMap = getAttributeArgs(attr); + if (argsMap.fields) { + const fieldNames = (argsMap.fields as ArrayExpr).items.map( + (item) => resolved((item as ReferenceExpr).target).name + ); + let constraintName = argsMap.name && getLiteral(argsMap.name); + if (!constraintName) { + // default constraint name is fields concatenated with underscores + constraintName = fieldNames.join('_'); + } + constraints.push({ name: constraintName, fields: fieldNames }); + } + } + return constraints; +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 554769994..018906cba 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index d01f7ffe0..095ba6c46 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -1,4 +1,12 @@ -import { AstNode, Expression, isArrayExpr, isLiteralExpr, Reference } from '@zenstackhq/language/ast'; +import { + AstNode, + DataModelAttribute, + DataModelFieldAttribute, + Expression, + isArrayExpr, + isLiteralExpr, + Reference, +} from '@zenstackhq/language/ast'; export function resolved(ref: Reference): T { if (!ref.ref) { @@ -36,3 +44,14 @@ export default function indentString(string: string, count = 4): string { const indent = ' '; return string.replace(/^(?!\s*$)/gm, indent.repeat(count)); } + +export function getAttributeArgs(attr: DataModelAttribute | DataModelFieldAttribute): Record { + const result: Record = {}; + for (const arg of attr.args) { + if (!arg.$resolvedParam) { + continue; + } + result[arg.$resolvedParam.name] = arg.value; + } + return result; +} diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index b4e69b890..43ae64eac 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.30", + "version": "1.0.0-alpha.31", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -160,7 +160,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/tests/integration/tests/with-policy/multi-field-unique.test.ts b/tests/integration/tests/with-policy/multi-field-unique.test.ts new file mode 100644 index 000000000..d9c02b4e2 --- /dev/null +++ b/tests/integration/tests/with-policy/multi-field-unique.test.ts @@ -0,0 +1,193 @@ +import path from 'path'; +import { MODEL_PRELUDE, loadPrisma, run } from '../../utils'; + +describe('With Policy: multi-field unique', () => { + let origDir: string; + const suite = 'multi-field-unique'; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('toplevel crud test unnamed constraint', async () => { + const { withPolicy } = await loadPrisma( + `${suite}/toplevel-crud-unnamed`, + ` + ${MODEL_PRELUDE} + + model Model { + id String @id @default(uuid()) + a String + b String + x Int + @@unique([a, b]) + + @@allow('all', x > 0) + @@deny('update', future().x <= 0) + } + ` + ); + + const db = withPolicy(); + + await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); + await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 2 } })).toBeRejectedWithCode('P2002'); + await expect(db.model.create({ data: { a: 'a2', b: 'b2', x: 0 } })).toBeRejectedByPolicy(); + + await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).toResolveFalsy(); + await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 2 } })).toResolveTruthy(); + await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 0 } })).toBeRejectedByPolicy(); + + await expect(db.model.delete({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + }); + + it('toplevel crud test named constraint', async () => { + const { withPolicy } = await loadPrisma( + `${suite}/toplevel-crud-named`, + ` + ${MODEL_PRELUDE} + + model Model { + id String @id @default(uuid()) + a String + b String + x Int + @@unique([a, b], name: 'myconstraint') + + @@allow('all', x > 0) + @@deny('update', future().x <= 0) + } + ` + ); + + const db = withPolicy(); + + await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); + await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b2' } } })).toResolveFalsy(); + await expect( + db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 2 } }) + ).toResolveTruthy(); + await expect( + db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 0 } }) + ).toBeRejectedByPolicy(); + await expect(db.model.delete({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + }); + + it('nested crud test', async () => { + const { withPolicy } = await loadPrisma( + `${suite}/nested-crud`, + ` + ${MODEL_PRELUDE} + + model M1 { + id String @id @default(uuid()) + m2 M2[] + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + a String + b String + x Int + m1 M1 @relation(fields: [m1Id], references: [id]) + m1Id String + + @@unique([a, b]) + @@allow('all', x > 0) + } + ` + ); + + const db = withPolicy(); + + await expect(db.m1.create({ data: { id: '1', m2: { create: { a: 'a1', b: 'b1', x: 1 } } } })).toResolveTruthy(); + await expect( + db.m1.create({ data: { id: '2', m2: { create: { a: 'a1', b: 'b1', x: 2 } } } }) + ).toBeRejectedWithCode('P2002'); + await expect( + db.m1.create({ data: { id: '3', m2: { create: { a: 'a1', b: 'b2', x: 0 } } } }) + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + connectOrCreate: { + where: { a_b: { a: 'a1', b: 'b1' } }, + create: { a: 'a1', b: 'b1', x: 2 }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.count()).resolves.toBe(1); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + connectOrCreate: { + where: { a_b: { a: 'a1', b: 'b2' } }, + create: { a: 'a1', b: 'b2', x: 2 }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.count()).resolves.toBe(2); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + connectOrCreate: { + where: { a_b: { a: 'a2', b: 'b2' } }, + create: { a: 'a2', b: 'b2', x: 0 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { + where: { a_b: { a: 'a1', b: 'b2' } }, + data: { x: 3 }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).resolves.toEqual( + expect.objectContaining({ x: 3 }) + ); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + delete: { + a_b: { a: 'a1', b: 'b1' }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.count()).resolves.toBe(1); + }); +}); From 778e41ea5b6acf4853f8f53c4043631bd4131ef9 Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Sat, 4 Feb 2023 23:06:46 +0800 Subject: [PATCH 4/9] merge main back to dev (#198) From 1540d09842499c5b6f92e5bf2a97e290a2e9ccf9 Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Sun, 5 Feb 2023 09:09:45 +0800 Subject: [PATCH 5/9] chore: bump version (#200) --- README.md | 4 ++++ package.json | 2 +- packages/language/package.json | 2 +- packages/next/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 +- tests/integration/test-run/package-lock.json | 4 ++-- 10 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2104ff0eb..9205a8c44 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ const MyPosts = () => { }; ``` +The following diagram gives a high-level overview of how it works. + +![Architecture](https://zenstack.dev/img/architecture-light.png) + ## Links - [Home](https://zenstack.dev) diff --git a/package.json b/package.json index b8864a499..504b225d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.31", + "version": "1.0.0-alpha.32", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 94dcb52da..ac846c670 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.31", + "version": "1.0.0-alpha.32", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/next/package.json b/packages/next/package.json index 334de8570..404f81e06 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.31", + "version": "1.0.0-alpha.32", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index bac8594ee..4b60694b2 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.31", + "version": "1.0.0-alpha.32", "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 3ef3b3492..0679f8747 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.31", + "version": "1.0.0-alpha.32", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b53331462..65ec38c7b 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.31", + "version": "1.0.0-alpha.32", "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 84f8be89c..6919c0d3c 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.31", + "version": "1.0.0-alpha.32", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 018906cba..f1f9e58de 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.31", + "version": "1.0.0-alpha.32", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 43ae64eac..57b873cef 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.31", + "version": "1.0.0-alpha.32", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -160,7 +160,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.31", + "version": "1.0.0-alpha.32", "hasInstallScript": true, "license": "MIT", "dependencies": { From 6230dc1ecf1b7b7b86cb79bc02cf16cda1077f95 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Wed, 8 Feb 2023 09:51:56 +0000 Subject: [PATCH 6/9] feat: Support code action of generating opposite relation fields (#201) --- .../schema/src/language-server/constants.ts | 4 + .../validator/datamodel-validator.ts | 4 +- .../src/language-server/zmodel-code-action.ts | 138 ++++++++++++++++++ .../src/language-server/zmodel-formatter.ts | 22 ++- .../src/language-server/zmodel-module.ts | 2 + 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 packages/schema/src/language-server/zmodel-code-action.ts diff --git a/packages/schema/src/language-server/constants.ts b/packages/schema/src/language-server/constants.ts index d6e7a27fd..e2bd60339 100644 --- a/packages/schema/src/language-server/constants.ts +++ b/packages/schema/src/language-server/constants.ts @@ -12,3 +12,7 @@ export const SCALAR_TYPES = ['String', 'Int', 'Float', 'Decimal', 'BigInt', 'Boo * Name of standard library module */ export const STD_LIB_MODULE_NAME = 'stdlib.zmodel'; + +export enum IssueCodes { + MissingOppositeRelation = 'miss-opposite-relation', +} diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 7c7742af9..828e40c26 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -15,7 +15,7 @@ import { import { ValidationAcceptor } from 'langium'; import pluralize from 'pluralize'; import { analyzePolicies } from '../../utils/ast-utils'; -import { SCALAR_TYPES } from '../constants'; +import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; import { assignableToAttributeParam, validateDuplicatedDeclarations } from './utils'; @@ -297,7 +297,7 @@ export default class DataModelValidator implements AstValidator { accept( 'error', `The relation field "${field.name}" on model "${field.$container.name}" is missing an opposite relation field on model "${oppositeModel.name}"`, - { node: field } + { node: field, code: IssueCodes.MissingOppositeRelation } ); return; } else if (oppositeFields.length > 1) { diff --git a/packages/schema/src/language-server/zmodel-code-action.ts b/packages/schema/src/language-server/zmodel-code-action.ts new file mode 100644 index 000000000..3a545fed7 --- /dev/null +++ b/packages/schema/src/language-server/zmodel-code-action.ts @@ -0,0 +1,138 @@ +import { DataModel, DataModelField, isDataModel } from '@zenstackhq/language/ast'; +import { + AstReflection, + CodeActionProvider, + findDeclarationNodeAtOffset, + getContainerOfType, + IndexManager, + LangiumDocument, + LangiumServices, + MaybePromise, +} from 'langium'; + +import { + CancellationToken, + CodeAction, + CodeActionKind, + CodeActionParams, + Command, + Diagnostic, +} from 'vscode-languageserver'; +import { IssueCodes } from './constants'; +import { ZModelFormatter } from './zmodel-formatter'; + +export class ZModelCodeActionProvider implements CodeActionProvider { + protected readonly reflection: AstReflection; + protected readonly indexManager: IndexManager; + protected readonly formatter: ZModelFormatter; + + constructor(services: LangiumServices) { + this.reflection = services.shared.AstReflection; + this.indexManager = services.shared.workspace.IndexManager; + this.formatter = services.lsp.Formatter as ZModelFormatter; + } + + getCodeActions( + document: LangiumDocument, + params: CodeActionParams, + cancelToken?: CancellationToken + ): MaybePromise | undefined> { + const result: CodeAction[] = []; + const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca); + for (const diagnostic of params.context.diagnostics) { + this.createCodeActions(diagnostic, document, acceptor); + } + return result; + } + + private createCodeActions( + diagnostic: Diagnostic, + document: LangiumDocument, + accept: (ca: CodeAction | undefined) => void + ) { + switch (diagnostic.code) { + case IssueCodes.MissingOppositeRelation: + accept(this.fixMissingOppositeRelation(diagnostic, document)); + } + + return undefined; + } + + private fixMissingOppositeRelation(diagnostic: Diagnostic, document: LangiumDocument): CodeAction | undefined { + const offset = document.textDocument.offsetAt(diagnostic.range.start); + const rootCst = document.parseResult.value.$cstNode; + + if (rootCst) { + const cstNode = findDeclarationNodeAtOffset(rootCst, offset); + + const astNode = cstNode?.element as DataModelField; + + const oppositeModel = astNode.type.reference!.ref! as DataModel; + + const lastField = oppositeModel.fields[oppositeModel.fields.length - 1]; + + const container = getContainerOfType(cstNode?.element, isDataModel) as DataModel; + + const idField = container.fields.find((f) => + f.attributes.find((attr) => attr.decl.ref?.name === '@id') + ) as DataModelField; + + if (container && container.$cstNode && idField) { + // indent + let indent = '\t'; + const formatOptions = this.formatter.getFormatOptions(); + if (formatOptions?.insertSpaces) { + indent = ' '.repeat(formatOptions.tabSize); + } + indent = indent.repeat(this.formatter.getIndent()); + + const typeName = container.name; + const fieldName = this.lowerCaseFirstLetter(typeName); + + // might already exist + let referenceField = ''; + + const idFieldName = idField.name; + const referenceIdFieldName = fieldName + this.upperCaseFirstLetter(idFieldName); + + if (!oppositeModel.fields.find((f) => f.name === referenceIdFieldName)) { + referenceField = '\n' + indent + `${referenceIdFieldName} ${idField.type.type}`; + } + + return { + title: `Add opposite relation fields on ${oppositeModel.name}`, + kind: CodeActionKind.QuickFix, + diagnostics: [diagnostic], + isPreferred: false, + edit: { + changes: { + [document.textDocument.uri]: [ + { + range: { + start: lastField.$cstNode!.range.end, + end: lastField.$cstNode!.range.end, + }, + newText: + '\n' + + indent + + `${fieldName} ${typeName} @relation(fields: [${referenceIdFieldName}], references: [${idFieldName}])` + + referenceField, + }, + ], + }, + }, + }; + } + } + + return undefined; + } + + private lowerCaseFirstLetter(str: string) { + return str.charAt(0).toLowerCase() + str.slice(1); + } + + private upperCaseFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } +} diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index bf14e7268..372193279 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -1,13 +1,16 @@ -import { AbstractFormatter, AstNode, Formatting } from 'langium'; +import { AbstractFormatter, AstNode, Formatting, LangiumDocument } from 'langium'; import * as ast from '@zenstackhq/language/ast'; +import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver'; export class ZModelFormatter extends AbstractFormatter { + private formatOptions?: FormattingOptions; protected format(node: AstNode): void { const formatter = this.getNodeFormatter(node); if (ast.isAbstractDeclaration(node)) { const bracesOpen = formatter.keyword('{'); const bracesClose = formatter.keyword('}'); + // this line decide the indent count return by this.getIndent() formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent()); bracesOpen.prepend(Formatting.oneSpace()); bracesClose.prepend(Formatting.newLine()); @@ -17,4 +20,21 @@ export class ZModelFormatter extends AbstractFormatter { nodes.prepend(Formatting.noIndent()); } } + + protected override doDocumentFormat( + document: LangiumDocument, + options: FormattingOptions, + range?: Range | undefined + ): TextEdit[] { + this.formatOptions = options; + return super.doDocumentFormat(document, options, range); + } + + public getFormatOptions(): FormattingOptions | undefined { + return this.formatOptions; + } + + public getIndent() { + return 1; + } } diff --git a/packages/schema/src/language-server/zmodel-module.ts b/packages/schema/src/language-server/zmodel-module.ts index 5a3507789..077675ed5 100644 --- a/packages/schema/src/language-server/zmodel-module.ts +++ b/packages/schema/src/language-server/zmodel-module.ts @@ -20,6 +20,7 @@ import { import { TextDocuments } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-validator'; +import { ZModelCodeActionProvider } from './zmodel-code-action'; import { ZModelFormatter } from './zmodel-formatter'; import { ZModelLinker } from './zmodel-linker'; import { ZModelScopeComputation } from './zmodel-scope'; @@ -56,6 +57,7 @@ export const ZModelModule: Module new ZModelFormatter(), + CodeActionProvider: (services) => new ZModelCodeActionProvider(services), }, }; From 65377908b3aa439ec433de289b50b09235bac0d5 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 10 Feb 2023 14:02:50 +0000 Subject: [PATCH 7/9] feat: vs extension field auto formatter (#202) --- .../src/language-server/zmodel-formatter.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index 372193279..c9300fa7a 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -7,7 +7,22 @@ export class ZModelFormatter extends AbstractFormatter { private formatOptions?: FormattingOptions; protected format(node: AstNode): void { const formatter = this.getNodeFormatter(node); - if (ast.isAbstractDeclaration(node)) { + if (ast.isDataModelField(node)) { + formatter.property('type').prepend(Formatting.oneSpace()); + if (node.attributes.length > 0) { + formatter.properties('attributes').prepend(Formatting.oneSpace()); + } + } else if (ast.isDataModelFieldAttribute(node)) { + formatter.keyword('(').surround(Formatting.noSpace()); + formatter.keyword(')').prepend(Formatting.noSpace()); + formatter.keyword(',').append(Formatting.oneSpace()); + if (node.args.length > 1) { + formatter.nodes(...node.args.slice(1)).prepend(Formatting.oneSpace()); + } + } else if (ast.isAttributeArg(node)) { + formatter.keyword(':').prepend(Formatting.noSpace()); + formatter.keyword(':').append(Formatting.oneSpace()); + } else if (ast.isAbstractDeclaration(node)) { const bracesOpen = formatter.keyword('{'); const bracesClose = formatter.keyword('}'); // this line decide the indent count return by this.getIndent() From 39ae9bf36dfa51e6a664bc410a54b23437d0a9f0 Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Sat, 11 Feb 2023 16:58:51 +0800 Subject: [PATCH 8/9] fix: support using enum as field default (#204) --- .github/workflows/build-test.yml | 15 +++++++++------ .../src/language-server/validator/utils.ts | 19 ++++++++++++++++++- .../src/language-server/zmodel-code-action.ts | 15 +++++---------- .../validation/attribute-validation.test.ts | 15 +++++++++++++++ 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1df0c24d8..511bd9326 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,7 +14,7 @@ on: branches: ['dev', 'main', 'canary'] jobs: - build: + build-test: runs-on: ubuntu-latest strategy: @@ -35,11 +35,14 @@ jobs: cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: | - if [[ $GITHUB_REF == 'refs/heads/canary' ]]; then - DEFAULT_NPM_TAG=canary pnpm run build - else - DEFAULT_NPM_TAG=latest pnpm run build - fi + if [[ $GITHUB_REF == 'refs/heads/canary' ]]; then + DEFAULT_NPM_TAG=canary pnpm run build + else + DEFAULT_NPM_TAG=latest pnpm run build + fi + + - run: pnpm lint + # install again for internal dependencies - run: pnpm install --frozen-lockfile - run: pnpm run test diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index b15de1e91..4dfaff554 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -7,9 +7,11 @@ import { ExpressionType, isArrayExpr, isDataModelField, + isEnum, isLiteralExpr, isReferenceExpr, } from '@zenstackhq/language/ast'; +import { resolved } from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; /** @@ -99,7 +101,19 @@ export function assignableToAttributeParam( const dstIsArray = param.type.array; const dstRef = param.type.reference; - if (dstType) { + if (isEnum(argResolvedType.decl)) { + // enum type + + let attrArgDeclType = dstRef?.ref; + if (dstType === 'ContextType' && isDataModelField(attr.$container) && attr.$container?.type?.reference) { + // attribute parameter type is ContextType, need to infer type from + // the attribute's container + attrArgDeclType = resolved(attr.$container?.type?.reference); + } + return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array; + } else if (dstType) { + // scalar type + if (typeof argResolvedType?.decl !== 'string') { // destination type is not a reference, so argument type must be a plain expression return false; @@ -115,6 +129,8 @@ export function assignableToAttributeParam( return isReferenceExpr(arg.value) && isDataModelField(arg.value.target.ref); } } else if (dstType === 'ContextType') { + // attribute parameter type is ContextType, need to infer type from + // the attribute's container if (isDataModelField(attr.$container)) { if (!attr.$container?.type?.type) { return false; @@ -129,6 +145,7 @@ export function assignableToAttributeParam( typeAssignable(dstType, argResolvedType.decl) && (dstType === 'Any' || dstIsArray === argResolvedType.array) ); } else { + // reference type return dstRef?.ref === argResolvedType.decl && dstIsArray === argResolvedType.array; } } diff --git a/packages/schema/src/language-server/zmodel-code-action.ts b/packages/schema/src/language-server/zmodel-code-action.ts index 3a545fed7..23a64ca62 100644 --- a/packages/schema/src/language-server/zmodel-code-action.ts +++ b/packages/schema/src/language-server/zmodel-code-action.ts @@ -10,14 +10,7 @@ import { MaybePromise, } from 'langium'; -import { - CancellationToken, - CodeAction, - CodeActionKind, - CodeActionParams, - Command, - Diagnostic, -} from 'vscode-languageserver'; +import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver'; import { IssueCodes } from './constants'; import { ZModelFormatter } from './zmodel-formatter'; @@ -34,8 +27,7 @@ export class ZModelCodeActionProvider implements CodeActionProvider { getCodeActions( document: LangiumDocument, - params: CodeActionParams, - cancelToken?: CancellationToken + params: CodeActionParams ): MaybePromise | undefined> { const result: CodeAction[] = []; const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca); @@ -67,6 +59,7 @@ export class ZModelCodeActionProvider implements CodeActionProvider { const astNode = cstNode?.element as DataModelField; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const oppositeModel = astNode.type.reference!.ref! as DataModel; const lastField = oppositeModel.fields[oppositeModel.fields.length - 1]; @@ -109,7 +102,9 @@ export class ZModelCodeActionProvider implements CodeActionProvider { [document.textDocument.uri]: [ { range: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion start: lastField.$cstNode!.range.end, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion end: lastField.$cstNode!.range.end, }, newText: diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index e713a43ae..0d49e050f 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -314,4 +314,19 @@ describe('Attribute tests', () => { `) ).toContain('attribute "@length" cannot be used on this type of field'); }); + + it('enum as default', async () => { + await loadModel(` + ${prelude} + + enum E { + E1 + E2 + } + + model M { + e E @default(E1) + } + `); + }); }); From 0597a72ed3630ab67fbb129e5dae1dab684cf281 Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Sat, 11 Feb 2023 17:07:57 +0800 Subject: [PATCH 9/9] chore: bump version (#206) --- package.json | 2 +- packages/language/package.json | 2 +- packages/next/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 +- tests/integration/test-run/package-lock.json | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 504b225d4..7460dcb26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.32", + "version": "1.0.0-alpha.33", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index ac846c670..f2b0565c0 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.32", + "version": "1.0.0-alpha.33", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/next/package.json b/packages/next/package.json index 404f81e06..d8bb8f774 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.32", + "version": "1.0.0-alpha.33", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index 4b60694b2..33d19d71d 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.32", + "version": "1.0.0-alpha.33", "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 0679f8747..27dfb597b 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.32", + "version": "1.0.0-alpha.33", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 65ec38c7b..2f3772b1d 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.32", + "version": "1.0.0-alpha.33", "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 6919c0d3c..7bdd76848 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.32", + "version": "1.0.0-alpha.33", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f1f9e58de..bff8cc0ba 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.32", + "version": "1.0.0-alpha.33", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 57b873cef..ee995b27a 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.32", + "version": "1.0.0-alpha.33", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -160,7 +160,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.32", + "version": "1.0.0-alpha.33", "hasInstallScript": true, "license": "MIT", "dependencies": {