-
-
Notifications
You must be signed in to change notification settings - Fork 128
merge dev to main (v2.6.2) #1752
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
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ plugins { | |
| } | ||
|
|
||
| group = "dev.zenstack" | ||
| version = "2.6.1" | ||
| version = "2.6.2" | ||
|
|
||
| repositories { | ||
| mavenCentral() | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import { z as Z } from 'zod'; | ||
|
|
||
| /** | ||
| * A smarter version of `z.union` that decide which candidate to use based on how few unrecognized keys it has. | ||
| * | ||
| * The helper is used to deal with ambiguity in union generated for Prisma inputs when the zod schemas are configured | ||
| * to run in "strip" object parsing mode. Since "strip" automatically drops unrecognized keys, it may result in | ||
| * accidentally matching a less-ideal schema candidate. | ||
| * | ||
| * The helper uses a custom schema to find the candidate that results in the fewest unrecognized keys when parsing the data. | ||
| */ | ||
| export function smartUnion(z: typeof Z, candidates: Z.ZodSchema[]) { | ||
| // strip `z.lazy` | ||
| const processedCandidates = candidates.map((candidate) => unwrapLazy(z, candidate)); | ||
|
|
||
| if (processedCandidates.some((c) => !(c instanceof z.ZodObject || c instanceof z.ZodArray))) { | ||
| // fall back to plain union if not all candidates are objects or arrays | ||
| return z.union(candidates as any); | ||
| } | ||
|
|
||
| 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; | ||
| }); | ||
| } | ||
|
|
||
| function smartArrayUnion(z: typeof Z, candidates: Array<Z.ZodArray<Z.ZodObject<Z.ZodRawShape>>>, data: any) { | ||
| if (candidates.length === 0) { | ||
| return { data: undefined, success: false }; | ||
| } | ||
|
|
||
| if (!Array.isArray(data)) { | ||
| return { data: undefined, success: false }; | ||
| } | ||
|
|
||
| if (data.length === 0) { | ||
| return { data, success: true }; | ||
| } | ||
|
|
||
| // use the first element to identify the candidate schema to use | ||
| const item = data[0]; | ||
| const itemSchema = identifyCandidate( | ||
| z, | ||
| candidates.map((candidate) => candidate.element), | ||
| item | ||
| ); | ||
|
|
||
| // find the matching schema and re-parse the data | ||
| const schema = candidates.find((candidate) => candidate.element === itemSchema); | ||
| return schema!.safeParse(data); | ||
ymc9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| function smartObjectUnion(z: typeof Z, candidates: Z.ZodObject<Z.ZodRawShape>[], data: any) { | ||
| if (candidates.length === 0) { | ||
| return { data: undefined, success: false }; | ||
| } | ||
| const schema = identifyCandidate(z, candidates, data); | ||
| return schema.safeParse(data); | ||
| } | ||
|
|
||
| function identifyCandidate( | ||
| z: typeof Z, | ||
| candidates: Array<Z.ZodObject<Z.ZodRawShape> | Z.ZodLazy<Z.ZodObject<Z.ZodRawShape>>>, | ||
| data: any | ||
| ) { | ||
| 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; | ||
| } | ||
|
|
||
| function countUnrecognizedKeys(issues: Z.ZodIssue[]) { | ||
| return issues | ||
| .filter((issue) => issue.code === 'unrecognized_keys') | ||
| .map((issue) => issue.keys.length) | ||
| .reduce((a, b) => a + b, 0); | ||
| } | ||
|
|
||
| function unwrapLazy<T extends Z.ZodSchema>(z: typeof Z, schema: T | Z.ZodLazy<T>): T { | ||
| return schema instanceof z.ZodLazy ? schema.schema : schema; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { z } from 'zod'; | ||
| import { smartUnion } from '../../src/zod-utils'; | ||
|
|
||
| describe('Zod smart union', () => { | ||
| it('should work with scalar union', () => { | ||
| const schema = smartUnion(z, [z.string(), z.number()]); | ||
| expect(schema.safeParse('test')).toMatchObject({ success: true, data: 'test' }); | ||
| expect(schema.safeParse(1)).toMatchObject({ success: true, data: 1 }); | ||
| expect(schema.safeParse(true)).toMatchObject({ success: false }); | ||
| }); | ||
|
|
||
| it('should work with non-ambiguous object union', () => { | ||
| const schema = smartUnion(z, [z.object({ a: z.string() }), z.object({ b: z.number() }).strict()]); | ||
| expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } }); | ||
| expect(schema.safeParse({ b: 1 })).toMatchObject({ success: true, data: { b: 1 } }); | ||
| expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true }); | ||
| expect(schema.safeParse({ b: 1, c: 'test' })).toMatchObject({ success: false }); | ||
| expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false }); | ||
| }); | ||
|
|
||
| it('should work with ambiguous object union', () => { | ||
| const schema = smartUnion(z, [ | ||
| z.object({ a: z.string(), b: z.number() }), | ||
| z.object({ a: z.string(), c: z.boolean() }), | ||
| ]); | ||
| expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } }); | ||
| expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } }); | ||
| expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({ | ||
| success: true, | ||
| data: { a: 'test', b: 1 }, | ||
| }); | ||
| expect(schema.safeParse({ a: 'test', c: true, z: 'z' })).toMatchObject({ | ||
| success: true, | ||
| data: { a: 'test', c: true }, | ||
| }); | ||
| expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false }); | ||
| }); | ||
|
|
||
| it('should work with non-ambiguous array union', () => { | ||
| const schema = smartUnion(z, [ | ||
| z.object({ a: z.string() }).array(), | ||
| z.object({ b: z.number() }).strict().array(), | ||
| ]); | ||
|
|
||
| expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: true, data: [{ a: 'test' }] }); | ||
| expect(schema.safeParse([{ a: 'test' }, { a: 'test1' }])).toMatchObject({ | ||
| success: true, | ||
| data: [{ a: 'test' }, { a: 'test1' }], | ||
| }); | ||
|
|
||
| expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] }); | ||
| expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true }); | ||
| expect(schema.safeParse([{ b: 1, c: 'test' }])).toMatchObject({ success: false }); | ||
| expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false }); | ||
|
|
||
| // all items must match the same candidate | ||
| expect(schema.safeParse([{ a: 'test' }, { b: 1 }])).toMatchObject({ success: false }); | ||
| }); | ||
|
|
||
| it('should work with ambiguous array union', () => { | ||
| const schema = smartUnion(z, [ | ||
| z.object({ a: z.string(), b: z.number() }).array(), | ||
| z.object({ a: z.string(), c: z.boolean() }).array(), | ||
| ]); | ||
|
|
||
| expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true, data: [{ a: 'test', b: 1 }] }); | ||
| expect(schema.safeParse([{ a: 'test', c: true }])).toMatchObject({ | ||
| success: true, | ||
| data: [{ a: 'test', c: true }], | ||
| }); | ||
| expect(schema.safeParse([{ a: 'test', b: 1, z: 'z' }])).toMatchObject({ | ||
| success: true, | ||
| data: [{ a: 'test', b: 1 }], | ||
| }); | ||
| expect(schema.safeParse([{ a: 'test', c: true, z: 'z' }])).toMatchObject({ | ||
| success: true, | ||
| data: [{ a: 'test', c: true }], | ||
| }); | ||
| expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false }); | ||
|
|
||
| // all items must match the same candidate | ||
| expect(schema.safeParse([{ a: 'test' }, { c: true }])).toMatchObject({ success: false }); | ||
| }); | ||
|
|
||
| it('should work with lazy schemas', () => { | ||
| const schema = smartUnion(z, [ | ||
| z.lazy(() => z.object({ a: z.string(), b: z.number() })), | ||
| z.lazy(() => z.object({ a: z.string(), c: z.boolean() })), | ||
| ]); | ||
| expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } }); | ||
| expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } }); | ||
| expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({ | ||
| success: true, | ||
| data: { a: 'test', b: 1 }, | ||
| }); | ||
| }); | ||
|
|
||
| it('should work with mixed object and array unions', () => { | ||
| const schema = smartUnion(z, [ | ||
| z.object({ a: z.string() }).strict(), | ||
| z.object({ b: z.number() }).strict().array(), | ||
| ]); | ||
|
|
||
| expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } }); | ||
| expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] }); | ||
| expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: false }); | ||
| expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: false }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.