From 5ecae707b9e0e4494e60ac18a5dd839a5039169f Mon Sep 17 00:00:00 2001 From: augustin Date: Sun, 20 Jul 2025 19:55:30 +0200 Subject: [PATCH 01/14] feat: enable alias declarations --- packages/language/src/generated/ast.ts | 91 +++- packages/language/src/generated/grammar.ts | 506 ++++++++++++------ packages/language/src/zmodel.langium | 17 +- packages/language/syntaxes/zmodel.tmLanguage | 2 +- .../language/syntaxes/zmodel.tmLanguage.json | 2 +- .../function-invocation-validator.ts | 53 +- .../src/language-server/validator/utils.ts | 21 + .../src/language-server/zmodel-linker.ts | 28 +- .../src/language-server/zmodel-scope.ts | 16 + .../enhancer/policy/expression-writer.ts | 3 + .../enhancer/policy/policy-guard-generator.ts | 39 ++ .../src/plugins/enhancer/policy/utils.ts | 4 +- .../src/plugins/prisma/schema-generator.ts | 8 + packages/schema/src/res/stdlib.zmodel | 5 +- packages/schema/src/utils/ast-utils.ts | 22 +- .../validation/attribute-validation.test.ts | 53 ++ packages/sdk/src/constants.ts | 1 + .../src/typescript-expression-transformer.ts | 19 +- packages/sdk/src/utils.ts | 15 +- pnpm-lock.yaml | 11 +- .../integration/tests/plugins/policy.test.ts | 75 +++ 21 files changed, 749 insertions(+), 242 deletions(-) diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 32fa2515d..7be74b486 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -20,7 +20,15 @@ export const ZModelTerminals = { SL_COMMENT: /\/\/[^\n\r]*/, }; -export type AbstractDeclaration = Attribute | DataModel | DataSource | Enum | FunctionDecl | GeneratorDecl | Plugin | TypeDef; +export type AbstractCallable = AliasDecl | FunctionDecl; + +export const AbstractCallable = 'AbstractCallable'; + +export function isAbstractCallable(item: unknown): item is AbstractCallable { + return reflection.isInstance(item, AbstractCallable); +} + +export type AbstractDeclaration = AliasDecl | Attribute | DataModel | DataSource | Enum | FunctionDecl | GeneratorDecl | Plugin | TypeDef; export const AbstractDeclaration = 'AbstractDeclaration'; @@ -86,10 +94,10 @@ export function isReferenceTarget(item: unknown): item is ReferenceTarget { return reflection.isInstance(item, ReferenceTarget); } -export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'type' | 'view' | string; +export type RegularID = 'abstract' | 'alias' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'type' | 'view' | string; export function isRegularID(item: unknown): item is RegularID { - return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || item === 'type' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item))); + return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || item === 'type' || item === 'alias' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item))); } export type RegularIDWithTypeNames = 'Any' | 'BigInt' | 'Boolean' | 'Bytes' | 'DateTime' | 'Decimal' | 'Float' | 'Int' | 'Json' | 'Null' | 'Object' | 'String' | 'Unsupported' | RegularID; @@ -98,7 +106,7 @@ export function isRegularIDWithTypeNames(item: unknown): item is RegularIDWithTy return isRegularID(item) || item === 'String' || item === 'Boolean' || item === 'Int' || item === 'BigInt' || item === 'Float' || item === 'Decimal' || item === 'DateTime' || item === 'Json' || item === 'Bytes' || item === 'Null' || item === 'Object' || item === 'Any' || item === 'Unsupported'; } -export type TypeDeclaration = DataModel | Enum | TypeDef; +export type TypeDeclaration = AliasDecl | DataModel | Enum | TypeDef; export const TypeDeclaration = 'TypeDeclaration'; @@ -114,6 +122,21 @@ export function isTypeDefFieldTypes(item: unknown): item is TypeDefFieldTypes { return reflection.isInstance(item, TypeDefFieldTypes); } +export interface AliasDecl extends AstNode { + readonly $container: Model; + readonly $type: 'AliasDecl'; + attributes: Array + expression: Expression + name: RegularID + params: Array +} + +export const AliasDecl = 'AliasDecl'; + +export function isAliasDecl(item: unknown): item is AliasDecl { + return reflection.isInstance(item, AliasDecl); +} + export interface Argument extends AstNode { readonly $container: InvocationExpr; readonly $type: 'Argument'; @@ -127,7 +150,7 @@ export function isArgument(item: unknown): item is Argument { } export interface ArrayExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ArrayExpr'; items: Array } @@ -198,7 +221,7 @@ export function isAttributeParamType(item: unknown): item is AttributeParamType } export interface BinaryExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'BinaryExpr'; left: Expression operator: '!' | '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '?' | '^' | 'in' | '||' @@ -212,7 +235,7 @@ export function isBinaryExpr(item: unknown): item is BinaryExpr { } export interface BooleanLiteral extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'BooleanLiteral'; value: Boolean } @@ -224,7 +247,7 @@ export function isBooleanLiteral(item: unknown): item is BooleanLiteral { } export interface ConfigArrayExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ConfigArrayExpr'; items: Array } @@ -306,7 +329,7 @@ export function isDataModelAttribute(item: unknown): item is DataModelAttribute } export interface DataModelField extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl | TypeDef; + readonly $container: AliasDecl | DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'DataModelField'; attributes: Array comments: Array @@ -378,7 +401,7 @@ export function isEnum(item: unknown): item is Enum { } export interface EnumField extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl | TypeDef; + readonly $container: AliasDecl | DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'EnumField'; attributes: Array comments: Array @@ -421,7 +444,7 @@ export function isFunctionDecl(item: unknown): item is FunctionDecl { } export interface FunctionParam extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl | TypeDef; + readonly $container: AliasDecl | DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'FunctionParam'; name: RegularID optional: boolean @@ -462,7 +485,7 @@ export function isGeneratorDecl(item: unknown): item is GeneratorDecl { } export interface InternalAttribute extends AstNode { - readonly $container: Attribute | AttributeParam | FunctionDecl; + readonly $container: AliasDecl | Attribute | AttributeParam | FunctionDecl; readonly $type: 'InternalAttribute'; args: Array decl: Reference @@ -475,10 +498,10 @@ export function isInternalAttribute(item: unknown): item is InternalAttribute { } export interface InvocationExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'InvocationExpr'; args: Array - function: Reference + function: Reference } export const InvocationExpr = 'InvocationExpr'; @@ -488,7 +511,7 @@ export function isInvocationExpr(item: unknown): item is InvocationExpr { } export interface MemberAccessExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'MemberAccessExpr'; member: Reference operand: Expression @@ -525,7 +548,7 @@ export function isModelImport(item: unknown): item is ModelImport { } export interface NullExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'NullExpr'; value: 'null' } @@ -537,7 +560,7 @@ export function isNullExpr(item: unknown): item is NullExpr { } export interface NumberLiteral extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'NumberLiteral'; value: string } @@ -549,7 +572,7 @@ export function isNumberLiteral(item: unknown): item is NumberLiteral { } export interface ObjectExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ObjectExpr'; fields: Array } @@ -600,7 +623,7 @@ export function isReferenceArg(item: unknown): item is ReferenceArg { } export interface ReferenceExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ReferenceExpr'; args: Array target: Reference @@ -613,7 +636,7 @@ export function isReferenceExpr(item: unknown): item is ReferenceExpr { } export interface StringLiteral extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'StringLiteral'; value: string } @@ -625,7 +648,7 @@ export function isStringLiteral(item: unknown): item is StringLiteral { } export interface ThisExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'ThisExpr'; value: 'this' } @@ -652,7 +675,7 @@ export function isTypeDef(item: unknown): item is TypeDef { } export interface TypeDefField extends AstNode { - readonly $container: DataModel | Enum | FunctionDecl | TypeDef; + readonly $container: AliasDecl | DataModel | Enum | FunctionDecl | TypeDef; readonly $type: 'TypeDefField'; attributes: Array comments: Array @@ -682,7 +705,7 @@ export function isTypeDefFieldType(item: unknown): item is TypeDefFieldType { } export interface UnaryExpr extends AstNode { - readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; + readonly $container: AliasDecl | Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType; readonly $type: 'UnaryExpr'; operand: Expression operator: '!' @@ -707,7 +730,9 @@ export function isUnsupportedFieldType(item: unknown): item is UnsupportedFieldT } export type ZModelAstType = { + AbstractCallable: AbstractCallable AbstractDeclaration: AbstractDeclaration + AliasDecl: AliasDecl Argument: Argument ArrayExpr: ArrayExpr Attribute: Attribute @@ -764,11 +789,14 @@ export type ZModelAstType = { export class ZModelAstReflection extends AbstractAstReflection { getAllTypes(): string[] { - return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'ConfigArrayExpr', 'ConfigExpr', 'ConfigField', 'ConfigInvocationArg', 'ConfigInvocationExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'MemberAccessTarget', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'TypeDef', 'TypeDefField', 'TypeDefFieldType', 'TypeDefFieldTypes', 'UnaryExpr', 'UnsupportedFieldType']; + return ['AbstractCallable', 'AbstractDeclaration', 'AliasDecl', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'ConfigArrayExpr', 'ConfigExpr', 'ConfigField', 'ConfigInvocationArg', 'ConfigInvocationExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'MemberAccessTarget', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'TypeDef', 'TypeDefField', 'TypeDefFieldType', 'TypeDefFieldTypes', 'UnaryExpr', 'UnsupportedFieldType']; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { switch (subtype) { + case AliasDecl: { + return this.isSubtype(AbstractCallable, supertype) || this.isSubtype(AbstractDeclaration, supertype) || this.isSubtype(TypeDeclaration, supertype); + } case ArrayExpr: case BinaryExpr: case MemberAccessExpr: @@ -781,7 +809,6 @@ export class ZModelAstReflection extends AbstractAstReflection { } case Attribute: case DataSource: - case FunctionDecl: case GeneratorDecl: case Plugin: { return this.isSubtype(AbstractDeclaration, supertype); @@ -809,6 +836,9 @@ export class ZModelAstReflection extends AbstractAstReflection { case FunctionParam: { return this.isSubtype(ReferenceTarget, supertype); } + case FunctionDecl: { + return this.isSubtype(AbstractCallable, supertype) || this.isSubtype(AbstractDeclaration, supertype); + } case InvocationExpr: case LiteralExpr: { return this.isSubtype(ConfigExpr, supertype) || this.isSubtype(Expression, supertype); @@ -836,7 +866,7 @@ export class ZModelAstReflection extends AbstractAstReflection { return Attribute; } case 'InvocationExpr:function': { - return FunctionDecl; + return AbstractCallable; } case 'MemberAccessExpr:member': { return MemberAccessTarget; @@ -855,6 +885,15 @@ export class ZModelAstReflection extends AbstractAstReflection { getTypeMetaData(type: string): TypeMetaData { switch (type) { + case 'AliasDecl': { + return { + name: 'AliasDecl', + mandatory: [ + { name: 'attributes', type: 'array' }, + { name: 'params', type: 'array' } + ] + }; + } case 'ArrayExpr': { return { name: 'ArrayExpr', diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index c6a0113b4..5ec36a1c1 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -70,7 +70,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@68" }, "arguments": [] } @@ -98,56 +98,63 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@3" + "$ref": "#/rules@4" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@5" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@7" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@47" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@3" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@52" }, "arguments": [] } @@ -160,6 +167,126 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, + { + "$type": "ParserRule", + "name": "AliasDecl", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@70" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "alias" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@50" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "(" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "params", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@48" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "params", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@48" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": ")" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "expression", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "}" + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@57" + }, + "arguments": [] + }, + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, { "$type": "ParserRule", "name": "DataSource", @@ -169,7 +296,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -185,7 +312,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -201,7 +328,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@6" }, "arguments": [] }, @@ -229,7 +356,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -245,7 +372,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -261,7 +388,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@6" }, "arguments": [] }, @@ -289,7 +416,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -301,7 +428,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -317,7 +444,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@19" }, "arguments": [] } @@ -340,7 +467,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -356,7 +483,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -372,7 +499,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@7" + "$ref": "#/rules@8" }, "arguments": [] }, @@ -400,7 +527,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -412,7 +539,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -431,21 +558,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@14" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@25" }, "arguments": [] } @@ -467,7 +594,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@34" }, "arguments": [] }, @@ -488,7 +615,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@69" }, "arguments": [] } @@ -510,7 +637,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@68" }, "arguments": [] } @@ -532,7 +659,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@61" + "$ref": "#/rules@62" }, "arguments": [] } @@ -553,21 +680,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@10" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@11" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@12" }, "arguments": [] } @@ -600,7 +727,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -619,7 +746,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -656,7 +783,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] } @@ -671,7 +798,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@16" }, "arguments": [], "cardinality": "?" @@ -706,7 +833,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@17" }, "arguments": [] } @@ -725,7 +852,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@17" }, "arguments": [] } @@ -754,7 +881,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] } @@ -770,7 +897,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] } @@ -807,14 +934,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@15" }, "arguments": [] } @@ -838,14 +965,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@15" }, "arguments": [] } @@ -880,21 +1007,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@27" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@18" }, "arguments": [] } @@ -963,7 +1090,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] }, @@ -980,7 +1107,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@23" }, "arguments": [] }, @@ -1014,7 +1141,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@24" }, "arguments": [] } @@ -1033,7 +1160,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@24" }, "arguments": [] } @@ -1062,7 +1189,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] } @@ -1078,7 +1205,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -1112,7 +1239,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@26" }, "arguments": [] } @@ -1131,7 +1258,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@26" }, "arguments": [] } @@ -1176,14 +1303,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@68" }, "arguments": [] } @@ -1201,7 +1328,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -1228,7 +1355,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@46" + "$ref": "#/types@1" }, "deprecatedSyntax": false } @@ -1240,7 +1367,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@36" }, "arguments": [], "cardinality": "?" @@ -1271,7 +1398,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@35" }, "arguments": [] }, @@ -1301,7 +1428,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@1" + "$ref": "#/types@2" }, "deprecatedSyntax": false } @@ -1342,7 +1469,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@28" }, "arguments": [] } @@ -1369,7 +1496,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@28" }, "arguments": [] }, @@ -1418,7 +1545,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -1452,7 +1579,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@30" }, "arguments": [] }, @@ -1484,7 +1611,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@30" }, "arguments": [] } @@ -1514,7 +1641,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@31" }, "arguments": [] }, @@ -1563,7 +1690,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@31" }, "arguments": [] } @@ -1593,7 +1720,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@32" }, "arguments": [] }, @@ -1634,7 +1761,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@32" }, "arguments": [] } @@ -1664,7 +1791,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@33" }, "arguments": [] }, @@ -1705,7 +1832,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@33" }, "arguments": [] } @@ -1742,7 +1869,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] }, @@ -1755,56 +1882,56 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@20" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@21" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@29" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@27" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@14" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@22" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1831,7 +1958,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -1850,7 +1977,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -1876,7 +2003,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -1901,7 +2028,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -1934,7 +2061,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -1953,7 +2080,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "deprecatedSyntax": false } @@ -1972,7 +2099,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "deprecatedSyntax": false } @@ -2004,7 +2131,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2027,7 +2154,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] } @@ -2039,7 +2166,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2073,7 +2200,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2086,7 +2213,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2098,7 +2225,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] } @@ -2110,7 +2237,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] }, @@ -2141,7 +2268,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@61" }, "arguments": [] } @@ -2153,7 +2280,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@44" }, "arguments": [] } @@ -2165,12 +2292,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@3" + "$ref": "#/types@4" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2230,7 +2357,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2247,7 +2374,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2266,7 +2393,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2278,7 +2405,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2312,7 +2439,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2325,7 +2452,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2337,7 +2464,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2349,7 +2476,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] }, @@ -2380,7 +2507,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@61" }, "arguments": [] } @@ -2392,12 +2519,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@2" + "$ref": "#/types@3" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2465,7 +2592,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@13" }, "arguments": [] } @@ -2496,7 +2623,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2513,7 +2640,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2532,7 +2659,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2544,7 +2671,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2578,7 +2705,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2591,7 +2718,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2603,7 +2730,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] }, @@ -2627,7 +2754,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -2643,7 +2770,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2662,7 +2789,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [] } @@ -2681,7 +2808,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [] } @@ -2707,7 +2834,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2723,7 +2850,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] }, @@ -2740,7 +2867,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, @@ -2764,7 +2891,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -2776,7 +2903,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2792,7 +2919,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2832,7 +2959,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [] } @@ -2844,12 +2971,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@3" + "$ref": "#/types@4" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2896,7 +3023,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] }, @@ -2939,6 +3066,10 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "Keyword", "value": "type" + }, + { + "$type": "Keyword", + "value": "alias" } ] }, @@ -2959,7 +3090,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -3037,7 +3168,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -3057,21 +3188,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@63" + "$ref": "#/rules@64" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@64" + "$ref": "#/rules@65" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@65" + "$ref": "#/rules@66" }, "arguments": [] } @@ -3092,7 +3223,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@53" }, "arguments": [] } @@ -3111,7 +3242,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@53" }, "arguments": [] } @@ -3133,7 +3264,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, @@ -3161,7 +3292,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -3184,7 +3315,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -3200,7 +3331,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "arguments": [] } @@ -3212,7 +3343,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, @@ -3246,7 +3377,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [] }, @@ -3272,12 +3403,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@3" + "$ref": "#/types@4" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -3337,12 +3468,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@65" + "$ref": "#/rules@66" }, "arguments": [] }, @@ -3359,7 +3490,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [], "cardinality": "?" @@ -3389,7 +3520,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -3401,12 +3532,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@64" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -3423,7 +3554,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [], "cardinality": "?" @@ -3457,12 +3588,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@63" + "$ref": "#/rules@64" }, "arguments": [] }, @@ -3479,7 +3610,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [], "cardinality": "?" @@ -3514,7 +3645,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] } @@ -3533,7 +3664,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] } @@ -3565,7 +3696,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -3584,7 +3715,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@9" }, "arguments": [] } @@ -3837,25 +3968,46 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" + } + } + ] + } + }, + { + "$type": "Type", + "name": "AbstractCallable", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@47" + } + }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@3" } } ] @@ -3870,13 +4022,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" } } ] @@ -3891,13 +4043,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" } } ] @@ -3912,19 +4064,25 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" + } + }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@45" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@44" + "$ref": "#/rules@3" } } ] diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 4b311bcba..10897db0f 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -11,7 +11,14 @@ ModelImport: 'import' path=STRING ';'?; AbstractDeclaration: - DataSource | GeneratorDecl| Plugin | DataModel | TypeDef | Enum | FunctionDecl | Attribute; + DataSource | GeneratorDecl | Plugin | DataModel | TypeDef | Enum | FunctionDecl | AliasDecl | Attribute; + +// alias +AliasDecl: + TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' '{' expression=Expression '}' (attributes+=InternalAttribute)*; + +// AliasExpr: +// function=[AliasDecl] '(' ArgumentList? ')'; // datasource DataSource: @@ -91,8 +98,10 @@ ObjectExpr: FieldInitializer: name=(RegularID | STRING) ':' value=(Expression); +type AbstractCallable = FunctionDecl | AliasDecl; + InvocationExpr: - function=[FunctionDecl] '(' ArgumentList? ')'; + function=[AbstractCallable] '(' ArgumentList? ')'; type MemberAccessTarget = DataModelField | TypeDefField; @@ -228,7 +237,7 @@ FunctionParamType: // https://github.com/langium/langium/discussions/1012 RegularID returns string: // include keywords that we'd like to work as ID in most places - ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type'; + ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type' | 'alias'; RegularIDWithTypeNames returns string: RegularID | 'String' | 'Boolean' | 'Int' | 'BigInt' | 'Float' | 'Decimal' | 'DateTime' | 'Json' | 'Bytes' | 'Null' | 'Object' | 'Any' | 'Unsupported'; @@ -245,7 +254,7 @@ AttributeParam: AttributeParamType: (type=(ExpressionType | 'FieldReference' | 'TransitiveFieldReference' | 'ContextType') | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?; -type TypeDeclaration = DataModel | TypeDef | Enum; +type TypeDeclaration = DataModel | TypeDef | Enum | AliasDecl; DataModelFieldAttribute: decl=[Attribute:FIELD_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?; diff --git a/packages/language/syntaxes/zmodel.tmLanguage b/packages/language/syntaxes/zmodel.tmLanguage index 40b92fb9a..0576223fa 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage +++ b/packages/language/syntaxes/zmodel.tmLanguage @@ -20,7 +20,7 @@ name keyword.control.zmodel match - \b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\b + \b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|alias|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\b name diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index 0fb0227e5..a410cdc01 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|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\\b" + "match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|alias|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\\b" }, { "name": "string.quoted.double.zmodel", 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 eff614e3c..b29a4a07a 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -1,12 +1,14 @@ import { + AbstractCallable, + AliasDecl, Argument, DataModel, DataModelAttribute, DataModelFieldAttribute, Expression, - FunctionDecl, FunctionParam, InvocationExpr, + isAliasDecl, isArrayExpr, isDataModel, isDataModelAttribute, @@ -47,24 +49,24 @@ function func(name: string) { */ 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 }); + const callableDecl = expr.function.ref; + if (!callableDecl) { + accept('error', 'function or alias cannot be resolved', { node: expr }); return; } - if (!this.validateArgs(funcDecl, expr.args, accept)) { + if (!this.validateArgs(callableDecl, expr.args, accept)) { return; } - if (isFromStdlib(funcDecl)) { + if (isFromStdlib(callableDecl)) { // validate standard library functions // find the containing attribute context for the invocation let curr: AstNode | undefined = expr.$container; - let containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined; + let containerAttribute: DataModelAttribute | DataModelFieldAttribute | AliasDecl | undefined; while (curr) { - if (isDataModelAttribute(curr) || isDataModelFieldAttribute(curr)) { + if (isDataModelAttribute(curr) || isDataModelFieldAttribute(curr) || isAliasDecl(curr)) { containerAttribute = curr; break; } @@ -75,12 +77,12 @@ export default class FunctionInvocationValidator implements AstValidator 0 && (!exprContext || !funcAllowedContext.includes(exprContext))) { accept( 'error', - `function "${funcDecl.name}" is not allowed in the current context${ + `function "${callableDecl.name}" is not allowed in the current context${ exprContext ? ': ' + exprContext : '' }`, { @@ -93,7 +95,7 @@ export default class FunctionInvocationValidator implements AstValidator(expr.args[0]?.value); if (arg && !allCasing.includes(arg)) { accept('error', `argument must be one of: ${allCasing.map((c) => '"' + c + '"').join(', ')}`, { @@ -108,6 +110,12 @@ export default class FunctionInvocationValidator implements AstValidator + (item: Expression) => isLiteralExpr(item) || isEnumFieldReference(item) || isAuthOrAuthMemberAccess(item) ) ) @@ -144,19 +152,24 @@ export default class FunctionInvocationValidator implements AstValidator attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny' + (attr: DataModelAttribute) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny' ); for (const attr of policyAttrs) { const rule = attr.args[1]; diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 032bf9a5e..3ce887f79 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -5,6 +5,7 @@ import { isDataModelField, isMemberAccessExpr, isStringLiteral, + ResolvedShape, } from '@zenstackhq/language/ast'; import { isAuthInvocation } from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; @@ -101,6 +102,26 @@ export function mapBuiltinTypeToExpressionType( } } +/** + * Maps an expression type (e.g. StringLiteral) to a resolved shape (e.g. String) + */ +export function mappedRawExpressionTypeToResolvedShape(expressionType: Expression['$type']): ResolvedShape { + switch (expressionType) { + case 'StringLiteral': + return 'String'; + case 'NumberLiteral': + return 'Int'; + case 'BooleanLiteral': + return 'Boolean'; + case 'ObjectExpr': + return 'Object'; + case 'NullExpr': + return 'Null'; + default: + return 'Any'; + } +} + export function isAuthOrAuthMemberAccess(expr: Expression): boolean { return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand)); } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 1e2491bda..dc08e9a42 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -1,4 +1,5 @@ import { + AliasDecl, ArrayExpr, AttributeArg, AttributeParam, @@ -55,9 +56,13 @@ import { } from 'langium'; import { match } from 'ts-pattern'; import { CancellationToken } from 'vscode-jsonrpc'; -import { getAllLoadedAndReachableDataModelsAndTypeDefs, getContainingDataModel } from '../utils/ast-utils'; +import { + getAllLoadedAndReachableDataModelsAndTypeDefs, + getContainingDataModel, + isAliasInvocation, +} from '../utils/ast-utils'; import { isMemberContainer } from './utils'; -import { mapBuiltinTypeToExpressionType } from './validator/utils'; +import { mapBuiltinTypeToExpressionType, mappedRawExpressionTypeToResolvedShape } from './validator/utils'; interface DefaultReference extends Reference { _ref?: AstNode | LinkingError; @@ -278,8 +283,7 @@ export class ZModelLinker extends DefaultLinker { this.linkReference(node, 'function', document, extraScopes); node.args.forEach((arg) => this.resolve(arg, document, extraScopes)); if (node.function.ref) { - // eslint-disable-next-line @typescript-eslint/ban-types - const funcDecl = node.function.ref as FunctionDecl; + const funcDecl = node.function.ref as FunctionDecl | AliasDecl; if (isAuthInvocation(node)) { // auth() function is resolved against all loaded and reachable documents @@ -296,8 +300,22 @@ export class ZModelLinker extends DefaultLinker { } else if (isFutureExpr(node)) { // future() function is resolved to current model node.$resolvedType = { decl: getContainingDataModel(node) }; + } else if (isAliasInvocation(node)) { + // function is resolved to matching alias declaration + + const expressionType = funcDecl.expression?.$type; + if (!expressionType) { + this.createLinkingError({ + reference: node.function, + container: node, + property: 'alias', + }); + return; + } + const mappedType = mappedRawExpressionTypeToResolvedShape(expressionType); + this.resolveToBuiltinTypeOrDecl(node, mappedType); } else { - this.resolveToDeclaredType(node, funcDecl.returnType); + this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType); } } } diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 11cbb4909..669c7721e 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -8,6 +8,7 @@ import { isMemberAccessExpr, isModel, isReferenceExpr, + isAliasDecl, isThisExpr, isTypeDef, isTypeDefField, @@ -69,6 +70,16 @@ export class ZModelScopeComputation extends DefaultScopeComputation { ); result.push(desc); } + + if (isAliasDecl(node)) { + // add alias decls to the global scope + const desc = this.services.workspace.AstNodeDescriptionProvider.createDescription( + node, + node.name, + document + ); + result.push(desc); + } } return result; @@ -128,6 +139,11 @@ export class ZModelScopeProvider extends DefaultScopeProvider { } } + // if (isAliasExpr(context.container)) { + // // resolve `[rule]()` to the containing model + // return this.createScopeForContainingModel(context.container, this.getGlobalScope('AliasDecl', context)); + // } + return super.getScope(context); } diff --git a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts index d3dccfaa6..344940c69 100644 --- a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -5,6 +5,7 @@ import { DataModelField, Expression, InvocationExpr, + isAliasDecl, isDataModel, isDataModelField, isEnumField, @@ -805,6 +806,8 @@ export class ExpressionWriter { extraArgs ); }); + } else if (isAliasDecl(funcDecl)) { + // noop } else { throw new PluginError(name, `Unsupported function ${funcDecl.name}`); } diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index f257f0830..afa0968bf 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -12,7 +12,9 @@ import { isReferenceExpr, isThisExpr, isTypeDef, + isAliasDecl, } from '@zenstackhq/language/ast'; + import { PolicyCrudKind, type PolicyOperationKind } from '@zenstackhq/runtime'; import { type CodeWriter, @@ -60,6 +62,8 @@ export class PolicyGenerator { this.writeImports(model, output, sf); + this.writeAliasFunctions(model); + const models = getDataModels(model); const writer = new FastWriter(); @@ -467,6 +471,41 @@ export class PolicyGenerator { writer.write(`guard: ${guardFunc.name},`); } + /** + * Generates functions for the Aliases + */ + private writeAliasFunctions(model: Model) { + for (const decl of model.declarations) { + if (isAliasDecl(decl)) { + const alias = decl; + const params = alias.params?.map((p) => ({ name: p.name, type: 'any' })) ?? []; + if (alias.expression.$cstNode?.text.includes('auth()')) { + params.push({ + name: 'user', + type: 'PermissionCheckerContext["user"]', + }); + } + const transformer = new TypeScriptExpressionTransformer({ + context: ExpressionContext.AliasFunction, + }); + const writer = new FastWriter(); + try { + writer.write('return '); + writer.write(transformer.transform(alias.expression, false)); + writer.write(';'); + } catch (e) { + writer.write('return undefined /* erreur de transformation de la règle */;'); + } + this.extraFunctions.push({ + name: alias.name, + returnType: 'any', + parameters: params, + statements: [writer.result], + }); + } + } + } + // writes `permissionChecker: ...` for a given policy operation kind private writePermissionChecker( model: DataModel, diff --git a/packages/schema/src/plugins/enhancer/policy/utils.ts b/packages/schema/src/plugins/enhancer/policy/utils.ts index ae9a7846f..978917f5b 100644 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ b/packages/schema/src/plugins/enhancer/policy/utils.ts @@ -15,6 +15,7 @@ import { getLiteral, getQueryGuardFunctionName, isAuthInvocation, + hasAuthInvocation, isDataModelFieldReference, isEnumFieldReference, isFromStdlib, @@ -506,8 +507,7 @@ export function generateNormalizedAuthRef( statements: string[] ) { // check if any allow or deny rule contains 'auth()' invocation - const hasAuthRef = [...allows, ...denies].some((rule) => streamAst(rule).some((child) => isAuthInvocation(child))); - + const hasAuthRef = [...allows, ...denies].some((rule) => streamAst(rule).some((child) => hasAuthInvocation(child))); if (hasAuthRef) { const authModel = getAuthDecl(getDataModelAndTypeDefs(model.$container, true)); if (!authModel) { diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 4e29bb91d..1ffc49d99 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -38,6 +38,7 @@ import { getInheritedFromDelegate, getLiteral, getRelationKeyPairs, + isAliasExpr, isDelegateModel, isIdField, PluginError, @@ -962,6 +963,13 @@ export class PrismaSchemaGenerator { ) ); } else if (isInvocationExpr(node)) { + if (isAliasExpr(node)) { + const expression = node.function?.ref?.expression; + if (!expression) { + throw new PluginError(name, `Alias has no expression reference: ${this.exprToText(node)}`); + } + return this.makeAttributeArgValue(expression); + } // invocation return new PrismaAttributeArgValue('FunctionCall', this.makeFunctionCall(node)); } else { diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 1fde1fc57..5d9fe2343 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -65,6 +65,9 @@ enum ExpressionContext { // used in @@index Index + + // used in alias functions + AliasFunction } /** @@ -77,7 +80,7 @@ function env(name: String): String { * Gets the current login user. */ function auth(): Any { -} @@@expressionContext([DefaultValue, AccessPolicy]) +} @@@expressionContext([DefaultValue, AccessPolicy, AliasFunction]) /** * Gets current date-time (as DateTime type). diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index f59ee7faa..261ece1ff 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -11,9 +11,11 @@ import { isInvocationExpr, isModel, isReferenceExpr, + isAliasDecl, isTypeDef, Model, ModelImport, + AliasDecl, TypeDef, } from '@zenstackhq/language/ast'; import { @@ -40,7 +42,7 @@ import path from 'node:path'; import { URI, Utils } from 'vscode-uri'; import { findNodeModulesFile } from './pkg-utils'; -export function extractDataModelsWithAllowRules(model: Model): DataModel[] { +export function extractDataModelsWithAllowAliass(model: Model): DataModel[] { return model.declarations.filter( (d) => isDataModel(d) && d.attributes.some((attr) => attr.decl.ref?.name === '@@allow') ) as DataModel[]; @@ -169,6 +171,14 @@ export function isCheckInvocation(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'check' && isFromStdlib(node.function.ref); } +export function isAliasInvocation(node: AstNode) { + // check if a matching alias exists + const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? []; + // const aliasDecls = getAllLoadedAlias(this.langiumDocuments()); + return isInvocationExpr(node) && allAlias.some((alias) => alias.name === node.function.$refText); + // (!node.function.ref || !isFromStdlib(node.function.ref)) /* && isAliasDecl(node.function.ref) */ +} + export function resolveImportUri(imp: ModelImport): URI | undefined { if (!imp.path) return undefined; // This will return true if imp.path is undefined, null, or an empty string (""). @@ -312,6 +322,16 @@ export function getAllLoadedAndReachableDataModelsAndTypeDefs( return allDataModels; } +/** + * Gets all data models and type defs from all loaded documents + */ +export function getAllLoadedAlias(langiumDocuments: LangiumDocuments) { + return langiumDocuments.all + .map((doc) => doc.parseResult.value as Model) + .flatMap((model) => model.declarations.filter((d): d is AliasDecl => isAliasDecl(d))) + .toArray(); +} + /** * Walk up the inheritance chain to find the path from the start model to the target model */ diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 3e0553ee2..923081813 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1391,4 +1391,57 @@ describe('Attribute tests', () => { `) ).resolves.toContain('Invalid regular expression'); }); + + it('alias expressions', async () => { + await loadModel(` + ${prelude} + + alias foo() { + true + } + + model A { + id String @id + opened Boolean @default(foo()) + + @@allow('all', foo()) + } + `); + + await loadModel(` + ${prelude} + + alias allowAll() { + true + } + + alias currentUser() { + auth().id + } + + model User { + id String @id + opened Boolean @default(allowAll()) + + @@allow('all', allowAll() && currentUser()) + } + `); + + expect( + await loadModelWithError(` + ${prelude} + + alias foo() { + "ok" + } + + model A { + id String @id + opened Boolean @default(foo()) + + @@allow('all', foo()) + } + `) + ).toContain(`Value is not assignable to parameter`); + }); }); diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index 1e0d22d67..e4633eb65 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -13,6 +13,7 @@ export enum ExpressionContext { AccessPolicy = 'AccessPolicy', ValidationRule = 'ValidationRule', Index = 'Index', + AliasFunction = 'AliasFunction', } export const STD_LIB_MODULE_NAME = 'stdlib.zmodel'; diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts index 801db4d4f..68f5ac110 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -24,7 +24,15 @@ import { getContainerOfType } from 'langium'; import { P, match } from 'ts-pattern'; import { ExpressionContext } from './constants'; import { getEntityCheckerFunctionName } from './names'; -import { getIdFields, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from './utils'; +import { + getIdFields, + getLiteral, + hasAuthInvocation, + isAliasExpr, + isDataModelFieldReference, + isFromStdlib, + isFutureExpr, +} from './utils'; export class TypeScriptExpressionTransformerError extends Error { constructor(message: string) { @@ -142,6 +150,15 @@ export class TypeScriptExpressionTransformer { const funcName = expr.function.ref.name; const isStdFunc = isFromStdlib(expr.function.ref); + const isAlias = isAliasExpr(expr); + + if (isAlias) { + // if the function is an alias, we need to resolve it to the actual alias declaration + const hasAuth = hasAuthInvocation(expr); + const aliasInvocation = `${expr.function.ref.name}(${hasAuth ? 'user' : ''})`; + // const aliasExpression = `${expr.function.ref.expression?.$cstNode?.text}`; + return aliasInvocation; + } if (!isStdFunc) { throw new TypeScriptExpressionTransformerError('User-defined functions are not supported yet'); diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 09a538f77..5051c05be 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -1,4 +1,5 @@ import { + AbstractCallable, AstNode, Attribute, AttributeParam, @@ -13,6 +14,7 @@ import { FunctionDecl, GeneratorDecl, InternalAttribute, + InvocationExpr, isArrayExpr, isConfigArrayExpr, isDataModel, @@ -426,7 +428,7 @@ export function parseOptionAsStrings(options: PluginDeclaredOptions, optionName: } } -export function getFunctionExpressionContext(funcDecl: FunctionDecl) { +export function getFunctionExpressionContext(funcDecl: AbstractCallable) { const funcAllowedContext: ExpressionContext[] = []; const funcAttr = funcDecl.attributes.find((attr) => attr.decl.$refText === '@@@expressionContext'); if (funcAttr) { @@ -446,10 +448,21 @@ export function isFutureExpr(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); } +export function isAliasExpr(node: AstNode) { + return isInvocationExpr(node) && node.function.ref?.$type === 'AliasDecl'; +} + export function isAuthInvocation(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); } +export function hasAuthInvocation(node: AstNode) { + return ( + isAuthInvocation(node) || + (isAliasExpr(node) && (node as InvocationExpr).function?.ref?.expression?.$cstNode?.text.includes('auth()')) + ); +} + export function isFromStdlib(node: AstNode) { const model = getContainingModel(node); return !!model && !!model.$document && model.$document.uri.path.endsWith(STD_LIB_MODULE_NAME); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0c0d7f99..bb9cb3860 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4128,7 +4128,7 @@ packages: engines: {node: '>= 14'} concat-map@0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -4566,7 +4566,7 @@ packages: engines: {node: '>=4'} ee-first@1.1.1: - resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} electron-to-chromium@1.4.814: resolution: {integrity: sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==} @@ -6143,7 +6143,7 @@ packages: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} media-typer@0.3.0: - resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} merge-descriptors@1.0.1: @@ -7763,7 +7763,7 @@ packages: superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superjson@1.13.3: resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} @@ -7776,6 +7776,7 @@ packages: supertest@6.3.4: resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} engines: {node: '>=6.4.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -8321,7 +8322,7 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} utils-merge@1.0.1: - resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} uuid@10.0.0: diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index 3d9e75f98..642f6f115 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -1,5 +1,7 @@ /// +import fs from 'fs'; +import path from 'path'; import { loadSchema } from '@zenstackhq/testtools'; describe('Policy plugin tests', () => { @@ -108,4 +110,77 @@ model M { (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 123 }] } } }) ).toEqual(expect.objectContaining({ AND: [{ AND: [] }, { value: { gt: 10 } }] })); }); + + it('alias expressions', async () => { + const { policy, projectDir } = await loadSchema( + ` + alias allowAll() { + true + } + + alias defaultTitle() { + 'Default Title' + } + + alias currentUser() { + auth().id + } + + model Post { + id Int @id @default(autoincrement()) + title String @default(defaultTitle()) + published Boolean @default(allowAll()) + + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + @@allow('read', allowAll()) + @@allow('create,update,delete', currentUser() == authorId && published) + } + + model User { + id String @id @default(cuid()) + name String? + posts Post[] + + @@allow('all', allowAll()) + } + `, + { + compile: false, + generateNoCompile: true, + output: 'out/', + } + ); + + // Test allowAll alias used in policy and default + expect((policy.policy.post.modelLevel.read.guard as Function)({}, undefined)).toEqual({ AND: [] }); + expect((policy.policy.user.modelLevel.read.guard as Function)({}, undefined)).toEqual({ AND: [] }); + + // Test currentUser alias used in policy + expect( + (policy.policy.post.modelLevel.create.guard as Function)( + { user: { id: 'u1' }, authorId: 'u1', published: true }, + undefined + ) + ).toEqual({ AND: [{ authorId: { equals: 'u1' } }, { published: true }] }); + expect( + (policy.policy.post.modelLevel.create.guard as Function)( + { user: { id: 'u2' }, authorId: 'u1', published: true }, + undefined + ) + ).toEqual({ AND: [{ authorId: { equals: 'u2' } }, { published: true }] }); + + const content = fs.readFileSync(path.join(projectDir, 'out/policy.ts'), 'utf-8'); + expect(content.replace(/\s+/g, ' ')).toContain(`function allowAll(): any { return true; }`); + expect(content.replace(/\s+/g, ' ')).toContain(`function defaultTitle(): any { return 'Default Title'; }`); + expect(content.replace(/\s+/g, ' ')).toContain( + `function currentUser(user: PermissionCheckerContext["user"]): any { return user?.id; }` + ); + + // // Test direct alias function calls + // expect((allowAll as Function)()).toEqual(true); + // expect((defaultTitle as Function)()).toEqual('Default Title'); + // expect((currentUser as Function)({ id: 'u1' })).toEqual('u1'); + }); }); From 25b5e97b750892b6c1f8a5543f935ff16f6d9d29 Mon Sep 17 00:00:00 2001 From: augustin Date: Mon, 21 Jul 2025 10:16:36 +0200 Subject: [PATCH 02/14] fix test --- .../schema/tests/schema/validation/attribute-validation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 923081813..840429eae 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -406,7 +406,7 @@ describe('Attribute tests', () => { id String @id @default(foo()) } `) - ).toContain(`Could not resolve reference to FunctionDecl named 'foo'.`); + ).toContain(`Could not resolve reference to AbstractCallable named 'foo'.`); expect( await loadModelWithError(` From 791bc5223b7792038ab0adf94a7194dfb27c287f Mon Sep 17 00:00:00 2001 From: augustin Date: Wed, 23 Jul 2025 10:42:54 +0200 Subject: [PATCH 03/14] fix: link alias references to first matching model field --- .../src/language-server/validator/utils.ts | 6 +- .../src/language-server/zmodel-linker.ts | 57 ++++++++++++++++++- .../src/language-server/zmodel-scope.ts | 18 +----- .../enhancer/policy/expression-writer.ts | 2 +- .../enhancer/policy/policy-guard-generator.ts | 38 ------------- .../src/plugins/enhancer/policy/utils.ts | 12 +++- packages/schema/src/utils/ast-utils.ts | 8 +-- .../validation/attribute-validation.test.ts | 34 +++++++++-- .../src/typescript-expression-transformer.ts | 20 ++----- 9 files changed, 102 insertions(+), 93 deletions(-) diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 3ce887f79..2ac4e2250 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -87,6 +87,8 @@ export function mapBuiltinTypeToExpressionType( case 'Int': case 'Float': case 'Null': + case 'Object': + case 'Unsupported': return type; case 'BigInt': return 'Int'; @@ -95,10 +97,6 @@ export function mapBuiltinTypeToExpressionType( case 'Json': case 'Bytes': return 'Any'; - case 'Object': - return 'Object'; - case 'Unsupported': - return 'Unsupported'; } } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index dc08e9a42..52c10b926 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -10,6 +10,7 @@ import { DataModelFieldType, Enum, EnumField, + Expression, ExpressionType, FunctionDecl, FunctionParam, @@ -27,16 +28,20 @@ import { ThisExpr, TypeDefFieldType, UnaryExpr, + isAliasDecl, isArrayExpr, + isBinaryExpr, isBooleanLiteral, isDataModel, isDataModelField, isDataModelFieldType, isEnum, + isModel, isNumberLiteral, isReferenceExpr, isStringLiteral, isTypeDefField, + isUnaryExpr, } from '@zenstackhq/language/ast'; import { getAuthDecl, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; import { @@ -155,10 +160,16 @@ export class ZModelLinker extends DefaultLinker { break; case ReferenceExpr: - this.resolveReference(node as ReferenceExpr, document, extraScopes); + // If the reference comes from an alias, we resolve it against the first matching data model + if (getContainerOfType(node, isAliasDecl)) { + this.resolveAliasExpr(node, document); + } else { + this.resolveReference(node as ReferenceExpr, document, extraScopes); + } break; case MemberAccessExpr: + // TODO: check from alias ? this.resolveMemberAccess(node as MemberAccessExpr, document, extraScopes); break; @@ -261,6 +272,7 @@ export class ZModelLinker extends DefaultLinker { if (node.target.ref.$type === EnumField) { this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); } else { + // TODO: if the reference is from an alias, we should resolve it against the first matching data model this.resolveToDeclaredType(node, (node.target.ref as DataModelField | FunctionParam).type); } } @@ -300,9 +312,8 @@ export class ZModelLinker extends DefaultLinker { } else if (isFutureExpr(node)) { // future() function is resolved to current model node.$resolvedType = { decl: getContainingDataModel(node) }; - } else if (isAliasInvocation(node)) { + } else if (isAliasInvocation(node) || !!getContainerOfType(node, isAliasDecl)) { // function is resolved to matching alias declaration - const expressionType = funcDecl.expression?.$type; if (!expressionType) { this.createLinkingError({ @@ -448,6 +459,46 @@ export class ZModelLinker extends DefaultLinker { node.$resolvedType = node.value.$resolvedType; } + private resolveAliasExpr(node: AstNode, document: LangiumDocument) { + const container = getContainerOfType(node, isAliasDecl); + if (!container) { + return; + } + const model = getContainerOfType(node, isModel); + const models = model?.declarations.filter(isDataModel) ?? []; + // Find the first model that has the alias reference as a field + const matchingModel = models.find((model) => model.fields.some((f) => f.name === node.$cstNode?.text)); + if (!matchingModel) { + this.createLinkingError({ + reference: (node as ReferenceExpr).target, + container: node, + property: 'target', + }); + return; + } + + const scopeProvider = (name: string) => + getModelFieldsWithBases(matchingModel).find((field) => field.name === name); + + const visitExpr = (node: Expression) => { + if (isReferenceExpr(node)) { + const resolved = this.resolveFromScopeProviders(node, 'target', document, [scopeProvider]); + if (resolved) { + this.resolveToDeclaredType(node, (resolved as DataModelField).type); + } else { + this.unresolvableRefExpr(node); + } + } else if (isBinaryExpr(node)) { + visitExpr(node.left); + visitExpr(node.right); + } else if (isUnaryExpr(node)) { + visitExpr(node.operand); + } + }; + + visitExpr(container.expression); + } + private unresolvableRefExpr(item: ReferenceExpr) { const ref = item.target as DefaultReference; ref._ref = this.createLinkingError({ diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 669c7721e..c75978a04 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -8,7 +8,6 @@ import { isMemberAccessExpr, isModel, isReferenceExpr, - isAliasDecl, isThisExpr, isTypeDef, isTypeDefField, @@ -70,16 +69,6 @@ export class ZModelScopeComputation extends DefaultScopeComputation { ); result.push(desc); } - - if (isAliasDecl(node)) { - // add alias decls to the global scope - const desc = this.services.workspace.AstNodeDescriptionProvider.createDescription( - node, - node.name, - document - ); - result.push(desc); - } } return result; @@ -139,11 +128,6 @@ export class ZModelScopeProvider extends DefaultScopeProvider { } } - // if (isAliasExpr(context.container)) { - // // resolve `[rule]()` to the containing model - // return this.createScopeForContainingModel(context.container, this.getGlobalScope('AliasDecl', context)); - // } - return super.getScope(context); } @@ -235,7 +219,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { } } - private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false) { + private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false): Scope { if (isDataModel(node)) { return this.createScopeForNodes(getModelFieldsWithBases(node), globalScope); } else if (includeTypeDefScope && isTypeDef(node)) { diff --git a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts index 344940c69..0b4172da7 100644 --- a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts +++ b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts @@ -807,7 +807,7 @@ export class ExpressionWriter { ); }); } else if (isAliasDecl(funcDecl)) { - // noop + this.write(funcDecl.expression); } else { throw new PluginError(name, `Unsupported function ${funcDecl.name}`); } diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index afa0968bf..cac87c9af 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -12,7 +12,6 @@ import { isReferenceExpr, isThisExpr, isTypeDef, - isAliasDecl, } from '@zenstackhq/language/ast'; import { PolicyCrudKind, type PolicyOperationKind } from '@zenstackhq/runtime'; @@ -62,8 +61,6 @@ export class PolicyGenerator { this.writeImports(model, output, sf); - this.writeAliasFunctions(model); - const models = getDataModels(model); const writer = new FastWriter(); @@ -471,41 +468,6 @@ export class PolicyGenerator { writer.write(`guard: ${guardFunc.name},`); } - /** - * Generates functions for the Aliases - */ - private writeAliasFunctions(model: Model) { - for (const decl of model.declarations) { - if (isAliasDecl(decl)) { - const alias = decl; - const params = alias.params?.map((p) => ({ name: p.name, type: 'any' })) ?? []; - if (alias.expression.$cstNode?.text.includes('auth()')) { - params.push({ - name: 'user', - type: 'PermissionCheckerContext["user"]', - }); - } - const transformer = new TypeScriptExpressionTransformer({ - context: ExpressionContext.AliasFunction, - }); - const writer = new FastWriter(); - try { - writer.write('return '); - writer.write(transformer.transform(alias.expression, false)); - writer.write(';'); - } catch (e) { - writer.write('return undefined /* erreur de transformation de la règle */;'); - } - this.extraFunctions.push({ - name: alias.name, - returnType: 'any', - parameters: params, - statements: [writer.result], - }); - } - } - } - // writes `permissionChecker: ...` for a given policy operation kind private writePermissionChecker( model: DataModel, diff --git a/packages/schema/src/plugins/enhancer/policy/utils.ts b/packages/schema/src/plugins/enhancer/policy/utils.ts index 978917f5b..43d811fdf 100644 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ b/packages/schema/src/plugins/enhancer/policy/utils.ts @@ -43,7 +43,12 @@ import deepmerge from 'deepmerge'; import { getContainerOfType, streamAllContents, streamAst, streamContents } from 'langium'; import { FunctionDeclarationStructure, OptionalKind } from 'ts-morph'; import { name } from '..'; -import { isCheckInvocation, isCollectionPredicate, isFutureInvocation } from '../../../utils/ast-utils'; +import { + isAliasInvocation, + isCheckInvocation, + isCollectionPredicate, + isFutureInvocation, +} from '../../../utils/ast-utils'; import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; /** @@ -312,7 +317,9 @@ export function generateQueryGuardFunction( // future().??? isFutureExpr(child) || // field reference - (isReferenceExpr(child) && isDataModelField(child.target.ref)) + (isReferenceExpr(child) && isDataModelField(child.target.ref)) || + // TODO: field access from alias expression + isAliasInvocation(child) ) ); @@ -545,6 +552,7 @@ export function isEnumReferenced(model: Model, decl: Enum): unknown { function hasCrossModelComparison(expr: Expression) { return streamAst(expr).some((node) => { + // TODO: check cross model comparison in alias expression target if (isBinaryExpr(node) && ['==', '!=', '>', '<', '>=', '<=', 'in'].includes(node.operator)) { const leftRoot = getSourceModelOfFieldAccess(node.left); const rightRoot = getSourceModelOfFieldAccess(node.right); diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 261ece1ff..5aa02b4d5 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -42,12 +42,6 @@ import path from 'node:path'; import { URI, Utils } from 'vscode-uri'; import { findNodeModulesFile } from './pkg-utils'; -export function extractDataModelsWithAllowAliass(model: Model): DataModel[] { - return model.declarations.filter( - (d) => isDataModel(d) && d.attributes.some((attr) => attr.decl.ref?.name === '@@allow') - ) as DataModel[]; -} - type BuildReference = ( node: AstNode, property: string, @@ -323,7 +317,7 @@ export function getAllLoadedAndReachableDataModelsAndTypeDefs( } /** - * Gets all data models and type defs from all loaded documents + * Gets all alias declarations from all loaded documents */ export function getAllLoadedAlias(langiumDocuments: LangiumDocuments) { return langiumDocuments.all diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 840429eae..f29b682d6 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1397,12 +1397,12 @@ describe('Attribute tests', () => { ${prelude} alias foo() { - true + opened } model A { id String @id - opened Boolean @default(foo()) + opened Boolean @default(true) @@allow('all', foo()) } @@ -1415,15 +1415,37 @@ describe('Attribute tests', () => { true } + alias defaultTitle() { + 'Default Title' + } + alias currentUser() { - auth().id + auth().id + } + + alias ownPublishedPosts() { + currentUser() != null && published + } + + model Post { + id Int @id @default(autoincrement()) + title String + published Boolean @default(true) + + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + @@allow('read', true) + @@deny('all', !ownPublishedPosts()) } + model User { - id String @id - opened Boolean @default(allowAll()) + id String @id @default(cuid()) + name String? + posts Post[] - @@allow('all', allowAll() && currentUser()) + @@allow('all', allowAll()) } `); diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts index 68f5ac110..a4a2e520d 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -24,15 +24,7 @@ import { getContainerOfType } from 'langium'; import { P, match } from 'ts-pattern'; import { ExpressionContext } from './constants'; import { getEntityCheckerFunctionName } from './names'; -import { - getIdFields, - getLiteral, - hasAuthInvocation, - isAliasExpr, - isDataModelFieldReference, - isFromStdlib, - isFutureExpr, -} from './utils'; +import { getIdFields, getLiteral, isAliasExpr, isDataModelFieldReference, isFromStdlib, isFutureExpr } from './utils'; export class TypeScriptExpressionTransformerError extends Error { constructor(message: string) { @@ -97,6 +89,7 @@ export class TypeScriptExpressionTransformer { return this.this(expr as ThisExpr); case ReferenceExpr: + // TODO: ensure referenceExpr from alias is resolved return this.reference(expr as ReferenceExpr); case InvocationExpr: @@ -153,11 +146,8 @@ export class TypeScriptExpressionTransformer { const isAlias = isAliasExpr(expr); if (isAlias) { - // if the function is an alias, we need to resolve it to the actual alias declaration - const hasAuth = hasAuthInvocation(expr); - const aliasInvocation = `${expr.function.ref.name}(${hasAuth ? 'user' : ''})`; - // const aliasExpression = `${expr.function.ref.expression?.$cstNode?.text}`; - return aliasInvocation; + // if the function invocation comes from an alias, we transform its expression + return this.transform(expr.function.ref.expression!, normalizeUndefined); } if (!isStdFunc) { @@ -404,7 +394,7 @@ export class TypeScriptExpressionTransformer { private reference(expr: ReferenceExpr) { if (!expr.target.ref) { - throw new TypeScriptExpressionTransformerError(`Unresolved ReferenceExpr`); + throw new TypeScriptExpressionTransformerError(`Unresolved ReferenceExpr: ${expr.$cstNode?.text}`); } if (isEnumField(expr.target.ref)) { From afa982d0a69face8a5297424883decd3c405f37a Mon Sep 17 00:00:00 2001 From: augustin Date: Wed, 23 Jul 2025 11:08:10 +0200 Subject: [PATCH 04/14] delete tests that are no longer relevant --- tests/integration/tests/plugins/policy.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index 642f6f115..cdbdd747e 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -170,17 +170,5 @@ model M { undefined ) ).toEqual({ AND: [{ authorId: { equals: 'u2' } }, { published: true }] }); - - const content = fs.readFileSync(path.join(projectDir, 'out/policy.ts'), 'utf-8'); - expect(content.replace(/\s+/g, ' ')).toContain(`function allowAll(): any { return true; }`); - expect(content.replace(/\s+/g, ' ')).toContain(`function defaultTitle(): any { return 'Default Title'; }`); - expect(content.replace(/\s+/g, ' ')).toContain( - `function currentUser(user: PermissionCheckerContext["user"]): any { return user?.id; }` - ); - - // // Test direct alias function calls - // expect((allowAll as Function)()).toEqual(true); - // expect((defaultTitle as Function)()).toEqual('Default Title'); - // expect((currentUser as Function)({ id: 'u1' })).toEqual('u1'); }); }); From 6d2a08b93ed6338fe790f3cf158509d937337c2f Mon Sep 17 00:00:00 2001 From: augustin Date: Fri, 25 Jul 2025 03:37:40 +0200 Subject: [PATCH 05/14] fix: link invoked alias to its declaration --- .../attribute-application-validator.ts | 18 ++++++++++++++- .../validator/expression-validator.ts | 4 ++-- .../src/language-server/zmodel-linker.ts | 23 +++++++++---------- .../src/language-server/zmodel-semantic.ts | 3 ++- .../src/plugins/enhancer/policy/utils.ts | 2 +- packages/sdk/src/utils.ts | 14 +++++++---- .../integration/tests/plugins/policy.test.ts | 2 +- 7 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 0efa760b8..68fc5a1ed 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -8,6 +8,7 @@ import { DataModelFieldAttribute, InternalAttribute, ReferenceExpr, + isAliasDecl, isArrayExpr, isAttribute, isDataModel, @@ -28,7 +29,12 @@ import { import { ValidationAcceptor, streamAllContents, streamAst } from 'langium'; import pluralize from 'pluralize'; import { AstValidator } from '../types'; -import { getStringLiteral, mapBuiltinTypeToExpressionType, typeAssignable } from './utils'; +import { + getStringLiteral, + mapBuiltinTypeToExpressionType, + mappedRawExpressionTypeToResolvedShape, + typeAssignable, +} from './utils'; // a registry of function handlers marked with @check const attributeCheckers = new Map(); @@ -264,6 +270,16 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at let dstType = param.type.type; let dstIsArray = param.type.array; + if (isAliasDecl(arg.$resolvedType?.decl)) { + if (dstType === 'ContextType') { + // TODO: what is context type? Passed to true to avoid error, to be fixed later + return true; + } + const aliasExpression = arg.$resolvedType.decl.expression; + const mappedType = mappedRawExpressionTypeToResolvedShape(aliasExpression.$type); + return dstType === mappedType; + } + if (dstType === 'ContextType') { // ContextType is inferred from the attribute's container's type if (isDataModelField(attr.$container)) { diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 06083e9e4..905440741 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -50,9 +50,9 @@ export default class ExpressionValidator implements AstValidator { } return false; }); - if (!hasReferenceResolutionError) { + if (hasReferenceResolutionError) { // report silent errors not involving linker errors - accept('error', 'Expression cannot be resolved', { + accept('error', `Expression cannot be resolved: ${expr.$cstNode?.text}`, { node: expr, }); } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 52c10b926..8d9afb748 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -67,7 +67,7 @@ import { isAliasInvocation, } from '../utils/ast-utils'; import { isMemberContainer } from './utils'; -import { mapBuiltinTypeToExpressionType, mappedRawExpressionTypeToResolvedShape } from './validator/utils'; +import { mapBuiltinTypeToExpressionType } from './validator/utils'; interface DefaultReference extends Reference { _ref?: AstNode | LinkingError; @@ -312,19 +312,18 @@ export class ZModelLinker extends DefaultLinker { } else if (isFutureExpr(node)) { // future() function is resolved to current model node.$resolvedType = { decl: getContainingDataModel(node) }; - } else if (isAliasInvocation(node) || !!getContainerOfType(node, isAliasDecl)) { + } else if (isAliasInvocation(node)) { // function is resolved to matching alias declaration - const expressionType = funcDecl.expression?.$type; - if (!expressionType) { - this.createLinkingError({ - reference: node.function, - container: node, - property: 'alias', - }); - return; + const containingAlias = getContainerOfType(node, isAliasDecl); + const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? []; + const matchingAlias = + isAliasInvocation(node) && !containingAlias + ? allAlias.find((alias) => alias.name === node.function.$refText) + : containingAlias; + + if (matchingAlias) { + node.$resolvedType = { decl: matchingAlias, nullable: false }; } - const mappedType = mappedRawExpressionTypeToResolvedShape(expressionType); - this.resolveToBuiltinTypeOrDecl(node, mappedType); } else { this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType); } diff --git a/packages/schema/src/language-server/zmodel-semantic.ts b/packages/schema/src/language-server/zmodel-semantic.ts index 2e24cdb7c..b1d2f3fc5 100644 --- a/packages/schema/src/language-server/zmodel-semantic.ts +++ b/packages/schema/src/language-server/zmodel-semantic.ts @@ -1,4 +1,5 @@ import { + isAliasDecl, isAttribute, isAttributeArg, isConfigField, @@ -83,7 +84,7 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'function', type: SemanticTokenTypes.function, }); - } else if (isFunctionDecl(node) || isAttribute(node)) { + } else if (isFunctionDecl(node) || isAliasDecl(node) || isAttribute(node)) { acceptor({ node, property: 'name', diff --git a/packages/schema/src/plugins/enhancer/policy/utils.ts b/packages/schema/src/plugins/enhancer/policy/utils.ts index 43d811fdf..11a902822 100644 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ b/packages/schema/src/plugins/enhancer/policy/utils.ts @@ -14,8 +14,8 @@ import { getIdFields, getLiteral, getQueryGuardFunctionName, - isAuthInvocation, hasAuthInvocation, + isAuthInvocation, isDataModelFieldReference, isEnumFieldReference, isFromStdlib, diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 5051c05be..67dc6d715 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -14,7 +14,7 @@ import { FunctionDecl, GeneratorDecl, InternalAttribute, - InvocationExpr, + isAliasDecl, isArrayExpr, isConfigArrayExpr, isDataModel, @@ -41,6 +41,7 @@ import fs from 'node:fs'; import path from 'path'; import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; import { PluginError, type PluginDeclaredOptions, type PluginOptions } from './types'; +import { streamAst } from 'langium'; /** * Gets data models in the ZModel schema. @@ -457,10 +458,13 @@ export function isAuthInvocation(node: AstNode) { } export function hasAuthInvocation(node: AstNode) { - return ( - isAuthInvocation(node) || - (isAliasExpr(node) && (node as InvocationExpr).function?.ref?.expression?.$cstNode?.text.includes('auth()')) - ); + return streamAst(node).some((node) => { + const hasAuth = + isAuthInvocation(node) || + (isAliasDecl(node.$resolvedType?.decl) && + node.$resolvedType?.decl.expression?.$cstNode?.text.includes('auth()')); + return hasAuth; + }); } export function isFromStdlib(node: AstNode) { diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index cdbdd747e..bf04393bb 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -112,7 +112,7 @@ model M { }); it('alias expressions', async () => { - const { policy, projectDir } = await loadSchema( + const { policy } = await loadSchema( ` alias allowAll() { true From c19996716de8dd6269bf0d874bfc5b53ea1161e2 Mon Sep 17 00:00:00 2001 From: augustin Date: Fri, 25 Jul 2025 05:30:29 +0200 Subject: [PATCH 06/14] fix tests --- .../attribute-application-validator.ts | 20 +++---- .../integration/tests/plugins/policy.test.ts | 52 ++++++++++++++++++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 68fc5a1ed..fda505901 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -270,16 +270,6 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at let dstType = param.type.type; let dstIsArray = param.type.array; - if (isAliasDecl(arg.$resolvedType?.decl)) { - if (dstType === 'ContextType') { - // TODO: what is context type? Passed to true to avoid error, to be fixed later - return true; - } - const aliasExpression = arg.$resolvedType.decl.expression; - const mappedType = mappedRawExpressionTypeToResolvedShape(aliasExpression.$type); - return dstType === mappedType; - } - if (dstType === 'ContextType') { // ContextType is inferred from the attribute's container's type if (isDataModelField(attr.$container)) { @@ -310,6 +300,16 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at } } + // alias expression is compared to corresponding expression resolved shape + if (isAliasDecl(arg.$resolvedType?.decl)) { + // TODO: what is context type? Passed to true to avoid error, to be fixed later + if (dstType === 'ContextType') return true; + + const alias = arg.$resolvedType.decl; + const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type); + return dstType === mappedAliasResolvedType || dstType === 'Any' || mappedAliasResolvedType === 'Any'; + } + // destination is field reference or transitive field reference, check if // argument is reference or array or reference if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index bf04393bb..d478da9a4 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -111,7 +111,7 @@ model M { ).toEqual(expect.objectContaining({ AND: [{ AND: [] }, { value: { gt: 10 } }] })); }); - it('alias expressions', async () => { + it('simple alias expressions', async () => { const { policy } = await loadSchema( ` alias allowAll() { @@ -171,4 +171,54 @@ model M { ) ).toEqual({ AND: [{ authorId: { equals: 'u2' } }, { published: true }] }); }); + + it('complex alias expressions', async () => { + const model = ` + alias currentUserId() { + auth().id + } + + alias complexAlias() { + auth().cart.tasks?[id == 123] && value >10 && currentUserId() != null + } + + model User { + id Int @id @default(autoincrement()) + cart Cart? + } + + model Cart { + id Int @id @default(autoincrement()) + tasks Task[] + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } + + model Task { + id Int @id @default(autoincrement()) + cart Cart @relation(fields: [cartId], references: [id]) + cartId Int + value Int + @@allow('read', complexAlias()) + } + `; + + const { policy } = await loadSchema(model, { + compile: false, + generateNoCompile: true, + output: 'out/', + }); + + expect( + (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 1 }] } } }) + ).toEqual( + expect.objectContaining({ + AND: [{ AND: [{ OR: [] }, { value: { gt: 10 } }] }, { OR: [] }], + }) + ); + + expect( + (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 123 }] } } }) + ).toEqual(expect.objectContaining({ AND: [{ AND: [{ AND: [] }, { value: { gt: 10 } }] }, { OR: [] }] })); + }); }); From 20087f635704f9c28f9f67f823af4c03f21da3db Mon Sep 17 00:00:00 2001 From: augustin Date: Fri, 25 Jul 2025 19:53:01 +0200 Subject: [PATCH 07/14] fix enum in alias expression --- .../src/language-server/zmodel-linker.ts | 32 ++++++++++++------- .../integration/tests/plugins/policy.test.ts | 22 +++++++++---- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 8d9afb748..bde44cc4a 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -144,6 +144,13 @@ export class ZModelLinker extends DefaultLinker { } private resolve(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[] = []) { + // if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes + if (isDataModelField(node) && node.type.reference?.ref && isEnum(node.type.reference.ref)) { + const contextEnum = node.type.reference.ref as Enum; + const enumScope: ScopeProvider = (name) => contextEnum?.fields?.find((f) => f.name === name); + extraScopes = [enumScope, ...extraScopes]; + } + switch (node.$type) { case StringLiteral: case NumberLiteral: @@ -160,12 +167,7 @@ export class ZModelLinker extends DefaultLinker { break; case ReferenceExpr: - // If the reference comes from an alias, we resolve it against the first matching data model - if (getContainerOfType(node, isAliasDecl)) { - this.resolveAliasExpr(node, document); - } else { - this.resolveReference(node as ReferenceExpr, document, extraScopes); - } + this.resolveReference(node as ReferenceExpr, document, extraScopes); break; case MemberAccessExpr: @@ -265,6 +267,10 @@ export class ZModelLinker extends DefaultLinker { } private resolveReference(node: ReferenceExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { + // If the reference comes from an alias, we resolve it against the first matching data model + if (getContainerOfType(node, isAliasDecl)) { + this.resolveAliasExpr(node as ReferenceExpr, document); + } this.resolveDefault(node, document, extraScopes); if (node.target.ref) { @@ -468,11 +474,6 @@ export class ZModelLinker extends DefaultLinker { // Find the first model that has the alias reference as a field const matchingModel = models.find((model) => model.fields.some((f) => f.name === node.$cstNode?.text)); if (!matchingModel) { - this.createLinkingError({ - reference: (node as ReferenceExpr).target, - container: node, - property: 'target', - }); return; } @@ -481,6 +482,11 @@ export class ZModelLinker extends DefaultLinker { const visitExpr = (node: Expression) => { if (isReferenceExpr(node)) { + // enums in alias expressions are already resolved + if (isEnum(node.target.ref?.$container)) { + return; + } + const resolved = this.resolveFromScopeProviders(node, 'target', document, [scopeProvider]); if (resolved) { this.resolveToDeclaredType(node, (resolved as DataModelField).type); @@ -585,6 +591,10 @@ export class ZModelLinker extends DefaultLinker { //#region Utils private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType | TypeDefFieldType) { + // enums from alias expressions are already resolved and do not exist in the scope + if (!type) { + return; + } let nullable = false; if (isDataModelFieldType(type) || isTypeDefField(type)) { nullable = type.optional; diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index d478da9a4..f19c213e4 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -174,12 +174,22 @@ model M { it('complex alias expressions', async () => { const model = ` + enum TaskStatus { + TODO + IN_PROGRESS + DONE + } + + alias isInProgress() { + status == IN_PROGRESS + } + alias currentUserId() { auth().id } alias complexAlias() { - auth().cart.tasks?[id == 123] && value >10 && currentUserId() != null + status == IN_PROGRESS && auth().cart.tasks?[id == 123] && value >10 && currentUserId() != null } model User { @@ -196,6 +206,7 @@ model M { model Task { id Int @id @default(autoincrement()) + status TaskStatus @default(TODO) cart Cart @relation(fields: [cartId], references: [id]) cartId Int value Int @@ -213,12 +224,11 @@ model M { (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 1 }] } } }) ).toEqual( expect.objectContaining({ - AND: [{ AND: [{ OR: [] }, { value: { gt: 10 } }] }, { OR: [] }], + AND: [ + { AND: [{ AND: [{ status: { equals: 'IN_PROGRESS' } }, { OR: [] }] }, { value: { gt: 10 } }] }, + { OR: [] }, + ], }) ); - - expect( - (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 123 }] } } }) - ).toEqual(expect.objectContaining({ AND: [{ AND: [{ AND: [] }, { value: { gt: 10 } }] }, { OR: [] }] })); }); }); From 465956a719337668182c2ca163f2e4021cc6b84b Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 15:55:32 +0200 Subject: [PATCH 08/14] fix: add alias scope --- .../src/language-server/zmodel-linker.ts | 70 ++++------- .../src/language-server/zmodel-scope.ts | 106 ++++++++++++++++ .../integration/tests/plugins/policy.test.ts | 115 ++++++++++++++++-- 3 files changed, 236 insertions(+), 55 deletions(-) diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index bde44cc4a..ec0a91652 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -30,7 +30,6 @@ import { UnaryExpr, isAliasDecl, isArrayExpr, - isBinaryExpr, isBooleanLiteral, isDataModel, isDataModelField, @@ -41,7 +40,6 @@ import { isReferenceExpr, isStringLiteral, isTypeDefField, - isUnaryExpr, } from '@zenstackhq/language/ast'; import { getAuthDecl, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; import { @@ -171,7 +169,6 @@ export class ZModelLinker extends DefaultLinker { break; case MemberAccessExpr: - // TODO: check from alias ? this.resolveMemberAccess(node as MemberAccessExpr, document, extraScopes); break; @@ -207,6 +204,10 @@ export class ZModelLinker extends DefaultLinker { this.resolveDataModelField(node as DataModelField, document, extraScopes); break; + case AliasDecl: + // Don't resolve alias declarations - they will be resolved when used + break; + default: this.resolveDefault(node, document, extraScopes); break; @@ -267,10 +268,6 @@ export class ZModelLinker extends DefaultLinker { } private resolveReference(node: ReferenceExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { - // If the reference comes from an alias, we resolve it against the first matching data model - if (getContainerOfType(node, isAliasDecl)) { - this.resolveAliasExpr(node as ReferenceExpr, document); - } this.resolveDefault(node, document, extraScopes); if (node.target.ref) { @@ -278,7 +275,6 @@ export class ZModelLinker extends DefaultLinker { if (node.target.ref.$type === EnumField) { this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); } else { - // TODO: if the reference is from an alias, we should resolve it against the first matching data model this.resolveToDeclaredType(node, (node.target.ref as DataModelField | FunctionParam).type); } } @@ -329,6 +325,18 @@ export class ZModelLinker extends DefaultLinker { if (matchingAlias) { node.$resolvedType = { decl: matchingAlias, nullable: false }; + + // Resolve the alias expression in the context of the containing model + const containingModel = getContainingDataModel(node); + if (containingModel && matchingAlias.expression) { + const scopeProvider = (name: string) => + getModelFieldsWithBases(containingModel).find((field) => field.name === name); + + // Ensure the alias expression is fully resolved in the current context + this.resolveExpressionInContext(matchingAlias.expression, document, containingModel, [ + scopeProvider, + ]); + } } } else { this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType); @@ -464,44 +472,14 @@ export class ZModelLinker extends DefaultLinker { node.$resolvedType = node.value.$resolvedType; } - private resolveAliasExpr(node: AstNode, document: LangiumDocument) { - const container = getContainerOfType(node, isAliasDecl); - if (!container) { - return; - } - const model = getContainerOfType(node, isModel); - const models = model?.declarations.filter(isDataModel) ?? []; - // Find the first model that has the alias reference as a field - const matchingModel = models.find((model) => model.fields.some((f) => f.name === node.$cstNode?.text)); - if (!matchingModel) { - return; - } - - const scopeProvider = (name: string) => - getModelFieldsWithBases(matchingModel).find((field) => field.name === name); - - const visitExpr = (node: Expression) => { - if (isReferenceExpr(node)) { - // enums in alias expressions are already resolved - if (isEnum(node.target.ref?.$container)) { - return; - } - - const resolved = this.resolveFromScopeProviders(node, 'target', document, [scopeProvider]); - if (resolved) { - this.resolveToDeclaredType(node, (resolved as DataModelField).type); - } else { - this.unresolvableRefExpr(node); - } - } else if (isBinaryExpr(node)) { - visitExpr(node.left); - visitExpr(node.right); - } else if (isUnaryExpr(node)) { - visitExpr(node.operand); - } - }; - - visitExpr(container.expression); + private resolveExpressionInContext( + expr: Expression, + document: LangiumDocument, + contextModel: DataModel, + extraScopes: ScopeProvider[] + ) { + // Resolve the expression with the model context scope + this.resolve(expr, document, extraScopes); } private unresolvableRefExpr(item: ReferenceExpr) { diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index c75978a04..212b77493 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -1,6 +1,7 @@ import { BinaryExpr, MemberAccessExpr, + isAliasDecl, isDataModel, isDataModelField, isEnumField, @@ -117,6 +118,11 @@ export class ZModelScopeProvider extends DefaultScopeProvider { override getScope(context: ReferenceInfo): Scope { if (isMemberAccessExpr(context.container) && context.container.operand && context.property === 'member') { + // Check if we're inside an alias first + const aliasDecl = getContainerOfType(context.container, isAliasDecl); + if (aliasDecl) { + return this.getAliasMemberAccessScope(context); + } return this.getMemberAccessScope(context); } @@ -126,6 +132,12 @@ export class ZModelScopeProvider extends DefaultScopeProvider { if (containerCollectionPredicate) { return this.getCollectionPredicateScope(context, containerCollectionPredicate); } + + // Check if we're inside an alias declaration - if so, get scope from containing model + const aliasDecl = getContainerOfType(context.container, isAliasDecl); + if (aliasDecl) { + return this.getAliasScope(context); + } } return super.getScope(context); @@ -243,6 +255,100 @@ export class ZModelScopeProvider extends DefaultScopeProvider { return EMPTY_SCOPE; } } + + private getAliasScope(context: ReferenceInfo): Scope { + const referenceType = this.reflection.getReferenceType(context); + const globalScope = this.getGlobalScope(referenceType, context); + + // In aliases, we want to resolve references against all possible models + const model = getContainerOfType(context.container, isModel); + if (!model) { + return globalScope; + } + + // Collect all fields from all models + const allFields: AstNode[] = []; + for (const decl of model.declarations) { + if (isDataModel(decl)) { + allFields.push(...getModelFieldsWithBases(decl)); + } + } + + return this.createScopeForNodes(allFields, globalScope); + } + + private getAliasMemberAccessScope(context: ReferenceInfo): Scope { + const referenceType = this.reflection.getReferenceType(context); + const globalScope = this.getGlobalScope(referenceType, context); + const node = context.container as MemberAccessExpr; + + // For member access in aliases, we need to check all possible contexts + if (isReferenceExpr(node.operand)) { + const operandName = node.operand.$cstNode?.text; + if (!operandName) { + return EMPTY_SCOPE; + } + + // Check if this is used in an invocation context + let invocationContext: AstNode | undefined = node.$container; + while (invocationContext && !isInvocationExpr(invocationContext)) { + invocationContext = invocationContext.$container; + } + + if (invocationContext && isInvocationExpr(invocationContext)) { + // Find the model where this invocation is used + const containingModel = getContainerOfType(invocationContext, isDataModel); + if (containingModel) { + const field = getModelFieldsWithBases(containingModel).find( + (f) => f.name === operandName + ); + if (field && field.type.reference?.ref) { + return this.createScopeForContainer(field.type.reference.ref, globalScope); + } + } + } + + // Otherwise, check all models for possible matches + const model = getContainerOfType(context.container, isModel); + if (!model) { + return EMPTY_SCOPE; + } + + // Collect all possible scopes from all models + const allScopes: Scope[] = []; + for (const decl of model.declarations) { + if (isDataModel(decl)) { + const field = getModelFieldsWithBases(decl).find( + (f) => f.name === operandName + ); + if (field && field.type.reference?.ref) { + allScopes.push(this.createScopeForContainer(field.type.reference.ref, globalScope)); + } + } + } + + // Combine all scopes + if (allScopes.length > 0) { + return this.combineScopes(allScopes); + } + } + + return EMPTY_SCOPE; + } + + private combineScopes(scopes: Scope[]): Scope { + const allElements: AstNodeDescription[] = []; + for (const scope of scopes) { + const elements = scope.getAllElements(); + for (const element of elements) { + // Avoid duplicates + if (!allElements.some(e => e.name === element.name && e.type === element.type)) { + allElements.push(element); + } + } + } + return new StreamScope(stream(allElements)); + } } function getCollectionPredicateContext(node: AstNode) { diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index f19c213e4..bf3474730 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -184,12 +184,18 @@ model M { status == IN_PROGRESS } - alias currentUserId() { - auth().id - } alias complexAlias() { - status == IN_PROGRESS && auth().cart.tasks?[id == 123] && value >10 && currentUserId() != null + status == IN_PROGRESS && value > 10 + } + + alias memberAccessAlias() { + cart.tasks?[id == 123] + } + + alias memberAccess() { + // new task can be created the cart contains tasks with status TODO... + cart.tasks?[status == TODO] } model User { @@ -211,6 +217,8 @@ model M { cartId Int value Int @@allow('read', complexAlias()) + @@allow('update', memberAccessAlias()) + @@allow('create', memberAccess()) } `; @@ -220,15 +228,104 @@ model M { output: 'out/', }); + // Test simple complex alias for read operation - requires status IN_PROGRESS and value > 10 expect( - (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 1 }] } } }) + (policy.policy.task.modelLevel.read.guard as Function)({ + status: 'IN_PROGRESS', + value: 15, + }) ).toEqual( expect.objectContaining({ - AND: [ - { AND: [{ AND: [{ status: { equals: 'IN_PROGRESS' } }, { OR: [] }] }, { value: { gt: 10 } }] }, - { OR: [] }, - ], + AND: expect.arrayContaining([{ status: { equals: 'IN_PROGRESS' } }, { value: { gt: 10 } }]), }) ); + + // Test member access alias for update operation - requires cart with tasks having id 123 + expect( + (policy.policy.task.modelLevel.update.guard as Function)({ + user: { cart: { tasks: [{ id: 123 }] } }, + }) + ).toEqual({ + cart: { + tasks: { + some: { + id: { equals: 123 }, + }, + }, + }, + }); + + // Test member access alias for create operation - requires cart with tasks having status TODO + expect( + (policy.policy.task.modelLevel.create.guard as Function)({ + user: { cart: { tasks: [{ status: 'TODO' }] } }, + }) + ).toEqual({ + cart: { + tasks: { + some: { + status: { equals: 'TODO' }, + }, + }, + }, + }); + }); + + it('simple member access in alias', async () => { + const model = ` + alias memberAccess() { + cart.tasks?[id == 123] + } + + model User { + id Int @id @default(autoincrement()) + cart Cart? + } + + model Cart { + id Int @id @default(autoincrement()) + tasks Task[] + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } + + model Task { + id Int @id @default(autoincrement()) + cart Cart @relation(fields: [cartId], references: [id]) + cartId Int + value Int + @@allow('create', memberAccess()) + } + `; + + const { policy } = await loadSchema(model, { + compile: false, + generateNoCompile: true, + output: 'out/', + }); + + // Test that the policy is correctly generated + expect(policy.policy.task.modelLevel.create.guard).toBeDefined(); + + // Test with cart containing matching task + expect( + (policy.policy.task.modelLevel.create.guard as Function)({ + user: { cart: { tasks: [{ id: 123 }] } }, + }) + ).toEqual({ cart: { tasks: { some: { id: { equals: 123 } } } } }); + + // Test with cart containing non-matching task - policy still generates filter + expect( + (policy.policy.task.modelLevel.create.guard as Function)({ + user: { cart: { tasks: [{ id: 456 }] } }, + }) + ).toEqual({ cart: { tasks: { some: { id: { equals: 123 } } } } }); + + // Test with empty cart - policy still generates filter + expect( + (policy.policy.task.modelLevel.create.guard as Function)({ + user: { cart: { tasks: [] } }, + }) + ).toEqual({ cart: { tasks: { some: { id: { equals: 123 } } } } }); }); }); From b67728c5713f96f18205dc473494856da0f78960 Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 17:06:53 +0200 Subject: [PATCH 09/14] fix test and clean up --- .../validator/expression-validator.ts | 5 +++-- .../src/language-server/zmodel-linker.ts | 20 ++----------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 905440741..1275d7bd8 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -4,6 +4,7 @@ import { DataModelAttribute, Expression, ExpressionType, + isAliasDecl, isArrayExpr, isDataModel, isDataModelAttribute, @@ -21,7 +22,7 @@ import { isDataModelFieldReference, isEnumFieldReference, } from '@zenstackhq/sdk'; -import { ValidationAcceptor, streamAst } from 'langium'; +import { ValidationAcceptor, getContainerOfType, streamAst } from 'langium'; import { findUpAst, getContainingDataModel } from '../../utils/ast-utils'; import { AstValidator } from '../types'; import { isAuthOrAuthMemberAccess, typeAssignable } from './utils'; @@ -33,7 +34,7 @@ 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)) { + if (isAuthInvocation(expr) && !getContainerOfType(expr, isAliasDecl)) { // check was done at link time accept( 'error', diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index ec0a91652..073fb3482 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -10,7 +10,6 @@ import { DataModelFieldType, Enum, EnumField, - Expression, ExpressionType, FunctionDecl, FunctionParam, @@ -333,9 +332,8 @@ export class ZModelLinker extends DefaultLinker { getModelFieldsWithBases(containingModel).find((field) => field.name === name); // Ensure the alias expression is fully resolved in the current context - this.resolveExpressionInContext(matchingAlias.expression, document, containingModel, [ - scopeProvider, - ]); + // Pass both the model scope and existing extraScopes + this.resolve(matchingAlias.expression, document, [scopeProvider, ...extraScopes]); } } } else { @@ -472,16 +470,6 @@ export class ZModelLinker extends DefaultLinker { node.$resolvedType = node.value.$resolvedType; } - private resolveExpressionInContext( - expr: Expression, - document: LangiumDocument, - contextModel: DataModel, - extraScopes: ScopeProvider[] - ) { - // Resolve the expression with the model context scope - this.resolve(expr, document, extraScopes); - } - private unresolvableRefExpr(item: ReferenceExpr) { const ref = item.target as DefaultReference; ref._ref = this.createLinkingError({ @@ -569,10 +557,6 @@ export class ZModelLinker extends DefaultLinker { //#region Utils private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType | TypeDefFieldType) { - // enums from alias expressions are already resolved and do not exist in the scope - if (!type) { - return; - } let nullable = false; if (isDataModelFieldType(type) || isTypeDefField(type)) { nullable = type.optional; From 8b6af1b6e8b0b489d8b8bde30fef679db0eb7250 Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 17:28:24 +0200 Subject: [PATCH 10/14] more cleanup --- packages/language/src/generated/grammar.ts | 412 +++++++++--------- packages/language/src/zmodel.langium | 12 +- .../src/language-server/zmodel-linker.ts | 63 +-- 3 files changed, 245 insertions(+), 242 deletions(-) diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 5ec36a1c1..075533ab0 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -98,42 +98,42 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@4" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@7" + "$ref": "#/rules@6" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@37" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@40" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@44" }, "arguments": [] }, @@ -147,7 +147,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@3" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -167,126 +167,6 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, - { - "$type": "ParserRule", - "name": "AliasDecl", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@70" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "Keyword", - "value": "alias" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@50" - }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "(" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "params", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@48" - }, - "arguments": [] - } - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "," - }, - { - "$type": "Assignment", - "feature": "params", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@48" - }, - "arguments": [] - } - } - ], - "cardinality": "*" - } - ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": ")" - }, - { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Assignment", - "feature": "expression", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "}" - }, - { - "$type": "Assignment", - "feature": "attributes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@57" - }, - "arguments": [] - }, - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, { "$type": "ParserRule", "name": "DataSource", @@ -328,7 +208,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@5" }, "arguments": [] }, @@ -388,7 +268,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@5" }, "arguments": [] }, @@ -444,7 +324,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@18" }, "arguments": [] } @@ -499,7 +379,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@7" }, "arguments": [] }, @@ -558,21 +438,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@24" }, "arguments": [] } @@ -594,7 +474,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@33" }, "arguments": [] }, @@ -680,21 +560,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@10" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } @@ -727,7 +607,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -746,7 +626,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -798,7 +678,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@15" }, "arguments": [], "cardinality": "?" @@ -833,7 +713,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@16" }, "arguments": [] } @@ -852,7 +732,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@16" }, "arguments": [] } @@ -897,7 +777,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] } @@ -934,14 +814,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@14" }, "arguments": [] } @@ -965,14 +845,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@14" }, "arguments": [] } @@ -1007,21 +887,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@26" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -1107,7 +987,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -1141,7 +1021,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@23" }, "arguments": [] } @@ -1160,7 +1040,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@23" }, "arguments": [] } @@ -1205,7 +1085,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1239,7 +1119,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1258,7 +1138,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1328,7 +1208,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1367,7 +1247,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@35" }, "arguments": [], "cardinality": "?" @@ -1398,7 +1278,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@34" }, "arguments": [] }, @@ -1469,7 +1349,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@27" }, "arguments": [] } @@ -1496,7 +1376,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@27" }, "arguments": [] }, @@ -1545,7 +1425,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1579,7 +1459,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@29" }, "arguments": [] }, @@ -1611,7 +1491,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@29" }, "arguments": [] } @@ -1641,7 +1521,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@30" }, "arguments": [] }, @@ -1690,7 +1570,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@30" }, "arguments": [] } @@ -1720,7 +1600,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@31" }, "arguments": [] }, @@ -1761,7 +1641,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@31" }, "arguments": [] } @@ -1791,7 +1671,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@32" }, "arguments": [] }, @@ -1832,7 +1712,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@32" }, "arguments": [] } @@ -1869,7 +1749,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] }, @@ -1882,56 +1762,56 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@19" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@20" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@28" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@26" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@24" }, "arguments": [] } @@ -1958,7 +1838,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@36" }, "arguments": [] } @@ -1977,7 +1857,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@36" }, "arguments": [] } @@ -2003,7 +1883,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -2080,7 +1960,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@38" + "$ref": "#/rules@37" }, "deprecatedSyntax": false } @@ -2099,7 +1979,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@38" + "$ref": "#/rules@37" }, "deprecatedSyntax": false } @@ -2154,7 +2034,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2225,7 +2105,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@39" }, "arguments": [] } @@ -2280,7 +2160,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@43" }, "arguments": [] } @@ -2393,7 +2273,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2464,7 +2344,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2592,7 +2472,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] } @@ -2659,7 +2539,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@45" }, "arguments": [] } @@ -2745,6 +2625,126 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, + { + "$type": "ParserRule", + "name": "AliasDecl", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@70" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "alias" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@50" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "(" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "params", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@48" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "params", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@48" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": ")" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "expression", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "}" + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@57" + }, + "arguments": [] + }, + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, { "$type": "ParserRule", "name": "FunctionDecl", @@ -2850,7 +2850,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] }, @@ -3715,7 +3715,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -3974,19 +3974,19 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@39" + "$ref": "#/rules@38" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@42" + "$ref": "#/rules@41" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@46" + "$ref": "#/rules@45" } } ] @@ -4007,7 +4007,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@3" + "$ref": "#/rules@46" } } ] @@ -4022,13 +4022,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@39" + "$ref": "#/rules@38" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@42" + "$ref": "#/rules@41" } } ] @@ -4043,13 +4043,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@41" + "$ref": "#/rules@40" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@45" + "$ref": "#/rules@44" } } ] @@ -4064,25 +4064,25 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@38" + "$ref": "#/rules@37" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@41" + "$ref": "#/rules@40" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@45" + "$ref": "#/rules@44" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@3" + "$ref": "#/rules@46" } } ] diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 10897db0f..5f90fc96e 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -13,13 +13,6 @@ ModelImport: AbstractDeclaration: DataSource | GeneratorDecl | Plugin | DataModel | TypeDef | Enum | FunctionDecl | AliasDecl | Attribute; -// alias -AliasDecl: - TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' '{' expression=Expression '}' (attributes+=InternalAttribute)*; - -// AliasExpr: -// function=[AliasDecl] '(' ArgumentList? ')'; - // datasource DataSource: TRIPLE_SLASH_COMMENT* 'datasource' name=RegularID '{' (fields+=ConfigField)* '}'; @@ -224,6 +217,11 @@ EnumField: (comments+=TRIPLE_SLASH_COMMENT)* name=RegularIDWithTypeNames (attributes+=DataModelFieldAttribute)*; +// alias +AliasDecl: + TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' '{' expression=Expression '}' (attributes+=InternalAttribute)*; + + // function FunctionDecl: TRIPLE_SLASH_COMMENT* 'function' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}' (attributes+=InternalAttribute)*; diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 073fb3482..bec35a6f3 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -141,13 +141,6 @@ export class ZModelLinker extends DefaultLinker { } private resolve(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[] = []) { - // if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes - if (isDataModelField(node) && node.type.reference?.ref && isEnum(node.type.reference.ref)) { - const contextEnum = node.type.reference.ref as Enum; - const enumScope: ScopeProvider = (name) => contextEnum?.fields?.find((f) => f.name === name); - extraScopes = [enumScope, ...extraScopes]; - } - switch (node.$type) { case StringLiteral: case NumberLiteral: @@ -314,28 +307,7 @@ export class ZModelLinker extends DefaultLinker { // future() function is resolved to current model node.$resolvedType = { decl: getContainingDataModel(node) }; } else if (isAliasInvocation(node)) { - // function is resolved to matching alias declaration - const containingAlias = getContainerOfType(node, isAliasDecl); - const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? []; - const matchingAlias = - isAliasInvocation(node) && !containingAlias - ? allAlias.find((alias) => alias.name === node.function.$refText) - : containingAlias; - - if (matchingAlias) { - node.$resolvedType = { decl: matchingAlias, nullable: false }; - - // Resolve the alias expression in the context of the containing model - const containingModel = getContainingDataModel(node); - if (containingModel && matchingAlias.expression) { - const scopeProvider = (name: string) => - getModelFieldsWithBases(containingModel).find((field) => field.name === name); - - // Ensure the alias expression is fully resolved in the current context - // Pass both the model scope and existing extraScopes - this.resolve(matchingAlias.expression, document, [scopeProvider, ...extraScopes]); - } - } + this.resolveAliasInvocation(node, document, extraScopes); } else { this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType); } @@ -524,6 +496,13 @@ export class ZModelLinker extends DefaultLinker { // } // + // if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes + if (getContainerOfType(node, isAliasDecl) && node.type.reference?.ref && isEnum(node.type.reference.ref)) { + const contextEnum = node.type.reference.ref as Enum; + const enumScope: ScopeProvider = (name) => contextEnum?.fields?.find((f) => f.name === name); + extraScopes = [enumScope, ...extraScopes]; + } + // make sure type is resolved first this.resolve(node.type, document, extraScopes); @@ -552,6 +531,32 @@ export class ZModelLinker extends DefaultLinker { } } + private resolveAliasInvocation(node: InvocationExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { + // function is resolved to matching alias declaration + const containingAlias = getContainerOfType(node, isAliasDecl); + const matchingAlias = containingAlias || this.findMatchingAlias(node); + + if (matchingAlias) { + node.$resolvedType = { decl: matchingAlias, nullable: false }; + + // Resolve the alias expression in the context of the containing model + const containingModel = getContainingDataModel(node); + if (containingModel && matchingAlias.expression) { + const scopeProvider = (name: string) => + getModelFieldsWithBases(containingModel).find((field) => field.name === name); + + // Ensure the alias expression is fully resolved in the current context + // Pass both the model scope and existing extraScopes + this.resolve(matchingAlias.expression, document, [scopeProvider, ...extraScopes]); + } + } + } + + private findMatchingAlias(node: InvocationExpr): AliasDecl | undefined { + const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? []; + return allAlias.find((alias) => alias.name === node.function.$refText); + } + //#endregion //#region Utils From be2fcd5e97b5600faf42f8ba1e4581009ffe9954 Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 19:45:36 +0200 Subject: [PATCH 11/14] implement requested changes from review --- .../attribute-application-validator.ts | 38 +- .../function-invocation-validator.ts | 6 - .../src/language-server/zmodel-scope.ts | 27 ++ .../enhancer/policy/constraint-transformer.ts | 20 +- .../enhancer/policy/policy-guard-generator.ts | 26 +- .../src/plugins/enhancer/policy/utils.ts | 20 +- packages/schema/src/utils/ast-utils.ts | 62 ++- .../src/typescript-expression-transformer.ts | 6 +- .../integration/tests/plugins/policy.test.ts | 355 +++++++++++------- 9 files changed, 394 insertions(+), 166 deletions(-) diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index fda505901..98359406e 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -1,4 +1,5 @@ import { + AliasDecl, ArrayExpr, Attribute, AttributeArg, @@ -300,14 +301,9 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at } } - // alias expression is compared to corresponding expression resolved shape + // Handle alias expressions by comparing to their resolved shape if (isAliasDecl(arg.$resolvedType?.decl)) { - // TODO: what is context type? Passed to true to avoid error, to be fixed later - if (dstType === 'ContextType') return true; - - const alias = arg.$resolvedType.decl; - const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type); - return dstType === mappedAliasResolvedType || dstType === 'Any' || mappedAliasResolvedType === 'Any'; + return isAliasAssignableToType(arg.$resolvedType.decl, dstType ?? 'Any', attr); } // destination is field reference or transitive field reference, check if @@ -422,6 +418,34 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField) return allowed; } +function isAliasAssignableToType(alias: AliasDecl, dstType: string, attr: AttributeApplication): boolean { + const effectiveDstType = resolveEffectiveDestinationType(dstType, attr); + if (effectiveDstType === null) { + return false; + } + + const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type); + return ( + effectiveDstType === mappedAliasResolvedType || effectiveDstType === 'Any' || mappedAliasResolvedType === 'Any' + ); +} + +function resolveEffectiveDestinationType(dstType: string, attr: AttributeApplication): string | null { + if (dstType !== 'ContextType') { + return dstType; + } + + // ContextType is inferred from the attribute's container's type + if (isDataModelField(attr.$container)) { + if (!attr.$container?.type?.type) { + return null; + } + return mapBuiltinTypeToExpressionType(attr.$container.type.type); + } + + return 'Any'; +} + export function validateAttributeApplication(attr: AttributeApplication, accept: ValidationAcceptor) { new AttributeApplicationValidator().validate(attr, accept); } 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 b29a4a07a..2c830cad3 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -110,12 +110,6 @@ export default class FunctionInvocationValidator implements AstValidator 0) { + // For constraint purposes, use the alias name itself as the field identifier + // The actual field resolution will be handled by the expression transformer + return { name: aliasDecl.name }; + } + } + } return undefined; } diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index cac87c9af..b6fa00afd 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -36,7 +36,12 @@ import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { streamAst } from 'langium'; import path from 'path'; import { FunctionDeclarationStructure, OptionalKind, Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; -import { isCheckInvocation } from '../../../utils/ast-utils'; +import { + getAliasDeclaration, + getFieldsFromAliasExpression, + isAliasInvocation, + isCheckInvocation, +} from '../../../utils/ast-utils'; import { ConstraintTransformer } from './constraint-transformer'; import { generateConstantQueryGuardFunction, @@ -224,6 +229,25 @@ export class PolicyGenerator { } } + // Handle alias invocations + if (isAliasInvocation(expr)) { + const aliasDecl = getAliasDeclaration(expr); + if (aliasDecl) { + const referencedFields = getFieldsFromAliasExpression(aliasDecl); + // Check if any referenced field would prevent create input checking + for (const field of referencedFields) { + if (field.$container === model && hasAttribute(field, '@default')) { + // Alias references field with default value + return false; + } + if (isForeignKeyField(field)) { + // Alias references foreign key field + return false; + } + } + } + } + return true; }); }); diff --git a/packages/schema/src/plugins/enhancer/policy/utils.ts b/packages/schema/src/plugins/enhancer/policy/utils.ts index 11a902822..b86757d73 100644 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ b/packages/schema/src/plugins/enhancer/policy/utils.ts @@ -44,6 +44,8 @@ import { getContainerOfType, streamAllContents, streamAst, streamContents } from import { FunctionDeclarationStructure, OptionalKind } from 'ts-morph'; import { name } from '..'; import { + getAliasDeclaration, + getFieldsFromAliasExpression, isAliasInvocation, isCheckInvocation, isCollectionPredicate, @@ -318,8 +320,8 @@ export function generateQueryGuardFunction( isFutureExpr(child) || // field reference (isReferenceExpr(child) && isDataModelField(child.target.ref)) || - // TODO: field access from alias expression - isAliasInvocation(child) + // field access from alias expression - resolve to actual fields + (isAliasInvocation(child) && isExpression(child) && hasFieldAccessInAlias(child)) ) ); @@ -598,3 +600,17 @@ function getSourceModelOfFieldAccess(expr: Expression) { return undefined; } + +/** + * Checks if an alias invocation resolves to actual field accesses + */ +function hasFieldAccessInAlias(aliasInvocation: Expression): boolean { + const aliasDecl = getAliasDeclaration(aliasInvocation); + if (!aliasDecl) { + return false; + } + + // Get all fields referenced in the alias expression + const fields = getFieldsFromAliasExpression(aliasDecl); + return fields.length > 0; +} diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 5aa02b4d5..0e079c029 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -9,8 +9,10 @@ import { isDataModel, isDataModelField, isInvocationExpr, + isMemberAccessExpr, isModel, isReferenceExpr, + isThisExpr, isAliasDecl, isTypeDef, Model, @@ -166,11 +168,63 @@ export function isCheckInvocation(node: AstNode) { } export function isAliasInvocation(node: AstNode) { - // check if a matching alias exists + if (!isInvocationExpr(node)) { + return false; + } + + // Check if the resolved reference is an alias declaration + if (node.function.ref && isAliasDecl(node.function.ref)) { + return true; + } + + // Fallback: check by name in the current model const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? []; - // const aliasDecls = getAllLoadedAlias(this.langiumDocuments()); - return isInvocationExpr(node) && allAlias.some((alias) => alias.name === node.function.$refText); - // (!node.function.ref || !isFromStdlib(node.function.ref)) /* && isAliasDecl(node.function.ref) */ + return allAlias.some((alias) => alias.name === node.function.$refText); +} + +/** + * Gets the alias declaration for a given alias invocation + */ +export function getAliasDeclaration(node: AstNode): AliasDecl | undefined { + if (!isInvocationExpr(node)) { + return undefined; + } + + const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? []; + return allAlias.find((alias) => alias.name === node.function.$refText); +} + +/** + * Extracts all DataModelField references from an alias expression + */ +export function getFieldsFromAliasExpression(alias: AliasDecl): DataModelField[] { + const fields: DataModelField[] = []; + + function extractFields(expr: Expression): void { + if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) { + fields.push(expr.target.ref); + } else if (isMemberAccessExpr(expr)) { + // Handle this.fieldName + if (isThisExpr(expr.operand) && expr.member.ref && isDataModelField(expr.member.ref)) { + fields.push(expr.member.ref); + } + } else if (isInvocationExpr(expr)) { + // Handle nested alias invocations + const nestedAlias = getAliasDeclaration(expr); + if (nestedAlias) { + fields.push(...getFieldsFromAliasExpression(nestedAlias)); + } + // Also check arguments for field references + expr.args.forEach((arg) => extractFields(arg.value)); + } else if (isBinaryExpr(expr)) { + extractFields(expr.left); + extractFields(expr.right); + } + // Add more expression types as needed + } + + extractFields(alias.expression); + return [...new Set(fields)]; // Remove duplicates } export function resolveImportUri(imp: ModelImport): URI | undefined { diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts index a4a2e520d..a37ee6ee6 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -89,7 +89,6 @@ export class TypeScriptExpressionTransformer { return this.this(expr as ThisExpr); case ReferenceExpr: - // TODO: ensure referenceExpr from alias is resolved return this.reference(expr as ReferenceExpr); case InvocationExpr: @@ -147,7 +146,10 @@ export class TypeScriptExpressionTransformer { if (isAlias) { // if the function invocation comes from an alias, we transform its expression - return this.transform(expr.function.ref.expression!, normalizeUndefined); + if (!expr.function.ref.expression) { + throw new TypeScriptExpressionTransformerError(`Unresolved alias expression`); + } + return this.transform(expr.function.ref.expression, normalizeUndefined); } if (!isStdFunc) { diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index bf3474730..207f55139 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -1,7 +1,5 @@ /// -import fs from 'fs'; -import path from 'path'; import { loadSchema } from '@zenstackhq/testtools'; describe('Policy plugin tests', () => { @@ -20,21 +18,21 @@ describe('Policy plugin tests', () => { it('short-circuit', async () => { const model = ` -model User { - id String @id @default(cuid()) - value Int -} - -model M { - id String @id @default(cuid()) - value Int - @@allow('read', auth() != null) - @@allow('create', auth().value > 0) - - @@allow('update', auth() != null) - @@deny('update', auth().value == null || auth().value <= 0) -} - `; + model User { + id String @id @default(cuid()) + value Int + } + + model M { + id String @id @default(cuid()) + value Int + @@allow('read', auth() != null) + @@allow('create', auth().value > 0) + + @@allow('update', auth() != null) + @@deny('update', auth().value == null || auth().value <= 0) + } + `; const { policy } = await loadSchema(model); @@ -56,17 +54,17 @@ model M { it('no short-circuit', async () => { const model = ` -model User { - id String @id @default(cuid()) - value Int -} - -model M { - id String @id @default(cuid()) - value Int - @@allow('read', auth() != null && value > 0) -} - `; + model User { + id String @id @default(cuid()) + value Int + } + + model M { + id String @id @default(cuid()) + value Int + @@allow('read', auth() != null && value > 0) + } + `; const { policy } = await loadSchema(model); @@ -80,26 +78,26 @@ model M { it('auth() multiple level member access', async () => { const model = ` - model User { - id Int @id @default(autoincrement()) - cart Cart? - } - - model Cart { - id Int @id @default(autoincrement()) - tasks Task[] - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model Task { - id Int @id @default(autoincrement()) - cart Cart @relation(fields: [cartId], references: [id]) - cartId Int - value Int - @@allow('read', auth().cart.tasks?[id == 123] && value >10) - } - `; + model User { + id Int @id @default(autoincrement()) + cart Cart? + } + + model Cart { + id Int @id @default(autoincrement()) + tasks Task[] + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } + + model Task { + id Int @id @default(autoincrement()) + cart Cart @relation(fields: [cartId], references: [id]) + cartId Int + value Int + @@allow('read', auth().cart.tasks?[id == 123] && value >10) + } + `; const { policy } = await loadSchema(model); expect( @@ -114,38 +112,38 @@ model M { it('simple alias expressions', async () => { const { policy } = await loadSchema( ` - alias allowAll() { - true - } - - alias defaultTitle() { - 'Default Title' - } - - alias currentUser() { - auth().id - } - - model Post { - id Int @id @default(autoincrement()) - title String @default(defaultTitle()) - published Boolean @default(allowAll()) - - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - @@allow('read', allowAll()) - @@allow('create,update,delete', currentUser() == authorId && published) - } - - model User { - id String @id @default(cuid()) - name String? - posts Post[] - - @@allow('all', allowAll()) - } - `, + alias allowAll() { + true + } + + alias defaultTitle() { + 'Default Title' + } + + alias currentUser() { + auth().id + } + + model Post { + id Int @id @default(autoincrement()) + title String @default(defaultTitle()) + published Boolean @default(allowAll()) + + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + @@allow('read', allowAll()) + @@allow('create,update,delete', currentUser() == authorId && published) + } + + model User { + id String @id @default(cuid()) + name String? + posts Post[] + + @@allow('all', allowAll()) + } + `, { compile: false, generateNoCompile: true, @@ -174,53 +172,52 @@ model M { it('complex alias expressions', async () => { const model = ` - enum TaskStatus { - TODO - IN_PROGRESS - DONE - } + enum TaskStatus { + TODO + IN_PROGRESS + DONE + } - alias isInProgress() { - status == IN_PROGRESS - } + alias isInProgress() { + status == IN_PROGRESS + } + alias complexAlias() { + status == IN_PROGRESS && value > 10 + } - alias complexAlias() { - status == IN_PROGRESS && value > 10 - } - - alias memberAccessAlias() { - cart.tasks?[id == 123] - } + alias memberAccessAlias() { + cart.tasks?[id == 123] + } - alias memberAccess() { - // new task can be created the cart contains tasks with status TODO... - cart.tasks?[status == TODO] - } + alias memberAccess() { + // new task can be created the cart contains tasks with status TODO... + cart.tasks?[status == TODO] + } - model User { - id Int @id @default(autoincrement()) - cart Cart? - } - - model Cart { - id Int @id @default(autoincrement()) - tasks Task[] - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model Task { - id Int @id @default(autoincrement()) - status TaskStatus @default(TODO) - cart Cart @relation(fields: [cartId], references: [id]) - cartId Int - value Int - @@allow('read', complexAlias()) - @@allow('update', memberAccessAlias()) - @@allow('create', memberAccess()) - } - `; + model User { + id Int @id @default(autoincrement()) + cart Cart? + } + + model Cart { + id Int @id @default(autoincrement()) + tasks Task[] + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } + + model Task { + id Int @id @default(autoincrement()) + status TaskStatus @default(TODO) + cart Cart @relation(fields: [cartId], references: [id]) + cartId Int + value Int + @@allow('read', complexAlias()) + @@allow('update', memberAccessAlias()) + @@allow('create', memberAccess()) + } + `; const { policy } = await loadSchema(model, { compile: false, @@ -273,30 +270,30 @@ model M { it('simple member access in alias', async () => { const model = ` - alias memberAccess() { - cart.tasks?[id == 123] - } + alias memberAccess() { + cart.tasks?[id == 123] + } - model User { - id Int @id @default(autoincrement()) - cart Cart? - } + model User { + id Int @id @default(autoincrement()) + cart Cart? + } - model Cart { - id Int @id @default(autoincrement()) - tasks Task[] - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } + model Cart { + id Int @id @default(autoincrement()) + tasks Task[] + user User @relation(fields: [userId], references: [id]) + userId Int @unique + } - model Task { - id Int @id @default(autoincrement()) - cart Cart @relation(fields: [cartId], references: [id]) - cartId Int - value Int - @@allow('create', memberAccess()) - } - `; + model Task { + id Int @id @default(autoincrement()) + cart Cart @relation(fields: [cartId], references: [id]) + cartId Int + value Int + @@allow('create', memberAccess()) + } + `; const { policy } = await loadSchema(model, { compile: false, @@ -328,4 +325,76 @@ model M { }) ).toEqual({ cart: { tasks: { some: { id: { equals: 123 } } } } }); }); + + it('alias field access resolution in policy rules', async () => { + const model = ` + alias isAdminUser() { + auth().role == 'admin' + } + + // TODO: enable parameters in alias + // alias isInSameDepartment(targetDepartment: String) { + // auth().department == targetDepartment + // } + + alias hasFieldAccess() { + auth().role != null && auth().department != null + } + + model User { + id String @id @default(cuid()) + role String + department String + } + + model Document { + id String @id @default(cuid()) + title String + department String + sensitive Boolean @default(false) + + // @@allow('read', isAdminUser() || isInSameDepartment(department)) + @@allow('create', hasFieldAccess() && !sensitive) + @@allow('update', isAdminUser()) + } + `; + + const { policy } = await loadSchema(model); + const docPolicy = policy.policy.document.modelLevel; + + // Test admin user access + const adminUser = { id: '1', role: 'admin', department: 'IT' }; + expect((docPolicy.read.guard as Function)({ user: adminUser })).toEqual({ + OR: [ + { AND: [] }, // isAdminUser() resolves to true + { department: { equals: 'IT' } }, // isInSameDepartment() check + ], + }); + + // // Test same department user access + // const deptUser = { id: '2', role: 'user', department: 'HR' }; + // expect((docPolicy.read.guard as Function)({ user: deptUser })).toEqual({ + // OR: [ + // { OR: [] }, // isAdminUser() resolves to false + // { department: { equals: 'HR' } }, // isInSameDepartment() check + // ], + // }); + + // Test create policy with field access check + expect((docPolicy.create.guard as Function)({ user: adminUser })).toEqual({ + AND: [ + { AND: [] }, // hasFieldAccess() resolves to true for admin + { NOT: { sensitive: { equals: true } } }, // !sensitive check + ], + }); + + // Test user without proper field access + const limitedUser = { id: '3', role: null, department: 'Sales' }; + expect((docPolicy.create.guard as Function)({ user: limitedUser })).toEqual({ + AND: [ + { OR: [] }, // hasFieldAccess() resolves to false + { NOT: { sensitive: { equals: true } } }, + ], + }); + }); }); From 50f65c27f190dcf5747247b4c6811f92ce30c085 Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 21:02:35 +0200 Subject: [PATCH 12/14] fix: validate binary expr including alias --- .../attribute-application-validator.ts | 4 +- .../validator/expression-validator.ts | 37 +++++++++++++------ .../src/language-server/validator/utils.ts | 5 +-- .../integration/tests/plugins/policy.test.ts | 28 +++++++++----- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 98359406e..9ba473e3d 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -33,7 +33,7 @@ import { AstValidator } from '../types'; import { getStringLiteral, mapBuiltinTypeToExpressionType, - mappedRawExpressionTypeToResolvedShape, + translateExpressionTypeToResolve, typeAssignable, } from './utils'; @@ -424,7 +424,7 @@ function isAliasAssignableToType(alias: AliasDecl, dstType: string, attr: Attrib return false; } - const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type); + const mappedAliasResolvedType = translateExpressionTypeToResolve(alias.expression.$type); return ( effectiveDstType === mappedAliasResolvedType || effectiveDstType === 'Any' || mappedAliasResolvedType === 'Any' ); diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 1275d7bd8..b98e2bf6f 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -25,7 +25,7 @@ import { import { ValidationAcceptor, getContainerOfType, streamAst } from 'langium'; import { findUpAst, getContainingDataModel } from '../../utils/ast-utils'; import { AstValidator } from '../types'; -import { isAuthOrAuthMemberAccess, typeAssignable } from './utils'; +import { isAuthOrAuthMemberAccess, translateExpressionTypeToResolve, typeAssignable } from './utils'; /** * Validates expressions. @@ -108,19 +108,14 @@ export default class ExpressionValidator implements AstValidator { supportedShapes = ['Boolean', 'Any']; } - if ( - typeof expr.left.$resolvedType?.decl !== 'string' || - !supportedShapes.includes(expr.left.$resolvedType.decl) - ) { + if (!this.isValidOperandType(expr.left, supportedShapes)) { accept('error', `invalid operand type for "${expr.operator}" operator`, { node: expr.left, }); return; } - if ( - typeof expr.right.$resolvedType?.decl !== 'string' || - !supportedShapes.includes(expr.right.$resolvedType.decl) - ) { + + if (!this.isValidOperandType(expr.right, supportedShapes)) { accept('error', `invalid operand type for "${expr.operator}" operator`, { node: expr.right, }); @@ -128,11 +123,11 @@ export default class ExpressionValidator implements AstValidator { } // DateTime comparison is only allowed between two DateTime values - if (expr.left.$resolvedType.decl === 'DateTime' && expr.right.$resolvedType.decl !== 'DateTime') { + if (expr.left.$resolvedType?.decl === 'DateTime' && expr.right.$resolvedType?.decl !== 'DateTime') { accept('error', 'incompatible operand types', { node: expr }); } else if ( - expr.right.$resolvedType.decl === 'DateTime' && - expr.left.$resolvedType.decl !== 'DateTime' + expr.right.$resolvedType?.decl === 'DateTime' && + expr.left.$resolvedType?.decl !== 'DateTime' ) { accept('error', 'incompatible operand types', { node: expr }); } @@ -298,4 +293,22 @@ export default class ExpressionValidator implements AstValidator { (isArrayExpr(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item))) ); } + + private isValidOperandType(operand: Expression, supportedShapes: string[]): boolean { + const decl = operand.$resolvedType?.decl; + + // Check for valid type + if (typeof decl === 'string') { + return supportedShapes.includes(decl); + } + + // Check for valid AliasDecl + if (isAliasDecl(decl)) { + const mappedResolvedType = translateExpressionTypeToResolve(decl.expression?.$type); + return supportedShapes.includes(mappedResolvedType); + } + + // Any other type is invalid + return false; + } } diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 2ac4e2250..c5f0ce71f 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -5,7 +5,6 @@ import { isDataModelField, isMemberAccessExpr, isStringLiteral, - ResolvedShape, } from '@zenstackhq/language/ast'; import { isAuthInvocation } from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; @@ -101,9 +100,9 @@ export function mapBuiltinTypeToExpressionType( } /** - * Maps an expression type (e.g. StringLiteral) to a resolved shape (e.g. String) + * Map an expression $type (e.g. StringLiteral) to a resolved ExpressionType (e.g. String) */ -export function mappedRawExpressionTypeToResolvedShape(expressionType: Expression['$type']): ResolvedShape { +export function translateExpressionTypeToResolve(expressionType: Expression['$type']): ExpressionType { switch (expressionType) { case 'StringLiteral': return 'String'; diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts index 207f55139..e22fac275 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -364,12 +364,12 @@ describe('Policy plugin tests', () => { // Test admin user access const adminUser = { id: '1', role: 'admin', department: 'IT' }; - expect((docPolicy.read.guard as Function)({ user: adminUser })).toEqual({ - OR: [ - { AND: [] }, // isAdminUser() resolves to true - { department: { equals: 'IT' } }, // isInSameDepartment() check - ], - }); + // expect((docPolicy.read.guard as Function)({ user: adminUser })).toEqual({ + // OR: [ + // { AND: [] }, // isAdminUser() resolves to true + // { department: { equals: 'IT' } }, // isInSameDepartment() check + // ], + // }); // // Test same department user access // const deptUser = { id: '2', role: 'user', department: 'HR' }; @@ -383,8 +383,12 @@ describe('Policy plugin tests', () => { // Test create policy with field access check expect((docPolicy.create.guard as Function)({ user: adminUser })).toEqual({ AND: [ - { AND: [] }, // hasFieldAccess() resolves to true for admin - { NOT: { sensitive: { equals: true } } }, // !sensitive check + { + AND: [{ AND: [] }, { AND: [] }], + }, + { + NOT: { sensitive: true }, + }, ], }); @@ -392,8 +396,12 @@ describe('Policy plugin tests', () => { const limitedUser = { id: '3', role: null, department: 'Sales' }; expect((docPolicy.create.guard as Function)({ user: limitedUser })).toEqual({ AND: [ - { OR: [] }, // hasFieldAccess() resolves to false - { NOT: { sensitive: { equals: true } } }, + { + AND: [{ OR: [] }, { AND: [] }], + }, + { + NOT: { sensitive: true }, + }, ], }); }); From db721c679563ffd17b57e5a4e43cfa038059f943 Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 21:25:08 +0200 Subject: [PATCH 13/14] update --- .../validator/expression-validator.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index b98e2bf6f..454c16384 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -25,7 +25,7 @@ import { import { ValidationAcceptor, getContainerOfType, streamAst } from 'langium'; import { findUpAst, getContainingDataModel } from '../../utils/ast-utils'; import { AstValidator } from '../types'; -import { isAuthOrAuthMemberAccess, translateExpressionTypeToResolve, typeAssignable } from './utils'; +import { isAuthOrAuthMemberAccess, typeAssignable } from './utils'; /** * Validates expressions. @@ -295,19 +295,17 @@ export default class ExpressionValidator implements AstValidator { } private isValidOperandType(operand: Expression, supportedShapes: string[]): boolean { - const decl = operand.$resolvedType?.decl; + let decl = operand.$resolvedType?.decl; + if (isAliasDecl(decl)) { + // If it's an alias, we check the resolved type of the expression + decl = decl.expression?.$resolvedType?.decl; + } // Check for valid type if (typeof decl === 'string') { return supportedShapes.includes(decl); } - // Check for valid AliasDecl - if (isAliasDecl(decl)) { - const mappedResolvedType = translateExpressionTypeToResolve(decl.expression?.$type); - return supportedShapes.includes(mappedResolvedType); - } - // Any other type is invalid return false; } From 612da0fcbb818bf9db51095f6eadb71bfa72ad84 Mon Sep 17 00:00:00 2001 From: augustin Date: Sat, 26 Jul 2025 21:33:53 +0200 Subject: [PATCH 14/14] remove approximate mapping --- .../attribute-application-validator.ts | 13 +++--------- .../src/language-server/validator/utils.ts | 20 ------------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 9ba473e3d..472b445bc 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -30,12 +30,7 @@ import { import { ValidationAcceptor, streamAllContents, streamAst } from 'langium'; import pluralize from 'pluralize'; import { AstValidator } from '../types'; -import { - getStringLiteral, - mapBuiltinTypeToExpressionType, - translateExpressionTypeToResolve, - typeAssignable, -} from './utils'; +import { getStringLiteral, mapBuiltinTypeToExpressionType, typeAssignable } from './utils'; // a registry of function handlers marked with @check const attributeCheckers = new Map(); @@ -424,10 +419,8 @@ function isAliasAssignableToType(alias: AliasDecl, dstType: string, attr: Attrib return false; } - const mappedAliasResolvedType = translateExpressionTypeToResolve(alias.expression.$type); - return ( - effectiveDstType === mappedAliasResolvedType || effectiveDstType === 'Any' || mappedAliasResolvedType === 'Any' - ); + const aliasExpressionType = alias.expression.$resolvedType?.decl; + return effectiveDstType === aliasExpressionType || effectiveDstType === 'Any' || aliasExpressionType === 'Any'; } function resolveEffectiveDestinationType(dstType: string, attr: AttributeApplication): string | null { diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index c5f0ce71f..ca13b03df 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -99,26 +99,6 @@ export function mapBuiltinTypeToExpressionType( } } -/** - * Map an expression $type (e.g. StringLiteral) to a resolved ExpressionType (e.g. String) - */ -export function translateExpressionTypeToResolve(expressionType: Expression['$type']): ExpressionType { - switch (expressionType) { - case 'StringLiteral': - return 'String'; - case 'NumberLiteral': - return 'Int'; - case 'BooleanLiteral': - return 'Boolean'; - case 'ObjectExpr': - return 'Object'; - case 'NullExpr': - return 'Null'; - default: - return 'Any'; - } -} - export function isAuthOrAuthMemberAccess(expr: Expression): boolean { return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand)); }