Skip to content

Commit be8c1c4

Browse files
authored
merge dev to main (v2.6.2) (#1752)
2 parents 6f30022 + 46d6a63 commit be8c1c4

File tree

26 files changed

+539
-116
lines changed

26 files changed

+539
-116
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "2.6.1",
3+
"version": "2.6.2",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/ide/jetbrains/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
}
1010

1111
group = "dev.zenstack"
12-
version = "2.6.1"
12+
version = "2.6.2"
1313

1414
repositories {
1515
mavenCentral()

packages/ide/jetbrains/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jetbrains",
3-
"version": "2.6.1",
3+
"version": "2.6.2",
44
"displayName": "ZenStack JetBrains IDE Plugin",
55
"description": "ZenStack JetBrains IDE plugin",
66
"homepage": "https://zenstack.dev",

packages/language/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "2.6.1",
3+
"version": "2.6.2",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/misc/redwood/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/redwood",
33
"displayName": "ZenStack RedwoodJS Integration",
4-
"version": "2.6.1",
4+
"version": "2.6.2",
55
"description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.",
66
"repository": {
77
"type": "git",

packages/plugins/openapi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/openapi",
33
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
4-
"version": "2.6.1",
4+
"version": "2.6.2",
55
"description": "ZenStack plugin and runtime supporting OpenAPI",
66
"main": "index.js",
77
"repository": {

packages/plugins/swr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/swr",
33
"displayName": "ZenStack plugin for generating SWR hooks",
4-
"version": "2.6.1",
4+
"version": "2.6.2",
55
"description": "ZenStack plugin for generating SWR hooks",
66
"main": "index.js",
77
"repository": {

packages/plugins/tanstack-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/tanstack-query",
33
"displayName": "ZenStack plugin for generating tanstack-query hooks",
4-
"version": "2.6.1",
4+
"version": "2.6.2",
55
"description": "ZenStack plugin for generating tanstack-query hooks",
66
"main": "index.js",
77
"exports": {

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "2.6.1",
4+
"version": "2.6.2",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/runtime",
33
"displayName": "ZenStack Runtime Library",
4-
"version": "2.6.1",
4+
"version": "2.6.2",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",
@@ -76,6 +76,10 @@
7676
"./models": {
7777
"types": "./models.d.ts"
7878
},
79+
"./zod-utils": {
80+
"types": "./zod-utils.d.ts",
81+
"default": "./zod-utils.js"
82+
},
7983
"./package.json": {
8084
"default": "./package.json"
8185
}
@@ -107,7 +111,7 @@
107111
"zod-validation-error": "^1.5.0"
108112
},
109113
"peerDependencies": {
110-
"@prisma/client": "5.0.0 - 5.19.x"
114+
"@prisma/client": "5.0.0 - 5.20.x"
111115
},
112116
"author": {
113117
"name": "ZenStack Team"

packages/runtime/src/zod-utils.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { z as Z } from 'zod';
3+
4+
/**
5+
* A smarter version of `z.union` that decide which candidate to use based on how few unrecognized keys it has.
6+
*
7+
* The helper is used to deal with ambiguity in union generated for Prisma inputs when the zod schemas are configured
8+
* to run in "strip" object parsing mode. Since "strip" automatically drops unrecognized keys, it may result in
9+
* accidentally matching a less-ideal schema candidate.
10+
*
11+
* The helper uses a custom schema to find the candidate that results in the fewest unrecognized keys when parsing the data.
12+
*/
13+
export function smartUnion(z: typeof Z, candidates: Z.ZodSchema[]) {
14+
// strip `z.lazy`
15+
const processedCandidates = candidates.map((candidate) => unwrapLazy(z, candidate));
16+
17+
if (processedCandidates.some((c) => !(c instanceof z.ZodObject || c instanceof z.ZodArray))) {
18+
// fall back to plain union if not all candidates are objects or arrays
19+
return z.union(candidates as any);
20+
}
21+
22+
let resultData: any;
23+
24+
return z
25+
.custom((data) => {
26+
if (Array.isArray(data)) {
27+
const { data: result, success } = smartArrayUnion(
28+
z,
29+
processedCandidates.filter((c) => c instanceof z.ZodArray),
30+
data
31+
);
32+
if (success) {
33+
resultData = result;
34+
}
35+
return success;
36+
} else {
37+
const { data: result, success } = smartObjectUnion(
38+
z,
39+
processedCandidates.filter((c) => c instanceof z.ZodObject),
40+
data
41+
);
42+
if (success) {
43+
resultData = result;
44+
}
45+
return success;
46+
}
47+
})
48+
.transform(() => {
49+
// return the parsed data
50+
return resultData;
51+
});
52+
}
53+
54+
function smartArrayUnion(z: typeof Z, candidates: Array<Z.ZodArray<Z.ZodObject<Z.ZodRawShape>>>, data: any) {
55+
if (candidates.length === 0) {
56+
return { data: undefined, success: false };
57+
}
58+
59+
if (!Array.isArray(data)) {
60+
return { data: undefined, success: false };
61+
}
62+
63+
if (data.length === 0) {
64+
return { data, success: true };
65+
}
66+
67+
// use the first element to identify the candidate schema to use
68+
const item = data[0];
69+
const itemSchema = identifyCandidate(
70+
z,
71+
candidates.map((candidate) => candidate.element),
72+
item
73+
);
74+
75+
// find the matching schema and re-parse the data
76+
const schema = candidates.find((candidate) => candidate.element === itemSchema);
77+
return schema!.safeParse(data);
78+
}
79+
80+
function smartObjectUnion(z: typeof Z, candidates: Z.ZodObject<Z.ZodRawShape>[], data: any) {
81+
if (candidates.length === 0) {
82+
return { data: undefined, success: false };
83+
}
84+
const schema = identifyCandidate(z, candidates, data);
85+
return schema.safeParse(data);
86+
}
87+
88+
function identifyCandidate(
89+
z: typeof Z,
90+
candidates: Array<Z.ZodObject<Z.ZodRawShape> | Z.ZodLazy<Z.ZodObject<Z.ZodRawShape>>>,
91+
data: any
92+
) {
93+
const strictResults = candidates.map((candidate) => {
94+
// make sure to strip `z.lazy` before parsing
95+
const unwrapped = unwrapLazy(z, candidate);
96+
return {
97+
schema: candidate,
98+
// force object schema to run in strict mode to capture unrecognized keys
99+
result: unwrapped.strict().safeParse(data),
100+
};
101+
});
102+
103+
// find the schema with the fewest unrecognized keys
104+
const { schema } = strictResults.sort((a, b) => {
105+
const aCount = countUnrecognizedKeys(a.result.error?.issues ?? []);
106+
const bCount = countUnrecognizedKeys(b.result.error?.issues ?? []);
107+
return aCount - bCount;
108+
})[0];
109+
return schema;
110+
}
111+
112+
function countUnrecognizedKeys(issues: Z.ZodIssue[]) {
113+
return issues
114+
.filter((issue) => issue.code === 'unrecognized_keys')
115+
.map((issue) => issue.keys.length)
116+
.reduce((a, b) => a + b, 0);
117+
}
118+
119+
function unwrapLazy<T extends Z.ZodSchema>(z: typeof Z, schema: T | Z.ZodLazy<T>): T {
120+
return schema instanceof z.ZodLazy ? schema.schema : schema;
121+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { z } from 'zod';
2+
import { smartUnion } from '../../src/zod-utils';
3+
4+
describe('Zod smart union', () => {
5+
it('should work with scalar union', () => {
6+
const schema = smartUnion(z, [z.string(), z.number()]);
7+
expect(schema.safeParse('test')).toMatchObject({ success: true, data: 'test' });
8+
expect(schema.safeParse(1)).toMatchObject({ success: true, data: 1 });
9+
expect(schema.safeParse(true)).toMatchObject({ success: false });
10+
});
11+
12+
it('should work with non-ambiguous object union', () => {
13+
const schema = smartUnion(z, [z.object({ a: z.string() }), z.object({ b: z.number() }).strict()]);
14+
expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } });
15+
expect(schema.safeParse({ b: 1 })).toMatchObject({ success: true, data: { b: 1 } });
16+
expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true });
17+
expect(schema.safeParse({ b: 1, c: 'test' })).toMatchObject({ success: false });
18+
expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false });
19+
});
20+
21+
it('should work with ambiguous object union', () => {
22+
const schema = smartUnion(z, [
23+
z.object({ a: z.string(), b: z.number() }),
24+
z.object({ a: z.string(), c: z.boolean() }),
25+
]);
26+
expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } });
27+
expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } });
28+
expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({
29+
success: true,
30+
data: { a: 'test', b: 1 },
31+
});
32+
expect(schema.safeParse({ a: 'test', c: true, z: 'z' })).toMatchObject({
33+
success: true,
34+
data: { a: 'test', c: true },
35+
});
36+
expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false });
37+
});
38+
39+
it('should work with non-ambiguous array union', () => {
40+
const schema = smartUnion(z, [
41+
z.object({ a: z.string() }).array(),
42+
z.object({ b: z.number() }).strict().array(),
43+
]);
44+
45+
expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: true, data: [{ a: 'test' }] });
46+
expect(schema.safeParse([{ a: 'test' }, { a: 'test1' }])).toMatchObject({
47+
success: true,
48+
data: [{ a: 'test' }, { a: 'test1' }],
49+
});
50+
51+
expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] });
52+
expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true });
53+
expect(schema.safeParse([{ b: 1, c: 'test' }])).toMatchObject({ success: false });
54+
expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false });
55+
56+
// all items must match the same candidate
57+
expect(schema.safeParse([{ a: 'test' }, { b: 1 }])).toMatchObject({ success: false });
58+
});
59+
60+
it('should work with ambiguous array union', () => {
61+
const schema = smartUnion(z, [
62+
z.object({ a: z.string(), b: z.number() }).array(),
63+
z.object({ a: z.string(), c: z.boolean() }).array(),
64+
]);
65+
66+
expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true, data: [{ a: 'test', b: 1 }] });
67+
expect(schema.safeParse([{ a: 'test', c: true }])).toMatchObject({
68+
success: true,
69+
data: [{ a: 'test', c: true }],
70+
});
71+
expect(schema.safeParse([{ a: 'test', b: 1, z: 'z' }])).toMatchObject({
72+
success: true,
73+
data: [{ a: 'test', b: 1 }],
74+
});
75+
expect(schema.safeParse([{ a: 'test', c: true, z: 'z' }])).toMatchObject({
76+
success: true,
77+
data: [{ a: 'test', c: true }],
78+
});
79+
expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false });
80+
81+
// all items must match the same candidate
82+
expect(schema.safeParse([{ a: 'test' }, { c: true }])).toMatchObject({ success: false });
83+
});
84+
85+
it('should work with lazy schemas', () => {
86+
const schema = smartUnion(z, [
87+
z.lazy(() => z.object({ a: z.string(), b: z.number() })),
88+
z.lazy(() => z.object({ a: z.string(), c: z.boolean() })),
89+
]);
90+
expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } });
91+
expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } });
92+
expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({
93+
success: true,
94+
data: { a: 'test', b: 1 },
95+
});
96+
});
97+
98+
it('should work with mixed object and array unions', () => {
99+
const schema = smartUnion(z, [
100+
z.object({ a: z.string() }).strict(),
101+
z.object({ b: z.number() }).strict().array(),
102+
]);
103+
104+
expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } });
105+
expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] });
106+
expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: false });
107+
expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: false });
108+
});
109+
});

packages/schema/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack Language Tools",
55
"description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI",
6-
"version": "2.6.1",
6+
"version": "2.6.2",
77
"author": {
88
"name": "ZenStack Team"
99
},
@@ -123,10 +123,10 @@
123123
"zod-validation-error": "^1.5.0"
124124
},
125125
"peerDependencies": {
126-
"prisma": "5.0.0 - 5.19.x"
126+
"prisma": "5.0.0 - 5.20.x"
127127
},
128128
"devDependencies": {
129-
"@prisma/client": "5.19.x",
129+
"@prisma/client": "5.20.x",
130130
"@types/async-exit-hook": "^2.0.0",
131131
"@types/pluralize": "^0.0.29",
132132
"@types/semver": "^7.3.13",

0 commit comments

Comments
 (0)