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(zod): "strip" mode causes create payload fields to be accidentally dropped #1747

Merged
merged 2 commits into from
Sep 27, 2024

Conversation

ymc9
Copy link
Member

@ymc9 ymc9 commented Sep 27, 2024

Fixes #1746

Copy link
Contributor

coderabbitai bot commented Sep 27, 2024

📝 Walkthrough
📝 Walkthrough

Walkthrough

The changes introduce a new utility file zod-utils.ts with functions to improve Zod schema handling, particularly for union types. The ZodSchemaGenerator class is updated to include a mode property for schema generation, enhancing validation logic. The Transformer class is modified to incorporate a new ObjectMode type, affecting how schemas are generated based on the mode. A regression test is added to ensure the schema validation works correctly in various scenarios, addressing issues related to the "strip" mode in Zod.

Changes

Files Change Summary
packages/runtime/src/zod-utils.ts Introduced utility functions for handling Zod schemas, including smartUnion, smartArrayUnion, and smartObjectUnion, among others.
packages/schema/src/plugins/zod/generator.ts Added mode property to ZodSchemaGenerator, integrated validation logic into the constructor, and updated method calls to use the new mode.
packages/schema/src/plugins/zod/transformer.ts Introduced ObjectMode type, modified schema generation methods to conditionally use smartUnion, and added wrapWithSmartUnion method for union handling based on mode.
packages/schema/src/plugins/zod/types.ts Updated TransformerParams to include mode property and defined ObjectMode as a union type.
tests/regression/tests/issue-1746.test.ts Added a regression test for schema validation in a Prisma context, covering various input scenarios to ensure correct parsing behavior.

Assessment against linked issues

Objective Addressed Explanation
Zod plugin's "strip" mode causes create payload fields to be accidentally dropped (#1746)

Possibly related PRs


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between c68df70 and 5489e21.

📒 Files selected for processing (1)
  • packages/runtime/tests/zod/smart-union.test.ts (1 hunks)
🔇 Additional comments (1)
packages/runtime/tests/zod/smart-union.test.ts (1)

1-109: Comprehensive and Well-Structured Test Suite

The test cases effectively cover a wide range of scenarios for the smartUnion function, including scalar unions, non-ambiguous and ambiguous object unions, array unions, lazy schemas, and mixed object and array unions. The tests are thorough and ensure that the smartUnion function behaves as expected across different configurations and input types.


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

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

30-30: Well-defined ObjectMode type.

The ObjectMode type is a great addition that provides type safety and clarity for specifying the transformer's behavior. It directly addresses the PR's focus on the "strip" mode while also offering flexibility with 'strict' and 'passthrough' options.

Consider adding a brief comment above the type definition to explain the purpose of each mode. This would enhance code readability and maintainability. For example:

/**
 * Defines the mode of operation for object transformation:
 * - 'strict': Throws an error on unknown properties
 * - 'strip': Removes unknown properties
 * - 'passthrough': Allows unknown properties
 */
export type ObjectMode = 'strict' | 'strip' | 'passthrough';
packages/schema/src/plugins/zod/generator.ts (1)

44-56: LGTM: Constructor updated with mode validation and initialization.

The constructor has been updated to include validation for the mode option and initialization of the mode property. The changes look good and improve the robustness of the class.

Consider extracting the allowed mode values into a constant array for better maintainability. For example:

const ALLOWED_MODES = ['strip', 'strict', 'passthrough'] as const;
// ...
if (
    this.options.mode &&
    (typeof this.options.mode !== 'string' || !ALLOWED_MODES.includes(this.options.mode as any))
) {
    throw new PluginError(
        name,
        `Invalid mode option: "${this.options.mode}". Must be one of ${ALLOWED_MODES.join(', ')}.`
    );
}

This approach would make it easier to update the allowed modes in the future and ensure consistency throughout the code.

packages/runtime/src/zod-utils.ts (2)

13-52: Evaluate performance impact of multiple schema evaluations

The smartUnion function evaluates multiple candidate schemas to determine the one with the fewest unrecognized keys. While this approach improves accuracy in schema selection, it may impact performance, especially with a large number of candidates or complex schemas.

Consider profiling the performance and, if necessary, optimizing the candidate selection process to balance accuracy and efficiency.


54-117: Enhance error handling for unexpected inputs

Functions like smartArrayUnion, smartObjectUnion, and countUnrecognizedKeys may encounter unexpected inputs or edge cases. Improving error handling and input validation can make the code more robust and easier to debug.

tests/regression/tests/issue-1746.test.ts (5)

62-65: Consider adding assertions for the content field.

In this test case, you assert the values of userId, submissionId, and verify that unknown is undefined. To ensure comprehensive test coverage, consider adding an assertion for content to confirm it is correctly parsed.


76-79: Add assertion for the content field in the checked scenario.

Similar to the previous test, including an assertion for content will enhance the test's completeness by verifying all expected fields.


99-101: Include assertion for the content field in nested create schema tests.

In this nested create schema test, you assert userId and check that unknown is undefined. Adding an assertion for content will provide a more thorough verification of the parsed data.


115-118: Assert all elements in the unchecked array test.

Currently, assertions are made only for the first element in the array. To ensure all items are correctly parsed, consider adding assertions for the second element as well.


135-138: Extend assertions to all array elements in the checked array test.

Similar to the previous suggestion, including assertions for both elements in the array will enhance the test's robustness.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 1197c70 and c68df70.

⛔ Files ignored due to path filters (1)
  • packages/runtime/package.json is excluded by !**/*.json
📒 Files selected for processing (5)
  • packages/runtime/src/zod-utils.ts (1 hunks)
  • packages/schema/src/plugins/zod/generator.ts (5 hunks)
  • packages/schema/src/plugins/zod/transformer.ts (11 hunks)
  • packages/schema/src/plugins/zod/types.ts (2 hunks)
  • tests/regression/tests/issue-1746.test.ts (1 hunks)
🔇 Additional comments (15)
packages/schema/src/plugins/zod/types.ts (2)

17-17: Excellent addition to address the "strip" mode issue.

The inclusion of the mode property in TransformerParams is a crucial change that directly addresses the PR's objective. This addition will allow for better control over how object properties are handled, particularly in "strip" mode, which should resolve the issue of accidentally dropped fields.


Line range hint 1-30: Verify implementation in related files.

The changes in this file provide a solid foundation for addressing the "strip" mode issue. To ensure full resolution of the problem:

Please run the following script to check the usage of the new ObjectMode type and mode property in other relevant files:

This will help ensure that the changes are properly implemented throughout the Zod plugin.

packages/schema/src/plugins/zod/generator.ts (5)

26-26: LGTM: Import statement for ObjectMode added.

The import statement for ObjectMode from './types' has been correctly added. This import is necessary for the new mode property in the ZodSchemaGenerator class.


32-32: LGTM: Mode property added to ZodSchemaGenerator class.

The mode property has been correctly added as a private readonly property of type ObjectMode. This addition aligns with the class's purpose and maintains proper encapsulation.


223-223: LGTM: Mode property passed to Transformer in generateEnumSchemas.

The mode property has been correctly added to the Transformer constructor call in the generateEnumSchemas method. This ensures consistent behavior across the schema generation process.


252-252: LGTM: Mode property passed to Transformer in generateObjectSchemas.

The mode property has been correctly added to the Transformer constructor call in the generateObjectSchemas method. This change is consistent with the modification in generateEnumSchemas and ensures proper mode handling during object schema generation.


Line range hint 1-653: Overall assessment: Changes effectively address the PR objectives.

The modifications to the ZodSchemaGenerator class, including the addition of the mode property and its usage throughout the class, effectively address the issue mentioned in the PR objectives. The "strip" mode problem with create payload fields being accidentally dropped should now be resolved.

These changes provide more flexibility in schema generation based on the specified mode, allowing for better control over how Zod schemas are generated and validated. The consistent application of the mode property across different schema generation methods ensures a cohesive approach to handling the various modes (strip, strict, passthrough).

The code changes are well-implemented, maintain good coding practices, and improve the overall functionality of the Zod plugin.

tests/regression/tests/issue-1746.test.ts (1)

54-148: Tests effectively validate schema behavior with 'strip' mode.

The test cases comprehensively cover various scenarios, ensuring that unknown fields are stripped and that required fields are properly validated in both checked and unchecked modes.

packages/schema/src/plugins/zod/transformer.ts (7)

10-10: Import statement updated correctly.

The required types AggregateOperationSupport, ObjectMode, and TransformerParams are correctly imported from './types'.


31-31: 'mode' property added and initialized correctly.

The 'mode' property is added to the Transformer class and properly initialized in the constructor using params.mode.

Also applies to: 44-44


78-83: Conditional import of 'smartUnion' implemented correctly.

The generateImportZodStatement method now conditionally imports the smartUnion helper when mode is set to 'strip', ensuring necessary functionality is available.


220-232: Updated union generation logic to handle 'Unchecked' types appropriately.

The logic correctly utilizes wrapWithSmartUnion when alternatives include 'Unchecked' types, ensuring that the best candidate is parsed at runtime.


435-441: 'wrapWithSmartUnion' method implemented correctly.

The wrapWithSmartUnion method abstracts union creation, using smartUnion when in 'strip' mode and defaulting to z.union otherwise.


482-482: Import statements updated appropriately.

The generateImportZodStatement method is correctly included in the imports array, ensuring necessary imports are generated.


Line range hint 541-548: Data schema generation updated to use 'wrapWithSmartUnion'.

The data schemas for create, update, and upsert operations now use wrapWithSmartUnion when generateUnchecked is true, correctly combining checked and unchecked input types.

Also applies to: 589-592, 610-613, 633-636, 639-642

@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
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

Avoid disabling ESLint rules and eliminate any types

Disabling @typescript-eslint/no-explicit-any might hide potential type safety issues. It's recommended to avoid using any and instead provide explicit type definitions to ensure full type safety and maintainability.

Comment on lines +76 to +77
const schema = candidates.find((candidate) => candidate.element === itemSchema);
return schema!.safeParse(data);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle undefined schema to prevent runtime errors

In smartArrayUnion, the schema obtained from .find() may be undefined if no matching candidate is found. Using schema! assumes it's always defined, which can lead to runtime errors.

Add a check to ensure schema is defined before proceeding:

 const schema = candidates.find((candidate) => candidate.element === itemSchema);
+if (!schema) {
+    return { data: undefined, success: false };
+}
 return schema.safeParse(data);
📝 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
const schema = candidates.find((candidate) => candidate.element === itemSchema);
return schema!.safeParse(data);
const schema = candidates.find((candidate) => candidate.element === itemSchema);
if (!schema) {
return { data: undefined, success: false };
}
return schema.safeParse(data);

Comment on lines +93 to +110
const strictResults = candidates.map((candidate) => {
// make sure to strip `z.lazy` before parsing
const unwrapped = unwrapLazy(z, candidate);
return {
schema: candidate,
// force object schema to run in strict mode to capture unrecognized keys
result: unwrapped.strict().safeParse(data),
};
});

// find the schema with the fewest unrecognized keys
const { schema } = strictResults.sort((a, b) => {
const aCount = countUnrecognizedKeys(a.result.error?.issues ?? []);
const bCount = countUnrecognizedKeys(b.result.error?.issues ?? []);
return aCount - bCount;
})[0];
return schema;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure consistency between unwrapped and returned schemas

In identifyCandidate, you unwrapped candidate to unwrapped for parsing but return the original candidate in schema, which may still be a ZodLazy. This inconsistency can cause issues if the returned schema is used without unwrapping.

Modify the return object to include the unwrapped schema:

 return {
-    schema: candidate,
+    schema: unwrapped,
     result: unwrapped.strict().safeParse(data),
 };

This change ensures that the schema used for parsing and the one returned are the same, preventing potential errors when the schema is used later.

📝 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
const strictResults = candidates.map((candidate) => {
// make sure to strip `z.lazy` before parsing
const unwrapped = unwrapLazy(z, candidate);
return {
schema: candidate,
// force object schema to run in strict mode to capture unrecognized keys
result: unwrapped.strict().safeParse(data),
};
});
// find the schema with the fewest unrecognized keys
const { schema } = strictResults.sort((a, b) => {
const aCount = countUnrecognizedKeys(a.result.error?.issues ?? []);
const bCount = countUnrecognizedKeys(b.result.error?.issues ?? []);
return aCount - bCount;
})[0];
return schema;
}
const strictResults = candidates.map((candidate) => {
// make sure to strip `z.lazy` before parsing
const unwrapped = unwrapLazy(z, candidate);
return {
schema: unwrapped,
// force object schema to run in strict mode to capture unrecognized keys
result: unwrapped.strict().safeParse(data),
};
});
// find the schema with the fewest unrecognized keys
const { schema } = strictResults.sort((a, b) => {
const aCount = countUnrecognizedKeys(a.result.error?.issues ?? []);
const bCount = countUnrecognizedKeys(b.result.error?.issues ?? []);
return aCount - bCount;
})[0];
return schema;
}

Comment on lines +22 to +51
let resultData: any;

return z
.custom((data) => {
if (Array.isArray(data)) {
const { data: result, success } = smartArrayUnion(
z,
processedCandidates.filter((c) => c instanceof z.ZodArray),
data
);
if (success) {
resultData = result;
}
return success;
} else {
const { data: result, success } = smartObjectUnion(
z,
processedCandidates.filter((c) => c instanceof z.ZodObject),
data
);
if (success) {
resultData = result;
}
return success;
}
})
.transform(() => {
// return the parsed data
return resultData;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent mutating external state within Zod schemas

The use of an external variable resultData that's modified inside the .custom() method and then accessed in .transform() can lead to unexpected behaviors, especially in concurrent scenarios. Modifying external state inside schema parsing functions may cause race conditions or data inconsistency.

Consider refactoring to eliminate external state mutation by directly returning the parsed data in the .transform() method.

Here's a suggested refactor:

 export function smartUnion(z: typeof Z, candidates: Z.ZodSchema[]) {
     // ... [code omitted for brevity]
 
-    let resultData: any;
 
     return z
         .custom((data) => {
-            if (Array.isArray(data)) {
-                const { data: result, success } = smartArrayUnion(
+            let parseResult;
+            if (Array.isArray(data)) {
+                const result = smartArrayUnion(
                     z,
                     processedCandidates.filter((c) => c instanceof z.ZodArray),
                     data
                 );
-                if (success) {
-                    resultData = result;
+                if (result.success) {
+                    parseResult = result.data;
                     return true;
                 }
                 return false;
             } else {
-                const { data: result, success } = smartObjectUnion(
+                const result = smartObjectUnion(
                     z,
                     processedCandidates.filter((c) => c instanceof z.ZodObject),
                     data
                 );
-                if (success) {
-                    resultData = result;
+                if (result.success) {
+                    parseResult = result.data;
                     return true;
                 }
                 return false;
             }
         })
         .transform(() => {
-            // return the parsed data
-            return resultData;
+            return parseResult;
         });
 }
📝 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
let resultData: any;
return z
.custom((data) => {
if (Array.isArray(data)) {
const { data: result, success } = smartArrayUnion(
z,
processedCandidates.filter((c) => c instanceof z.ZodArray),
data
);
if (success) {
resultData = result;
}
return success;
} else {
const { data: result, success } = smartObjectUnion(
z,
processedCandidates.filter((c) => c instanceof z.ZodObject),
data
);
if (success) {
resultData = result;
}
return success;
}
})
.transform(() => {
// return the parsed data
return resultData;
});
return z
.custom((data) => {
let parseResult;
if (Array.isArray(data)) {
const result = smartArrayUnion(
z,
processedCandidates.filter((c) => c instanceof z.ZodArray),
data
);
if (result.success) {
parseResult = result.data;
return true;
}
return false;
} else {
const result = smartObjectUnion(
z,
processedCandidates.filter((c) => c instanceof z.ZodObject),
data
);
if (result.success) {
parseResult = result.data;
return true;
}
return false;
}
})
.transform(() => {
return parseResult;
});

unknown: 'unknown',
},
});
expect(parsed.success).toBe(false);
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

Verify the error details when parsing fails.

Since parsed.success is expected to be false, consider asserting the specific error messages to ensure that the failure is due to mixing checked and unchecked fields as intended.

{ userId: '1', content: 'Content2', unknown: 'unknown2' },
],
});
expect(parsed.success).toBe(false);
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

Check error details for mixed input parsing failure.

When parsing fails for mixed inputs, adding assertions for the error messages will help confirm that the schema correctly identifies the inconsistency between checked and unchecked fields.

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