Skip to content

feat: enable alias declarations #2196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open

Conversation

Azzerty23
Copy link
Contributor

This is a first attempt at implementing alias declarations. The goal is to enable code reuse for both policy rules and default functions.

Example of intended usage:

alias allowAll() {
    true
}

alias defaultTitle() {
    'Default Title'
}

alias currentUser() {
    auth().id 
}

alias ownPublishedPosts() {
    currentUser() == authorId && published
}

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)
    @@deny('all', !ownPublishedPosts())
}

model User {
    id          String    @id @default(cuid())
    name        String?
    posts       Post[]

    @@allow('all', allowAll())
}

However, during implementation, I realized that having a shared generic alias mechanism for both policy rules and default values might not be the best idea: policy rule expressions and default value expressions are not interchangeable, and it becomes tricky to handle validation rules properly.

I initially started working on rule (#1959), but got sidetracked along the way... 😅

Also, I might have done something completely off with the SDL (Langium) changes — I have zero experience in that area.

Currently, I'm generating functions from aliases inside the policy file. But I’m thinking of reverting to my original approach of inlining the expression instead, as it would make handling relations easier. For example, this:

// generated policy.ts
function allowAll(): any {
  return true;
}

function Post_read(context: QueryContext, db: CrudContract): any {
  if (allowAll()) {
    return { AND: [] };
  }
  return { OR: [] };
}

Would become something like:

// generated policy.ts
function Post_read(context: QueryContext, db: CrudContract): any {
  if (true) {
    return { AND: [] };
  }
  return { OR: [] };
}

I’m also facing scoping issues where the expression doesn't allow referencing model fields:

alias ownPublishedPosts() {
    currentUser() == authorId && published
}
// Could not resolve reference to ReferenceTarget named 'authorId'.
// Could not resolve reference to ReferenceTarget named 'published'.
// invalid operand type for "&&" operator

@ymc9, thanks a lot in advance for your help!
If this approach doesn’t make sense at all, please don’t hesitate to say so ;)

Copy link
Contributor

coderabbitai bot commented Jul 20, 2025

📝 Walkthrough

Walkthrough

Alias declarations have been introduced and integrated throughout the language grammar, type system, semantic analysis, validation, code generation, and plugin infrastructure. The changes enable defining, referencing, and invoking aliases similarly to functions, with updates to parsing, scope/linking, validation, semantic highlighting, and test suites. Multiple utilities and validators were extended or generalized to support alias handling, and new test cases were added to verify correct behavior.

Changes

Files / Areas Change Summary
packages/language/src/zmodel.langium
packages/language/syntaxes/zmodel.tmLanguage
Grammar and syntax highlighting updated to support the new AliasDecl construct and recognize the alias keyword.
packages/schema/src/language-server/validator/attribute-application-validator.ts
packages/schema/src/language-server/validator/expression-validator.ts
packages/schema/src/language-server/validator/function-invocation-validator.ts
packages/schema/src/language-server/validator/utils.ts
Attribute, expression, and function invocation validators updated to handle alias declarations and invocations, with new utility for mapping expression types and improved error/context handling.
packages/schema/src/language-server/zmodel-linker.ts
packages/schema/src/language-server/zmodel-scope.ts
Linking and scope resolution logic extended for alias declarations, including new methods for resolving alias invocations and constructing alias-aware scopes.
packages/schema/src/language-server/zmodel-semantic.ts Semantic token provider updated to highlight alias declarations as functions.
packages/schema/src/plugins/enhancer/policy/expression-writer.ts
packages/schema/src/plugins/enhancer/policy/utils.ts
Policy enhancer and utility functions updated to handle alias invocations and broaden detection of authorization expressions.
packages/schema/src/plugins/prisma/schema-generator.ts Prisma schema generator updated to resolve and inline alias expressions in attribute argument values.
packages/schema/src/res/stdlib.zmodel
packages/sdk/src/constants.ts
Expression context enum extended with AliasFunction and standard library updated to reflect new context.
packages/schema/src/utils/ast-utils.ts
packages/sdk/src/utils.ts
AST and SDK utilities extended with functions for alias detection, invocation, and traversal; some obsolete functions removed.
packages/sdk/src/typescript-expression-transformer.ts TypeScript transformer updated to inline alias expressions during transformation and improve diagnostics.
packages/schema/tests/schema/validation/attribute-validation.test.ts
tests/integration/tests/plugins/policy.test.ts
New and updated tests for alias expressions, usage in policies, default values, error cases, and member access, verifying end-to-end integration.
packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts Minor stylistic change (blank line added after imports).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Parser
    participant Linker
    participant ScopeProvider
    participant Validator
    participant CodeGen
    participant TestSuite

    User->>Parser: Define AliasDecl in model
    Parser->>Linker: Parse and create AST with AliasDecl
    Linker->>ScopeProvider: Resolve references (including AliasDecl)
    ScopeProvider->>Linker: Provide scope for AliasDecl/member access
    Linker->>Validator: Resolve invocations (AliasDecl or FunctionDecl)
    Validator->>Validator: Validate type, context, and arguments
    Validator->>CodeGen: Pass resolved AliasDecl/expressions
    CodeGen->>User: Generate code (inlines alias expressions)
    TestSuite->>Parser: Run tests with models using aliases
    TestSuite->>Validator: Validate correct handling and errors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Complexity: High. The changes span grammar, parsing, AST, scope/linking, validation, code generation, and extensive tests, with multiple new utility functions and control flow branches.
  • Volume: Dozens of files, significant logic, and many new test cases.
  • Review time: ~45 minutes for a thorough review, focusing on integration points, correctness of alias handling, and test coverage.

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db721c6 and 612da0f.

📒 Files selected for processing (2)
  • packages/schema/src/language-server/validator/attribute-application-validator.ts (4 hunks)
  • packages/schema/src/language-server/validator/utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/schema/src/language-server/validator/utils.ts
  • packages/schema/src/language-server/validator/attribute-application-validator.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: OSSAR-Scan
  • GitHub Check: build-test (20.x)
  • GitHub Check: dependency-review
  • GitHub Check: build-test (20.x)
  • GitHub Check: build-test (20.x)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Azzerty23
Copy link
Contributor Author

It’s conceptually more of a fragment than an alias...
Regarding my reference issue, I understand that it's not possible to include an expression inside the body of the alias —references can't be resolved statically outside the scope of a model.
And if we use a string instead of an Expression in the body of the declaration, the references won’t be resolved in the abstract syntax tree. I’m wondering if this "fragment" idea can actually be implemented.

@Azzerty23
Copy link
Contributor Author

I partially resolve my issue by linking references inside alias to the first matching model field (this could be improved). Type resolution appears to be working well, and both Prisma model generation and policy rule generation are functioning as expected (tested on simple use cases). I will continue later this week.

Before going further, I’d like to get the ZenStack team’s approval to make sure I’m not heading in the wrong direction. A few specific points I’d like to clarify:

  • Which keyword should I keep (rule, fragment, etc.)?
  • Should we keep this generic fragment system, or specialize it for policy rules (the original FR)? I feel it might make more sense to have a different keyword (if needed) to define stored values, e.g., const myValue = ....
  • Should we keep the parentheses in declarations like rule myRule() { ... }? I’m not sure I fully understand the benefit of parameters, considering references are supposed to be resolved in the scope of the model where the rule is invoked.

@Azzerty23 Azzerty23 marked this pull request as ready for review July 26, 2025 15:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (6)
packages/sdk/src/utils.ts (1)

460-468: Consider improving string-based auth detection in aliases.

The hasAuthInvocation function uses string matching on CST node text (text.includes('auth()')) to detect auth invocations within alias expressions. This approach could yield false positives (e.g., comments, string literals containing "auth()") and is brittle to formatting changes.

Consider using AST traversal of the alias expression instead:

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;
+        if (isAuthInvocation(node)) {
+            return true;
+        }
+        // Check if this is an alias declaration with auth() in its expression
+        if (isAliasDecl(node.$resolvedType?.decl) && node.$resolvedType?.decl.expression) {
+            return streamAst(node.$resolvedType.decl.expression).some(isAuthInvocation);
+        }
+        return false;
    });
}
packages/schema/src/plugins/enhancer/policy/utils.ts (1)

555-555: TODO indicates incomplete alias support in cross-model comparison.

The TODO comment highlights that cross-model comparisons involving alias expressions are not yet supported. This is a significant limitation that should be addressed or at least documented clearly.

Would you like me to help implement the cross-model comparison support for alias expressions or create an issue to track this limitation?

packages/schema/src/language-server/zmodel-linker.ts (2)

500-519: Consider refactoring duplicate enum scope logic

The enum scope creation logic is duplicated. Both blocks do essentially the same thing but check different conditions.

-        // 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);

         let scopes = extraScopes;

         // if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes
-        if (node.type.reference?.ref && isEnum(node.type.reference.ref)) {
+        if (node.type.reference?.ref && isEnum(node.type.reference.ref) && 
+            (getContainerOfType(node, isAliasDecl) || true)) {
             const contextEnum = node.type.reference.ref as Enum;
             const enumScope: ScopeProvider = (name) => contextEnum.fields.find((f) => f.name === name);
             scopes = [enumScope, ...scopes];
         }

534-558: Well-implemented alias resolution logic

The alias resolution correctly handles the context switching needed to resolve alias expressions in the containing model's scope. This addresses the scoping issues mentioned in the PR objectives.

Consider adding a comment to clarify why we check for containingAlias first:

     private resolveAliasInvocation(node: InvocationExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) {
         // function is resolved to matching alias declaration
+        // Check if we're already inside an alias (for nested alias calls)
         const containingAlias = getContainerOfType(node, isAliasDecl);
         const matchingAlias = containingAlias || this.findMatchingAlias(node);
packages/schema/src/language-server/zmodel-scope.ts (2)

280-337: Consider refactoring for improved maintainability

The getAliasMemberAccessScope method correctly handles member access resolution within aliases, but its complexity could be reduced.

Consider extracting helper methods:

  1. Extract the invocation context finding logic (lines 293-296) into a separate method
  2. Extract the field lookup logic that's duplicated on lines 302-304 and 321-323

Example refactoring:

+    private findInvocationContext(node: AstNode): AstNode | undefined {
+        let context: AstNode | undefined = node.$container;
+        while (context && !isInvocationExpr(context)) {
+            context = context.$container;
+        }
+        return context;
+    }
+
+    private findFieldByName(model: DataModel, fieldName: string): DataModelField | undefined {
+        return getModelFieldsWithBases(model).find(f => f.name === fieldName);
+    }

     private getAliasMemberAccessScope(context: ReferenceInfo): Scope {
         // ... existing code ...
         
-            // Check if this is used in an invocation context
-            let invocationContext: AstNode | undefined = node.$container;
-            while (invocationContext && !isInvocationExpr(invocationContext)) {
-                invocationContext = invocationContext.$container;
-            }
+            const invocationContext = this.findInvocationContext(node);
             
             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
-                    );
+                    const field = this.findFieldByName(containingModel, operandName);
                     if (field && field.type.reference?.ref) {
                         return this.createScopeForContainer(field.type.reference.ref, globalScope);
                     }

339-351: Efficient scope combination implementation

The combineScopes method correctly merges multiple scopes while avoiding duplicates.

For better performance with larger scopes, consider using a Set or Map for duplicate detection:

     private combineScopes(scopes: Scope[]): Scope {
-        const allElements: AstNodeDescription[] = [];
+        const elementMap = new Map<string, 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);
-                }
+                const key = `${element.name}:${element.type}`;
+                if (!elementMap.has(key)) {
+                    elementMap.set(key, element);
+                }
             }
         }
-        return new StreamScope(stream(allElements));
+        return new StreamScope(stream(Array.from(elementMap.values())));
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1d3026 and 7d4dbe8.

⛔ Files ignored due to path filters (4)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**, !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**, !**/generated/**
  • packages/language/syntaxes/zmodel.tmLanguage.json is excluded by !**/*.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/*.yaml
📒 Files selected for processing (20)
  • packages/language/src/zmodel.langium (5 hunks)
  • packages/language/syntaxes/zmodel.tmLanguage (1 hunks)
  • packages/schema/src/language-server/validator/attribute-application-validator.ts (3 hunks)
  • packages/schema/src/language-server/validator/expression-validator.ts (4 hunks)
  • packages/schema/src/language-server/validator/function-invocation-validator.ts (9 hunks)
  • packages/schema/src/language-server/validator/utils.ts (3 hunks)
  • packages/schema/src/language-server/zmodel-linker.ts (8 hunks)
  • packages/schema/src/language-server/zmodel-scope.ts (5 hunks)
  • packages/schema/src/language-server/zmodel-semantic.ts (2 hunks)
  • packages/schema/src/plugins/enhancer/policy/expression-writer.ts (2 hunks)
  • packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts (1 hunks)
  • packages/schema/src/plugins/enhancer/policy/utils.ts (5 hunks)
  • packages/schema/src/plugins/prisma/schema-generator.ts (2 hunks)
  • packages/schema/src/res/stdlib.zmodel (2 hunks)
  • packages/schema/src/utils/ast-utils.ts (3 hunks)
  • packages/schema/tests/schema/validation/attribute-validation.test.ts (2 hunks)
  • packages/sdk/src/constants.ts (1 hunks)
  • packages/sdk/src/typescript-expression-transformer.ts (4 hunks)
  • packages/sdk/src/utils.ts (5 hunks)
  • tests/integration/tests/plugins/policy.test.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (11)
packages/schema/src/language-server/validator/expression-validator.ts (1)
packages/schema/src/utils/ast-utils.ts (1)
  • isAuthInvocation (156-158)
packages/schema/src/language-server/validator/attribute-application-validator.ts (1)
packages/schema/src/language-server/validator/utils.ts (1)
  • mappedRawExpressionTypeToResolvedShape (106-121)
packages/sdk/src/typescript-expression-transformer.ts (1)
packages/sdk/src/utils.ts (1)
  • isAliasExpr (452-454)
packages/schema/src/plugins/enhancer/policy/utils.ts (2)
packages/schema/src/utils/ast-utils.ts (1)
  • isAliasInvocation (168-174)
packages/sdk/src/utils.ts (1)
  • hasAuthInvocation (460-468)
packages/schema/src/plugins/prisma/schema-generator.ts (2)
packages/sdk/src/utils.ts (1)
  • isAliasExpr (452-454)
packages/sdk/src/types.ts (1)
  • PluginError (115-119)
packages/sdk/src/utils.ts (1)
packages/schema/src/utils/ast-utils.ts (1)
  • isAuthInvocation (156-158)
packages/schema/tests/schema/validation/attribute-validation.test.ts (1)
packages/schema/tests/utils.ts (2)
  • loadModel (32-75)
  • loadModelWithError (77-87)
packages/schema/src/language-server/validator/utils.ts (1)
packages/language/src/ast.ts (1)
  • ResolvedShape (10-10)
packages/schema/src/language-server/validator/function-invocation-validator.ts (1)
packages/sdk/src/utils.ts (2)
  • isFromStdlib (470-473)
  • getFunctionExpressionContext (432-446)
packages/schema/src/utils/ast-utils.ts (2)
packages/language/src/ast.ts (2)
  • AstNode (4-4)
  • AstNode (78-83)
packages/schema/src/plugins/prisma/prisma-builder.ts (1)
  • Model (103-149)
tests/integration/tests/plugins/policy.test.ts (1)
packages/testtools/src/schema.ts (1)
  • loadSchema (173-249)
🔇 Additional comments (46)
packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts (1)

16-16: LGTM: Clean formatting improvement

Adding a blank line after the import block improves code readability by clearly separating imports from the main code.

packages/sdk/src/constants.ts (1)

16-16: LGTM: Essential addition for alias support

The AliasFunction expression context is properly added to support the new alias declarations feature. The naming follows the existing enum pattern and is consistent with the broader implementation.

packages/language/syntaxes/zmodel.tmLanguage (1)

23-23: LGTM: Proper syntax highlighting support for alias keyword

The addition of "alias" to the keyword pattern correctly enables syntax highlighting for the new alias declarations. The alphabetical placement between "abstract" and "attribute" maintains consistency.

packages/schema/src/plugins/enhancer/policy/expression-writer.ts (2)

8-8: LGTM: Necessary import for alias support

The isAliasDecl import is correctly added to support alias declaration checking in the expression writer.


809-811: LGTM: Proper alias expression handling

The alias handling logic correctly identifies alias declarations and recursively processes their underlying expressions. This enables seamless use of aliases in policy expressions by effectively "inlining" the alias definition at invocation sites.

packages/schema/src/res/stdlib.zmodel (2)

69-71: LGTM: Proper extension of expression context system

The addition of AliasFunction to the ExpressionContext enum correctly extends the context system to support alias declarations. The documentation is clear and follows the existing pattern.


83-83: LGTM: Correct context extension for auth() function

Adding AliasFunction to the auth() function's allowed expression contexts properly enables its use within alias declarations while maintaining the existing validation framework.

packages/sdk/src/typescript-expression-transformer.ts (2)

27-27: LGTM: Import addition is appropriate

The import of isAliasExpr utility function is correctly added to support alias expression detection.


397-397: Improved error message with contextual information

The addition of ${expr.$cstNode?.text} provides valuable context for debugging unresolved expressions.

packages/schema/src/language-server/validator/attribute-application-validator.ts (2)

11-11: LGTM: Import addition supports alias validation

The isAliasDecl import is correctly added to enable alias declaration detection in attribute validation.


32-37: LGTM: Appropriate utility imports for type validation

The additional imports provide necessary functionality for type checking and validation in the alias context.

packages/schema/src/language-server/zmodel-semantic.ts (2)

2-2: LGTM: Import addition for semantic highlighting

The isAliasDecl import is correctly added to support alias declaration highlighting.


87-87: LGTM: Consistent semantic highlighting for aliases

Treating alias declarations the same as function declarations for semantic highlighting is appropriate since they are both callable entities.

packages/schema/src/language-server/validator/expression-validator.ts (5)

7-7: LGTM: Import addition for alias detection

The isAliasDecl import is correctly added to support alias context checking.


25-25: LGTM: Import addition for container traversal

The getContainerOfType import is appropriate for checking if expressions are contained within alias declarations.


37-37: LGTM: Appropriate exclusion of auth() validation in alias contexts

Excluding auth() validation when inside an alias declaration is logical since aliases may be used in different contexts where auth resolution requirements differ.


56-56: LGTM: Enhanced error message with contextual information

Adding ${expr.$cstNode?.text} provides valuable context for debugging unresolved expressions, consistent with similar improvements in other files.


54-54: Confirm logic fix for reference resolution error detection

The updated condition now correctly reports errors when hasReferenceResolutionError is true (i.e., when any member‐ or reference‐resolution errors are present). I verified this is the only occurrence in
packages/schema/src/language-server/validator/expression-validator.ts, and no residual negated checks remain.

packages/schema/tests/schema/validation/attribute-validation.test.ts (2)

409-409: LGTM: Updated error message reflects broader callable concept

The change from "function" to "AbstractCallable" correctly reflects the generalization to include both functions and aliases as callable entities.


1395-1468: Excellent comprehensive test coverage for alias expressions

The test suite thoroughly covers alias functionality including:

  • Basic alias usage in policy rules and default values
  • Nested alias expressions with auth() integration
  • Complex alias compositions
  • Type validation error cases

The tests demonstrate practical usage patterns and validate both positive and negative scenarios.

packages/schema/src/plugins/prisma/schema-generator.ts (2)

41-41: Import addition looks correct.

The isAliasExpr utility import aligns with the new alias functionality and is used appropriately in the method below.


966-972: Alias expression handling implementation is solid.

The logic correctly:

  • Detects alias expressions using the imported utility
  • Extracts the underlying expression from the alias's function reference
  • Recursively processes the resolved expression
  • Provides clear error messaging when the alias reference is missing

The recursive approach ensures that nested alias expressions would be properly resolved.

packages/sdk/src/utils.ts (3)

2-2: Import additions are appropriate.

The new imports (AbstractCallable, isAliasDecl, streamAst) support the alias functionality being added and are used correctly in the updated functions.

Also applies to: 17-17, 44-44


432-432: Good generalization of function signature.

Changing the parameter type from FunctionDecl to AbstractCallable appropriately extends support to both functions and aliases, which makes sense since both can have expression contexts.


452-454: Simple and effective alias expression detection.

The isAliasExpr function correctly identifies invocation expressions that reference alias declarations by checking the $type of the referenced function.

packages/schema/src/plugins/enhancer/policy/utils.ts (2)

17-17: Import updates support alias functionality.

The new imports correctly bring in the hasAuthInvocation utility and alias-related functions needed for the enhanced policy handling.

Also applies to: 46-51


517-517: Enhanced auth detection for alias expressions.

Switching from isAuthInvocation to hasAuthInvocation properly extends auth detection to include alias expressions that contain auth invocations, which is necessary for the alias feature.

packages/schema/src/language-server/validator/utils.ts (3)

8-8: Import addition supports new type mapping functionality.

The ResolvedShape import is correctly added to support the new mappedRawExpressionTypeToResolvedShape function.


90-101: Enhanced builtin type mapping with proper handling.

The addition of 'Object' and 'Unsupported' cases correctly extends the type mapping function. The function structure is properly maintained with the switch statement covering all cases.


106-121: Well-structured expression type to resolved shape mapping.

The new mappedRawExpressionTypeToResolvedShape function provides a clean mapping from AST node types to their corresponding resolved shapes. The mappings are logical:

  • Literal types map to their corresponding basic types
  • ObjectExpr maps to 'Object'
  • NullExpr maps to 'Null'
  • Default fallback to 'Any' is appropriate for unknown types
packages/schema/src/utils/ast-utils.ts (2)

14-14: LGTM!

The imports for isAliasDecl and AliasDecl are properly added to support the new alias declaration feature.

Also applies to: 18-18


319-327: LGTM!

The getAllLoadedAlias function follows the established pattern for retrieving declarations from all loaded documents and correctly filters for AliasDecl types.

tests/integration/tests/plugins/policy.test.ts (3)

114-173: LGTM!

The test case comprehensively validates simple alias expressions in both default values and policy rules, with proper assertions on the generated policy guards.


175-272: LGTM!

Excellent test coverage for complex alias expressions including enum comparisons, logical operators, and member access with collection predicates. The assertions correctly verify the generated Prisma filter objects.


274-330: LGTM!

Good test coverage for member access in aliases, including edge cases with empty collections and non-matching data. The test correctly verifies that the generated policy filter is consistent regardless of the input context.

packages/language/src/zmodel.langium (2)

14-14: LGTM!

The type system changes properly integrate alias declarations as first-class citizens in the language. The AbstractCallable abstraction elegantly unifies functions and aliases for invocation expressions.

Also applies to: 94-94, 97-97, 255-255


221-223: Verify the parameter design for alias declarations

The alias declaration syntax includes parameters, but according to the PR objectives, the author questions whether parameters are beneficial since "references are resolved in the model scope where the rule is invoked."

Should alias declarations support parameters? If not, consider simplifying the syntax:

 AliasDecl:
-    TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' '{' expression=Expression '}' (attributes+=InternalAttribute)*;
+    TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '{' expression=Expression '}' (attributes+=InternalAttribute)*;

The current implementation with parameters may lead to confusion if they don't actually create a new scope for the alias body.

Also applies to: 238-238

packages/schema/src/language-server/validator/function-invocation-validator.ts (3)

2-3: LGTM!

The generalization from functions to callables is well-implemented with proper null checks and updated error messages.

Also applies to: 11-11, 52-60, 67-69


164-172: LGTM!

The expression context handling correctly identifies alias declarations and returns the appropriate AliasFunction context.

Also applies to: 80-80


157-161: LGTM!

The refactoring correctly moves the function-specific checker logic inside the standard library check and properly updates the method signature to accept AbstractCallable.

Also applies to: 187-187

packages/schema/src/language-server/zmodel-linker.ts (3)

2-2: LGTM!

The imports are properly organized to support alias declarations and invocations.

Also applies to: 30-30, 37-37, 61-65


199-201: LGTM!

Good design decision to defer alias resolution until usage. This allows the alias expression to be resolved in the correct model context.


292-292: LGTM!

The invocation resolution correctly differentiates between function and alias invocations, with appropriate type handling.

Also applies to: 309-312

packages/schema/src/language-server/zmodel-scope.ts (3)

4-4: LGTM!

The import for isAliasDecl is properly added and aligns with the new alias declaration feature.


121-126: Alias scope checks are correctly implemented

The additions properly detect when scope resolution occurs within an alias declaration and delegate to specialized methods. This ensures that alias expressions can reference model fields appropriately.

Consider caching the alias declaration lookup result to avoid repeated getContainerOfType calls if performance becomes a concern in larger schemas.

Also applies to: 135-141


259-278: Well-structured alias scope resolution

The getAliasScope method correctly aggregates fields from all data models, allowing alias expressions to reference any model field. The use of getModelFieldsWithBases ensures inherited fields are included.

Comment on lines 320 to 323
(isReferenceExpr(child) && isDataModelField(child.target.ref)) ||
// TODO: field access from alias expression
isAliasInvocation(child)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Field access detection extended for aliases with incomplete support.

The code correctly adds isAliasInvocation(child) to detect field access patterns, but the TODO comment on line 322 indicates that field access from alias expressions isn't fully implemented yet. This could lead to incomplete policy validation.

Please verify that the incomplete alias field access support doesn't introduce security vulnerabilities or incorrect policy enforcement:


🏁 Script executed:

#!/bin/bash
# Search for other locations where field access detection might need alias support
echo "=== Searching for field access patterns that might need alias support ==="
rg -A 3 -B 3 "isDataModelField.*target\.ref" --type ts

echo "=== Searching for policy-related field reference checks ==="
rg -A 3 -B 3 "isReferenceExpr.*isDataModelField" --type ts

echo "=== Searching for existing alias invocation usage ==="
rg -A 3 -B 3 "isAliasInvocation" --type ts

Length of output: 13523


Incomplete alias field access handling may lead to missed policy checks

The addition of isAliasInvocation(child) in packages/schema/src/plugins/enhancer/policy/utils.ts correctly flags alias calls, but there is no logic to extract the underlying data‐model field. Our searches show:

  • getFieldAccess in packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts only handles reference and member accesses—no alias support.
  • policy-guard-generator.ts and related guard logic never resolve alias invocations to actual fields.

Without extracting the actual DataModelField from an alias invocation, policy rules that use aliases can slip past validation.

Please address this by:

  • In packages/schema/src/plugins/enhancer/policy/utils.ts (lines ~320–323), replace the bare isAliasInvocation(child) check with code that resolves the alias and yields the child’s target field.
  • In packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts within getFieldAccess, add a branch for isAliasInvocation(expr) to resolve the alias and return the correct { name: … }.
  • Update policy-guard-generator.ts (as needed) so that guards generated for alias-based expressions reference the actual field.
  • Add unit tests covering alias‐based field accesses in policy rules to prevent regressions.
🤖 Prompt for AI Agents
In packages/schema/src/plugins/enhancer/policy/utils.ts around lines 320 to 323,
replace the simple isAliasInvocation(child) check with logic that resolves the
alias invocation to extract and return the underlying DataModelField. Then, in
packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts within the
getFieldAccess function, add a case to handle isAliasInvocation expressions by
resolving the alias and returning the correct field name object. Also, update
policy-guard-generator.ts to ensure that guards generated for alias-based
expressions reference the resolved actual data-model field instead of the alias.
Finally, add unit tests that cover alias-based field accesses in policy rules to
ensure these changes prevent missed validations and regressions.

Comment on lines 168 to 174
export function isAliasInvocation(node: AstNode) {
// check if a matching alias exists
const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? [];
// const aliasDecls = getAllLoadedAlias(this.langiumDocuments());
return isInvocationExpr(node) && allAlias.some((alias) => alias.name === node.function.$refText);
// (!node.function.ref || !isFromStdlib(node.function.ref)) /* && isAliasDecl(node.function.ref) */
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Clean up commented code and consider cross-model alias resolution

The function has commented-out code that should be removed. Additionally, the current implementation only checks aliases within the containing model, which may not handle imported aliases correctly.

Consider this cleaner implementation:

 export function isAliasInvocation(node: AstNode) {
-    // check if a matching alias exists
-    const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? [];
-    // const aliasDecls = getAllLoadedAlias(this.langiumDocuments());
-    return isInvocationExpr(node) && allAlias.some((alias) => alias.name === node.function.$refText);
-    // (!node.function.ref || !isFromStdlib(node.function.ref)) /*  && isAliasDecl(node.function.ref) */
+    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);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function isAliasInvocation(node: AstNode) {
// check if a matching alias exists
const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? [];
// const aliasDecls = getAllLoadedAlias(this.langiumDocuments());
return isInvocationExpr(node) && allAlias.some((alias) => alias.name === node.function.$refText);
// (!node.function.ref || !isFromStdlib(node.function.ref)) /* && isAliasDecl(node.function.ref) */
}
export function 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);
}
🤖 Prompt for AI Agents
In packages/schema/src/utils/ast-utils.ts around lines 168 to 174, remove the
commented-out code to clean up the function. Then, update the alias resolution
logic to check aliases not only in the containing model but also across imported
models to correctly handle cross-model alias references.

Comment on lines 92 to 93
// TODO: ensure referenceExpr from alias is resolved
return this.reference(expr as ReferenceExpr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Address the TODO comment for alias reference resolution

This TODO indicates that ReferenceExpr from aliases may not be properly resolved, which could lead to transformation failures.

Do you want me to help implement the reference resolution logic for alias expressions, or should this be tracked as a separate issue?


🏁 Script executed:

#!/bin/bash
# Search for ReferenceExpr handling in alias contexts to understand the current state
ast-grep --pattern $'ReferenceExpr' | head -20
rg -A 5 "ReferenceExpr.*alias" 

Length of output: 1806


Implement alias reference resolution or track as a separate issue

It looks like the // TODO: ensure referenceExpr from alias is resolved in
packages/sdk/src/typescript-expression-transformer.ts (lines 92–93) remains unaddressed—there is no logic that unwraps or resolves alias definitions before calling this.reference(expr as ReferenceExpr). Without that, aliased references can fail during transformation.

Please choose one of the following:

  • Implement alias resolution now
    Add code to detect when expr originates from an alias (e.g., via a lookup or mapping function) and replace it with the underlying ReferenceExpr before invoking this.reference(...).
  • Track as a separate issue
    Create a ticket to tackle alias-resolution in a dedicated PR, and update/remove the TODO to reference that issue.

Let me know which approach you’d like—and I can help with the implementation if you prefer.

🤖 Prompt for AI Agents
In packages/sdk/src/typescript-expression-transformer.ts around lines 92 to 93,
the TODO about resolving alias references before calling this.reference is not
implemented. To fix this, either add logic to detect if expr is an alias and
unwrap it to get the underlying ReferenceExpr before passing it to
this.reference, or create a separate issue to address alias resolution later and
update or remove the TODO comment to reference that issue accordingly.

@ymc9
Copy link
Member

ymc9 commented Jul 27, 2025

Hi @Azzerty23 , thanks a lot for driving this effort. A sharing mechanism of policy rules is a long-desired feature.

I think we have two main things to discuss:

  1. Is this mechanism more a "fragment" (its content literally copied over to the place where it's applied) or a "function" (with well-defined input and output)
  2. The keyword to use

For #1, my tendency is to conceptualize it as a function, because the concept already exists today, and a function can have its input's types defined so we can keep the ZModel a statically typed language as it is today). So it's basically:

[keyword] isCurrentUser(user: User) {
  user.id == auth().id
}

...

model User {
  ...
  @@allow('all', isCurrentUser(this))
}

model Post {
  ...
  @@allow('all', isCurrentUser(author))
}

Love to hear your thoughts about this.

The benefit is that the resolution is very clean and straightforward. The downside is you can only call the function where the type matches. It doesn't apply to other models that also have an "id" field. My feeling is that this should generally be fine, thanks to abstract inheritance and polymorphism. But please also verify if it works for your use cases.

For #2, we can choose to use a new keyword like rule, etc., so it becomes explicit that this construct is specifically used for policy rules. Or we can just reuse the function keyword, keeping the option open that such a construct can potentially be used in other context as well (e.g., @default). My current tendency is to reuse function and avoid introducing a new concept.

@Azzerty23
Copy link
Contributor Author

Azzerty23 commented Jul 27, 2025

Hi @ymc9, thank you for your response! While implementing this, I realized what I created was more like a fragment, but that wasn’t driven by any specific use case. I agree that a function, as you described, makes much more sense. Fragments feel too restrictive for code reusability since their references rely on having the same names.

I also agree with your second point about reusing the existing function keyword instead of introducing something new. The type-safe aspect you mentioned is definitely important, so I’ll experiment with the function-based approach and share my findings.

One question: do you think it would make sense to support parameters with default values for extra flexibility? (maybe as a future enhancement)

@ymc9
Copy link
Member

ymc9 commented Jul 28, 2025

Hi @ymc9, thank you for your response! While implementing this, I realized what I created was more like a fragment, but that wasn’t driven by any specific use case. I agree that a function, as you described, makes much more sense. Fragments feel too restrictive for code reusability since their references rely on having the same names.

I also agree with your second point about reusing the existing function keyword instead of introducing something new. The type-safe aspect you mentioned is definitely important, so I’ll experiment with the function-based approach and share my findings.

One question: do you think it would make sense to support parameters with default values for extra flexibility? (maybe as a future enhancement)

Awesome! So we're very much on the same page. I suggest we keep parameter default out of the scope first to keep it simple.

Additionally, please note that a significant portion of the implementation will need to be rewritten in v3 due to the entirely different mechanism for access policies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants