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..075533ab0 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": [] } @@ -137,6 +137,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel }, "arguments": [] }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@47" + }, + "arguments": [] + }, { "$type": "RuleCall", "rule": { @@ -147,7 +154,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -169,7 +176,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -185,7 +192,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -229,7 +236,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -245,7 +252,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -289,7 +296,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -301,7 +308,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -340,7 +347,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -356,7 +363,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -400,7 +407,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [], "cardinality": "*" @@ -412,7 +419,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -488,7 +495,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@69" }, "arguments": [] } @@ -510,7 +517,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@68" }, "arguments": [] } @@ -532,7 +539,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@61" + "$ref": "#/rules@62" }, "arguments": [] } @@ -656,7 +663,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] } @@ -754,7 +761,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] } @@ -963,7 +970,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] }, @@ -1062,7 +1069,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] } @@ -1176,14 +1183,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": [] } @@ -1228,7 +1235,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@46" + "$ref": "#/types@1" }, "deprecatedSyntax": false } @@ -1301,7 +1308,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/types@1" + "$ref": "#/types@2" }, "deprecatedSyntax": false } @@ -1901,7 +1908,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -1934,7 +1941,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2004,7 +2011,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2039,7 +2046,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2073,7 +2080,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2086,7 +2093,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2110,7 +2117,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] }, @@ -2141,7 +2148,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@61" }, "arguments": [] } @@ -2165,12 +2172,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 +2237,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2247,7 +2254,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2278,7 +2285,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2312,7 +2319,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2325,7 +2332,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2349,7 +2356,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] }, @@ -2380,7 +2387,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@61" }, "arguments": [] } @@ -2392,12 +2399,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": [] }, @@ -2496,7 +2503,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] }, @@ -2513,7 +2520,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2544,7 +2551,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2578,11 +2585,63 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@51" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@55" }, "arguments": [] }, "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "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", @@ -2596,6 +2655,74 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "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", @@ -2603,7 +2730,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@57" }, "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": [] } @@ -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": [] } @@ -3837,7 +3968,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" } }, { @@ -3861,6 +3992,27 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel ] } }, + { + "$type": "Type", + "name": "AbstractCallable", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@47" + } + }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@46" + } + } + ] + } + }, { "$type": "Type", "name": "MemberAccessTarget", @@ -3926,6 +4078,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "typeRef": { "$ref": "#/rules@44" } + }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@46" + } } ] } diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 4b311bcba..5f90fc96e 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -11,7 +11,7 @@ ModelImport: 'import' path=STRING ';'?; AbstractDeclaration: - DataSource | GeneratorDecl| Plugin | DataModel | TypeDef | Enum | FunctionDecl | Attribute; + DataSource | GeneratorDecl | Plugin | DataModel | TypeDef | Enum | FunctionDecl | AliasDecl | Attribute; // datasource DataSource: @@ -91,8 +91,10 @@ ObjectExpr: FieldInitializer: name=(RegularID | STRING) ':' value=(Expression); +type AbstractCallable = FunctionDecl | AliasDecl; + InvocationExpr: - function=[FunctionDecl] '(' ArgumentList? ')'; + function=[AbstractCallable] '(' ArgumentList? ')'; type MemberAccessTarget = DataModelField | TypeDefField; @@ -215,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)*; @@ -228,7 +235,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 +252,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/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 0efa760b8..472b445bc 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, @@ -8,6 +9,7 @@ import { DataModelFieldAttribute, InternalAttribute, ReferenceExpr, + isAliasDecl, isArrayExpr, isAttribute, isDataModel, @@ -294,6 +296,11 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at } } + // Handle alias expressions by comparing to their resolved shape + if (isAliasDecl(arg.$resolvedType?.decl)) { + return isAliasAssignableToType(arg.$resolvedType.decl, dstType ?? 'Any', attr); + } + // destination is field reference or transitive field reference, check if // argument is reference or array or reference if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { @@ -406,6 +413,32 @@ 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 aliasExpressionType = alias.expression.$resolvedType?.decl; + return effectiveDstType === aliasExpressionType || effectiveDstType === 'Any' || aliasExpressionType === '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/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index 06083e9e4..454c16384 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', @@ -50,9 +51,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, }); } @@ -107,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, }); @@ -127,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 }); } @@ -297,4 +293,20 @@ export default class ExpressionValidator implements AstValidator { (isArrayExpr(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item))) ); } + + private isValidOperandType(operand: Expression, supportedShapes: string[]): boolean { + 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); + } + + // Any other type is invalid + return false; + } } 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..2c830cad3 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(', ')}`, { @@ -130,7 +132,7 @@ export default class FunctionInvocationValidator implements AstValidator + (item: Expression) => isLiteralExpr(item) || isEnumFieldReference(item) || isAuthOrAuthMemberAccess(item) ) ) @@ -144,19 +146,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..ca13b03df 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -86,6 +86,8 @@ export function mapBuiltinTypeToExpressionType( case 'Int': case 'Float': case 'Null': + case 'Object': + case 'Unsupported': return type; case 'BigInt': return 'Int'; @@ -94,10 +96,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 1e2491bda..bec35a6f3 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, @@ -26,12 +27,14 @@ import { ThisExpr, TypeDefFieldType, UnaryExpr, + isAliasDecl, isArrayExpr, isBooleanLiteral, isDataModel, isDataModelField, isDataModelFieldType, isEnum, + isModel, isNumberLiteral, isReferenceExpr, isStringLiteral, @@ -55,7 +58,11 @@ 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'; @@ -189,6 +196,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; @@ -278,8 +289,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 +306,10 @@ 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)) { + this.resolveAliasInvocation(node, document, extraScopes); } else { - this.resolveToDeclaredType(node, funcDecl.returnType); + this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType); } } } @@ -484,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); @@ -512,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 diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index 11cbb4909..8ad0a27f8 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); @@ -219,7 +231,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)) { @@ -243,6 +255,127 @@ 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; + + // Handle auth() invocation in alias + if (isInvocationExpr(node.operand)) { + if (isAuthInvocation(node.operand)) { + return this.createScopeForAuth(node, globalScope); + } + if (isFutureInvocation(node.operand)) { + return this.createScopeForContainingModel(node, globalScope); + } + return EMPTY_SCOPE; + } + + // Handle this expression in alias + if (isThisExpr(node.operand)) { + return this.createScopeForContainingModel(node, globalScope); + } + + // 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); + } + } + + // Handle nested member access (e.g., some.nested.field) + if (isMemberAccessExpr(node.operand)) { + const ref = node.operand.member.ref; + if (isDataModelField(ref) && !ref.type.array) { + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, true); + } + if (isTypeDefField(ref) && !ref.type.array) { + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, true); + } + } + + 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/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/constraint-transformer.ts b/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts index 674348470..ee693723b 100644 --- a/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts +++ b/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts @@ -32,7 +32,12 @@ import { } from '@zenstackhq/sdk/ast'; import { P, match } from 'ts-pattern'; import { name } from '..'; -import { isCheckInvocation } from '../../../utils/ast-utils'; +import { + getAliasDeclaration, + getFieldsFromAliasExpression, + isAliasInvocation, + isCheckInvocation, +} from '../../../utils/ast-utils'; /** * Options for {@link ConstraintTransformer}. @@ -381,6 +386,19 @@ export class ConstraintTransformer { if (isMemberAccessExpr(expr)) { return isThisExpr(expr.operand) ? { name: expr.member.$refText } : undefined; } + if (isAliasInvocation(expr)) { + // Resolve alias to actual fields - for constraint transformation, + // we need to return a single field name representation + const aliasDecl = getAliasDeclaration(expr); + if (aliasDecl) { + const fields = getFieldsFromAliasExpression(aliasDecl); + if (fields.length > 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/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts index d3dccfaa6..0b4172da7 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)) { + 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 f257f0830..b6fa00afd 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -13,6 +13,7 @@ import { isThisExpr, isTypeDef, } from '@zenstackhq/language/ast'; + import { PolicyCrudKind, type PolicyOperationKind } from '@zenstackhq/runtime'; import { type CodeWriter, @@ -35,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, @@ -223,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 ae9a7846f..b86757d73 100644 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ b/packages/schema/src/plugins/enhancer/policy/utils.ts @@ -14,6 +14,7 @@ import { getIdFields, getLiteral, getQueryGuardFunctionName, + hasAuthInvocation, isAuthInvocation, isDataModelFieldReference, isEnumFieldReference, @@ -42,7 +43,14 @@ 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 { + getAliasDeclaration, + getFieldsFromAliasExpression, + isAliasInvocation, + isCheckInvocation, + isCollectionPredicate, + isFutureInvocation, +} from '../../../utils/ast-utils'; import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; /** @@ -311,7 +319,9 @@ export function generateQueryGuardFunction( // future().??? isFutureExpr(child) || // field reference - (isReferenceExpr(child) && isDataModelField(child.target.ref)) + (isReferenceExpr(child) && isDataModelField(child.target.ref)) || + // field access from alias expression - resolve to actual fields + (isAliasInvocation(child) && isExpression(child) && hasFieldAccessInAlias(child)) ) ); @@ -506,8 +516,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) { @@ -545,6 +554,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); @@ -590,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/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..0e079c029 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -9,11 +9,15 @@ import { isDataModel, isDataModelField, isInvocationExpr, + isMemberAccessExpr, isModel, isReferenceExpr, + isThisExpr, + isAliasDecl, isTypeDef, Model, ModelImport, + AliasDecl, TypeDef, } from '@zenstackhq/language/ast'; import { @@ -40,12 +44,6 @@ import path from 'node:path'; import { URI, Utils } from 'vscode-uri'; import { findNodeModulesFile } from './pkg-utils'; -export function extractDataModelsWithAllowRules(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, @@ -169,6 +167,66 @@ export function isCheckInvocation(node: AstNode) { return isInvocationExpr(node) && node.function.ref?.name === 'check' && isFromStdlib(node.function.ref); } +export function isAliasInvocation(node: AstNode) { + 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) ?? []; + 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 { if (!imp.path) return undefined; // This will return true if imp.path is undefined, null, or an empty string (""). @@ -312,6 +370,16 @@ export function getAllLoadedAndReachableDataModelsAndTypeDefs( return allDataModels; } +/** + * Gets all alias declarations 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..f29b682d6 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(` @@ -1391,4 +1391,79 @@ describe('Attribute tests', () => { `) ).resolves.toContain('Invalid regular expression'); }); + + it('alias expressions', async () => { + await loadModel(` + ${prelude} + + alias foo() { + opened + } + + model A { + id String @id + opened Boolean @default(true) + + @@allow('all', foo()) + } + `); + + await loadModel(` + ${prelude} + + alias allowAll() { + true + } + + alias defaultTitle() { + 'Default Title' + } + + alias currentUser() { + 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 @default(cuid()) + name String? + posts Post[] + + @@allow('all', allowAll()) + } + `); + + 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..a37ee6ee6 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -24,7 +24,7 @@ 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, isAliasExpr, isDataModelFieldReference, isFromStdlib, isFutureExpr } from './utils'; export class TypeScriptExpressionTransformerError extends Error { constructor(message: string) { @@ -142,6 +142,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 invocation comes from an alias, we transform its expression + if (!expr.function.ref.expression) { + throw new TypeScriptExpressionTransformerError(`Unresolved alias expression`); + } + return this.transform(expr.function.ref.expression, normalizeUndefined); + } if (!isStdFunc) { throw new TypeScriptExpressionTransformerError('User-defined functions are not supported yet'); @@ -387,7 +396,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)) { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 09a538f77..67dc6d715 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, + isAliasDecl, isArrayExpr, isConfigArrayExpr, isDataModel, @@ -39,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. @@ -426,7 +429,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 +449,24 @@ 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 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) { 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 4d35a0cc7..21b7c129e 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..e22fac275 100644 --- a/tests/integration/tests/plugins/policy.test.ts +++ b/tests/integration/tests/plugins/policy.test.ts @@ -18,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); @@ -54,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); @@ -78,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( @@ -108,4 +108,301 @@ model M { (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 123 }] } } }) ).toEqual(expect.objectContaining({ AND: [{ AND: [] }, { value: { gt: 10 } }] })); }); + + 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()) + } + `, + { + 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 }] }); + }); + + it('complex alias expressions', async () => { + const model = ` + enum TaskStatus { + TODO + IN_PROGRESS + DONE + } + + alias isInProgress() { + status == IN_PROGRESS + } + + alias complexAlias() { + 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 { + 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, + generateNoCompile: true, + 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)({ + status: 'IN_PROGRESS', + value: 15, + }) + ).toEqual( + expect.objectContaining({ + 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 } } } } }); + }); + + 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: [{ AND: [] }, { AND: [] }], + }, + { + NOT: { sensitive: true }, + }, + ], + }); + + // Test user without proper field access + const limitedUser = { id: '3', role: null, department: 'Sales' }; + expect((docPolicy.create.guard as Function)({ user: limitedUser })).toEqual({ + AND: [ + { + AND: [{ OR: [] }, { AND: [] }], + }, + { + NOT: { sensitive: true }, + }, + ], + }); + }); });