Skip to content
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

fix(json): support enums in type declarations #1837

Merged
merged 2 commits into from
Nov 7, 2024
Merged

fix(json): support enums in type declarations #1837

merged 2 commits into from
Nov 7, 2024

Conversation

ymc9
Copy link
Member

@ymc9 ymc9 commented Nov 7, 2024

fixes #1834

Copy link
Contributor

coderabbitai bot commented Nov 7, 2024

📝 Walkthrough
📝 Walkthrough

Walkthrough

The pull request introduces modifications to the grammar definitions of the ZModel language, particularly around TypeDef and TypeDefField. It allows for optional repetition of TypeDefField, introduces a new type alias TypeDefFieldTypes, and updates references accordingly. Enhancements to the model-typedef-generator.ts file improve enum handling in TypeScript type generation. Changes to the Transformer class streamline enum processing. Additionally, new test cases are added to validate the handling of enums in CRUD operations and typing scenarios.

Changes

File Path Change Summary
packages/language/src/zmodel.langium - Modified TypeDef to allow zero or more TypeDefField.
- Introduced type alias TypeDefFieldTypes for TypeDef and Enum.
- Updated TypeDefFieldType and DataModelFieldType to reference TypeDefFieldTypes.
packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts - Added makeEnumTypeReference function to handle enum type representation in TypeScript.
- Updated zmodelTypeToTsType to check for enum types and call makeEnumTypeReference.
packages/schema/src/plugins/zod/transformer.ts - Removed enumTypes property from Transformer class.
- Updated generateEnumSchemas to filter enums directly from this.zmodel.declarations.
tests/integration/tests/enhancements/json/crud.test.ts - Added tests for handling enums in data models: respects enums used by data models and respects enums unused by data models.
tests/integration/tests/enhancements/json/typing.test.ts - Added tests for enums in models, including a new Role enum and its usage in Profile and Foo models.

Assessment against linked issues

Objective Addressed Explanation
Use Enum field type in JSON type (Issue #1834)

Possibly related PRs


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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 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.

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: 1

🧹 Outside diff range and nitpick comments (6)
packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts (1)

69-81: Consider performance optimization and additional documentation.

The implementation is functionally correct but could benefit from the following improvements:

  1. Performance: Consider caching the model reference check results to avoid repeated traversals, especially if this function is called frequently during type generation.

  2. Documentation: Add more detailed JSDoc comments explaining:

    • The function's purpose
    • The rationale behind different handling of referenced vs. non-referenced enums
    • Parameters and return value
  3. Edge Cases: Add validation for empty enums

Here's a suggested improvement:

+/**
+ * Generates a TypeScript type reference for an enum declaration.
+ * 
+ * If the enum is referenced by any data model field, returns the enum name since
+ * Prisma will generate the corresponding type. Otherwise, generates an inline
+ * union type of string literals for better type isolation.
+ *
+ * @param enumDecl - The enum declaration to generate a type reference for
+ * @returns A string representing the TypeScript type
+ * @throws {PluginError} When the enum has no fields
+ */
 function makeEnumTypeReference(enumDecl: Enum) {
+    if (enumDecl.fields.length === 0) {
+        throw new PluginError(name, `Enum ${enumDecl.name} has no fields`);
+    }
+
     const zmodel = enumDecl.$container;
     const models = getDataModels(zmodel);
 
     if (models.some((model) => model.fields.some((field) => field.type.reference?.ref === enumDecl))) {
         return enumDecl.name;
     } else {
         return enumDecl.fields.map((field) => `'${field.name}'`).join(' | ');
     }
 }
packages/language/src/zmodel.langium (1)

189-190: Well-designed type system enhancement for enum support.

The introduction of TypeDefFieldTypes as a union type and its usage in TypeDefFieldType is an elegant solution that:

  1. Enables enum fields in JSON type declarations
  2. Maintains backward compatibility
  3. Follows the principle of composition over inheritance

This change effectively implements the feature request while keeping the grammar clean and maintainable.

Consider documenting this enhancement in the language specification to help users understand they can now use enums in type definitions.

Also applies to: 196-196

tests/integration/tests/enhancements/json/typing.test.ts (2)

182-234: Enhance test coverage for enum validation.

The test verifies basic enum functionality but could be strengthened by:

  1. Testing both enum values (USER and ADMIN)
  2. Adding negative test cases for invalid enum values
  3. Testing type inference without explicit Profile type annotation

Consider adding these test scenarios:

 async function main() {
     const profile: Profile = {
         role: Role.ADMIN,
     }
+    const profile2: Profile = {
+        role: Role.USER,
+    }
+    // Test type inference
+    const profile3 = {
+        role: Role.ADMIN,
+    }
 
     await db.user.create({ data: { profile: { role: Role.ADMIN } } });
+    await db.user.create({ data: { profile: { role: Role.USER } } });
     const user = await db.user.findFirstOrThrow();
     console.log(user.profile.role === Role.ADMIN);
+    
+    // Negative test case - should fail type checking
+    // @ts-expect-error
+    await db.user.create({ data: { profile: { role: 'INVALID_ROLE' } } });
 }

182-282: Consider structural improvements to enum tests.

The two enum-related tests share similar setup and validation patterns. Consider:

  1. Extracting common schema parts to reduce duplication
  2. Adding shared validation utilities for enum values
  3. Documenting the relationship between these tests

Consider extracting common parts:

// Common schema parts
const enumSchema = `
enum Role {
    USER
    ADMIN
}

type Profile {
    role Role
}
`;

// Common validation utility
const validateEnumUsage = async (db: any, useEnumType: boolean) => {
    const value = useEnumType ? Role.ADMIN : 'ADMIN';
    await db.user.create({ data: { profile: { role: value } } });
    const user = await db.user.findFirstOrThrow();
    expect(user.profile.role).toBe(value);
};
tests/integration/tests/enhancements/json/crud.test.ts (1)

194-233: Enhance test coverage for enum validation.

While the test case effectively validates basic enum functionality, consider adding the following scenarios for more comprehensive coverage:

  • Test the USER enum value to ensure all enum values work
  • Add tests for updating enum values
  • Verify case sensitivity handling (e.g., 'admin' vs 'ADMIN')
 it('respects enums used by data models', async () => {
     // ... existing setup ...

     // Test all enum values
+    await expect(db.user.create({ data: { profile: { role: 'USER' } } })).resolves.toMatchObject({
+        profile: { role: 'USER' },
+    });
     
     // Test updates
+    const user = await db.user.create({ data: { profile: { role: 'USER' } } });
+    await expect(
+        db.user.update({ 
+            where: { id: user.id },
+            data: { profile: { role: 'ADMIN' } }
+        })
+    ).resolves.toMatchObject({
+        profile: { role: 'ADMIN' },
+    });
     
     // Test case sensitivity
+    await expect(db.user.create({ data: { profile: { role: 'admin' } } })).toBeRejectedByPolicy();

     // ... existing tests ...
 });
packages/schema/src/plugins/zod/transformer.ts (1)

54-61: Consider adding error handling for enum fields access.

The code assumes enum fields will always exist. Consider adding validation to handle potential edge cases.

 const enums = this.zmodel.declarations.filter(isEnum);
 for (const enumDecl of enums) {
+    if (!enumDecl.fields?.length) {
+        console.warn(`Warning: Enum ${enumDecl.name} has no fields`);
+        continue;
+    }
     const name = upperCaseFirst(enumDecl.name);
     const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1fec0 and a5257ee.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**, !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**, !**/generated/**
📒 Files selected for processing (5)
  • packages/language/src/zmodel.langium (1 hunks)
  • packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts (3 hunks)
  • packages/schema/src/plugins/zod/transformer.ts (2 hunks)
  • tests/integration/tests/enhancements/json/crud.test.ts (1 hunks)
  • tests/integration/tests/enhancements/json/typing.test.ts (1 hunks)
🔇 Additional comments (9)
packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts (3)

1-2: LGTM: Imports are well-organized and specific.

The new imports appropriately support the enum functionality being added.


39-43: LGTM: Clean enum type handling implementation.

The implementation correctly differentiates between enum and non-enum references, maintaining backward compatibility while adding enum support.


39-43: Verify alignment with PR objectives.

The implementation successfully addresses the requirements from issue #1834:

  1. Properly detects enum types in type declarations
  2. Generates appropriate TypeScript types for enums
  3. Handles both cases where enums are referenced by data models and where they're used only in JSON types

Let's verify the implementation works with the example from the issue:

Also applies to: 69-81

✅ Verification successful

Implementation correctly handles enum types in JSON type declarations

The implementation is properly handling enum types as evidenced by:

  1. Comprehensive enum detection and type generation logic:

    • isEnum() checks are properly implemented and used throughout the codebase
    • makeEnumTypeReference() function intelligently handles both referenced and inline enum cases
    • Proper type generation for enums referenced in data models vs. JSON-only enums
  2. Extensive test coverage for enum handling:

    • Multiple test files contain enum declarations and their usage
    • Various enum scenarios are covered (Role, UserRole, etc.)
    • Tests exist across different contexts: JSON typing, CRUD operations, and schema validation
  3. The implementation in model-typedef-generator.ts correctly:

    • Detects enum types via isEnum(type.reference.ref)
    • Generates appropriate TypeScript types using makeEnumTypeReference
    • Handles both cases: referenced enums (using enum name) and JSON-only enums (using union types)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the implementation handles the example use case from issue #1834

# Test: Search for any existing enum type declarations matching the example
rg "enum\s+TagType\s*\{[\s\n]*One[\s\n]*,[\s\n]*Two[\s\n]*,[\s\n]*Three" 

# Test: Search for any JSON type declarations using enums
rg "type\s+TestData\s*\{[\s\n]*tag\??\s*:\s*TagType"

Length of output: 129


Script:

#!/bin/bash
# Let's verify the enum handling implementation more broadly

# Check for enum type declarations in test files
rg "enum\s+\w+\s*\{" -g "*.test.ts" -g "*.spec.ts"

# Check for actual enum usage in model definitions
rg "isEnum\(" -A 2

# Look for enum type references in the codebase
ast-grep --pattern 'makeEnumTypeReference($_)'

# Check the implementation of makeEnumTypeReference function
ast-grep --pattern 'function makeEnumTypeReference($_) {
  $$$
}'

# Look for test cases related to enum handling
rg "test\(.*enum" -A 3

Length of output: 11546

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

192-192: LGTM - Added comment support.

Good addition of comment support for type definition fields, maintaining consistency with other language constructs.


186-186: Verify the impact of making TypeDef fields optional.

The change from fields+=TypeDefField+ to fields+=TypeDefField* makes fields optional in type definitions. While this provides more flexibility, it could potentially break existing code that assumes type definitions must have at least one field.

✅ Verification successful

Based on the extensive search results, I can now generate a final response:

Making TypeDef fields optional is a valid change that aligns with JSON type support.

The change from fields+=TypeDefField+ to fields+=TypeDefField* is safe because:

  1. All existing type definitions in test files (tests/integration/tests/) contain fields, showing no current usage of empty types
  2. The change is part of a larger feature to support enums in JSON type declarations, where empty type definitions might be needed
  3. The codebase already has robust validation for type definitions through TypeDefValidator
  4. The runtime and enhancer code properly handles both empty and non-empty type definitions
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for empty type definitions in the codebase to assess impact
echo "Searching for potentially affected type definitions..."
rg -U "type\s+\w+\s*\{\s*\}" --type ts

Length of output: 159


Script:

#!/bin/bash
# Let's try a different approach to analyze type definitions:
# 1. Look for existing type definitions in test files
# 2. Check documentation or examples
# 3. Look for TypeDef rule usage patterns

# Search in test files for type definitions
echo "Searching in test files..."
rg -U "type\s+\w+" --type test

# Check for documentation examples
echo -e "\nChecking documentation..."
fd -e md -x grep -l "type.*{" {} \;

# Look for TypeDef rule usage in grammar tests
echo -e "\nChecking grammar tests..."
fd -g "*test*.ts" -x grep -l "TypeDef" {} \;

Length of output: 347


Script:

#!/bin/bash
# Let's try one final approach to analyze the impact:
# 1. Look for type definitions in .ts files
# 2. Check for test cases specifically related to TypeDef rule
# 3. Look for examples in documentation

# Search for type definitions in TypeScript files
echo "Searching for type definitions..."
rg -U "type\s+\w+\s*\{" --type ts

# Look for TypeDef-related test cases
echo -e "\nSearching for TypeDef tests..."
rg "TypeDef" -A 5 -B 5 --type ts

# Check examples in docs
echo -e "\nChecking examples in documentation..."
rg "type\s+\w+\s*\{" -A 3 --type md

Length of output: 96363

tests/integration/tests/enhancements/json/typing.test.ts (1)

236-282: 🛠️ Refactor suggestion

⚠️ Potential issue

Clarify the distinction between enum and string literal usage.

The test demonstrates inconsistent usage patterns between the two test cases:

  1. First test uses Role enum from @prisma/client
  2. Second test uses string literals ('ADMIN')

This could lead to confusion about the correct approach. Consider:

  1. Adding comments explaining when each approach is appropriate
  2. Adding validation to ensure string literals match enum values
  3. Testing error cases for invalid string values

Let's verify if this behavior is documented elsewhere:

Consider updating the test to clarify the behavior:

 async function main() {
+    // Demonstrate that both enum and string literal approaches work
     const profile: Profile = {
         role: 'ADMIN',
     }
+    
+    // Validate that only valid enum values are accepted
+    // @ts-expect-error
+    const invalidProfile: Profile = {
+        role: 'INVALID_VALUE',
+    }
 
     await db.user.create({ data: { profile: { role: 'ADMIN' } } });
     const user = await db.user.findFirstOrThrow();
     console.log(user.profile.role === 'ADMIN');
+    
+    // Test all valid enum values
+    await db.user.create({ data: { profile: { role: 'USER' } } });
 }
packages/schema/src/plugins/zod/transformer.ts (3)

3-3: LGTM: Import addition aligns with enum support.

The addition of isEnum import is necessary for the new enum type support functionality.


54-61: LGTM: Improved enum schema generation implementation.

The new implementation directly filters declarations for enums and maps their fields to schema values, providing a more maintainable and structured approach.


67-69: LGTM: Consistent enum schema exports.

The export generation correctly uses the updated enum declarations.

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: 0

🧹 Outside diff range and nitpick comments (1)
packages/schema/src/plugins/zod/transformer.ts (1)

Line range hint 17-17: Remove unused enumTypes property.

The enumTypes property is declared but never initialized or used in the code. This could lead to undefined behavior if accessed.

Apply this diff to remove the unused property:

-    enumTypes: readonly PrismaDMMF.SchemaEnum[];
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a5257ee and 2f8fbc1.

📒 Files selected for processing (1)
  • packages/schema/src/plugins/zod/transformer.ts (3 hunks)
🔇 Additional comments (3)
packages/schema/src/plugins/zod/transformer.ts (3)

3-3: LGTM: Import addition is appropriate.

The addition of isEnum import aligns with the PR's objective to support enum types in JSON declarations.


Line range hint 745-757: LGTM: Schema import generation is correct.

The method properly handles different types of imports, including the newly supported enum schemas, using the static enumNames array for validation.


Line range hint 56-80: LGTM: Comprehensive enum schema generation implementation.

The implementation properly handles both DMMF enums and additional enums from declarations. The code is well-structured and maintains a clear separation of concerns.

Let's verify that all enum declarations are properly processed:

✅ Verification successful

The previous commands didn't yield results, possibly due to the sandbox environment state. Let's try these alternative commands to gather information about enum declarations and their schema generations.


The previous output shows that z.enum is being used correctly in the transformer, but we need more context about the enum declarations and transformer implementation to verify the complete flow.


Based on the verification results, I can now provide a final response:

LGTM: Enum schema generation implementation is correctly handling both DMMF and declaration enums

The code correctly processes both types of enums:

  • DMMF enums from the Prisma schema are handled in the first loop
  • Additional enums from declarations (not referenced by data models) are properly captured in the second loop
  • Both implementations generate consistent schema files with proper z.enum() declarations
  • The implementation maintains a generated array to track processed enums and avoid duplicates

The test files show various enum use cases that would exercise this code path, confirming its robustness.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that all enum declarations have corresponding schema files

# Find all enum declarations in the codebase
ast-grep --pattern 'enum $enumName {
  $$$
}'

# Find all generated enum schema files
fd -e ts -p '.*\.schema\.ts$' -x rg -l 'z.enum\('

Length of output: 99


Script:

#!/bin/bash
# Let's try a different approach to verify enum handling

# First, let's check for enum declarations in .ts files
rg "enum\s+\w+\s*{" --type ts -A 5

# Then check for z.enum usage in schema files
rg "z\.enum\(" --type ts -A 2

# Also check the specific output directory structure
fd "enums" --type d

Length of output: 852


Script:

#!/bin/bash
# Let's try one more verification with fixed regex

# Check for enum declarations
rg "enum [A-Za-z]+" --type ts -A 5

# Check the transformer class implementation
rg "class Transformer" --type ts -A 10

# Look for enum-related type definitions
rg "type.*Enum" --type ts -A 3

Length of output: 48039

@ymc9 ymc9 merged commit 1bd1b8f into dev Nov 7, 2024
13 checks passed
@ymc9 ymc9 deleted the fix/issue-1834 branch November 7, 2024 20:03
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.

1 participant