Skip to content

Commit f3a9850

Browse files
ymc9claude
andauthored
fix: reject select with only false fields to prevent empty SELECT SQL (#2401)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d49c39e commit f3a9850

File tree

2 files changed

+80
-29
lines changed

2 files changed

+80
-29
lines changed

packages/orm/src/client/zod/factory.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,7 @@ export class ZodSchemaFactory<
216216
} else {
217217
fields['take'] = this.makeTakeSchema().optional();
218218
}
219-
fields['orderBy'] = this.orArray(
220-
this.makeOrderBySchema(model, true, false, options),
221-
true,
222-
).optional();
219+
fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false, options), true).optional();
223220
fields['cursor'] = this.makeCursorSchema(model, options).optional();
224221
fields['distinct'] = this.makeDistinctSchema(model).optional();
225222
}
@@ -228,6 +225,7 @@ export class ZodSchemaFactory<
228225
let result: ZodType = this.mergePluginArgsSchema(baseSchema, operation);
229226
result = this.refineForSelectIncludeMutuallyExclusive(result);
230227
result = this.refineForSelectOmitMutuallyExclusive(result);
228+
result = this.refineForSelectHasTruthyField(result);
231229

232230
if (!unique) {
233231
result = result.optional();
@@ -988,6 +986,7 @@ export class ZodSchemaFactory<
988986

989987
objSchema = this.refineForSelectIncludeMutuallyExclusive(objSchema);
990988
objSchema = this.refineForSelectOmitMutuallyExclusive(objSchema);
989+
objSchema = this.refineForSelectHasTruthyField(objSchema);
991990

992991
return z.union([z.boolean(), objSchema]);
993992
}
@@ -1039,7 +1038,12 @@ export class ZodSchemaFactory<
10391038
}
10401039

10411040
@cache()
1042-
private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean, options?: CreateSchemaOptions) {
1041+
private makeOrderBySchema(
1042+
model: string,
1043+
withRelation: boolean,
1044+
WithAggregation: boolean,
1045+
options?: CreateSchemaOptions,
1046+
) {
10431047
const modelDef = requireModel(this.schema, model);
10441048
const fields: Record<string, ZodType> = {};
10451049
const sort = z.union([z.literal('asc'), z.literal('desc')]);
@@ -1050,7 +1054,12 @@ export class ZodSchemaFactory<
10501054
// relations
10511055
if (withRelation && this.shouldIncludeRelations(options)) {
10521056
fields[field] = z.lazy(() => {
1053-
let relationOrderBy = this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation, nextOpts);
1057+
let relationOrderBy = this.makeOrderBySchema(
1058+
fieldDef.type,
1059+
withRelation,
1060+
WithAggregation,
1061+
nextOpts,
1062+
);
10541063
if (fieldDef.array) {
10551064
relationOrderBy = relationOrderBy.extend({
10561065
_count: sort,
@@ -1119,6 +1128,7 @@ export class ZodSchemaFactory<
11191128
let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create');
11201129
schema = this.refineForSelectIncludeMutuallyExclusive(schema);
11211130
schema = this.refineForSelectOmitMutuallyExclusive(schema);
1131+
schema = this.refineForSelectHasTruthyField(schema);
11221132
return schema as ZodType<CreateArgs<Schema, Model, Options, ExtQueryArgs>>;
11231133
}
11241134

@@ -1144,9 +1154,9 @@ export class ZodSchemaFactory<
11441154
omit: this.makeOmitSchema(model).optional().nullable(),
11451155
});
11461156
result = this.mergePluginArgsSchema(result, 'createManyAndReturn');
1147-
return this.refineForSelectOmitMutuallyExclusive(result).optional() as ZodType<
1148-
CreateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs>
1149-
>;
1157+
return this.refineForSelectHasTruthyField(
1158+
this.refineForSelectOmitMutuallyExclusive(result),
1159+
).optional() as ZodType<CreateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs>>;
11501160
}
11511161

11521162
@cache()
@@ -1315,12 +1325,7 @@ export class ZodSchemaFactory<
13151325

13161326
connect: this.makeConnectDataSchema(fieldType, array, options).optional(),
13171327

1318-
connectOrCreate: this.makeConnectOrCreateDataSchema(
1319-
fieldType,
1320-
array,
1321-
withoutFields,
1322-
options,
1323-
).optional(),
1328+
connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields, options).optional(),
13241329
};
13251330

13261331
if (array) {
@@ -1379,12 +1384,7 @@ export class ZodSchemaFactory<
13791384
true,
13801385
).optional();
13811386

1382-
fields['deleteMany'] = this.makeDeleteRelationDataSchema(
1383-
fieldType,
1384-
true,
1385-
false,
1386-
options,
1387-
).optional();
1387+
fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false, options).optional();
13881388
}
13891389
}
13901390

@@ -1393,18 +1393,12 @@ export class ZodSchemaFactory<
13931393

13941394
@cache()
13951395
private makeSetDataSchema(model: string, canBeArray: boolean, options?: CreateSchemaOptions) {
1396-
return this.orArray(
1397-
this.makeWhereSchema(model, true, false, false, options),
1398-
canBeArray,
1399-
);
1396+
return this.orArray(this.makeWhereSchema(model, true, false, false, options), canBeArray);
14001397
}
14011398

14021399
@cache()
14031400
private makeConnectDataSchema(model: string, canBeArray: boolean, options?: CreateSchemaOptions) {
1404-
return this.orArray(
1405-
this.makeWhereSchema(model, true, false, false, options),
1406-
canBeArray,
1407-
);
1401+
return this.orArray(this.makeWhereSchema(model, true, false, false, options), canBeArray);
14081402
}
14091403

14101404
@cache()
@@ -1476,6 +1470,7 @@ export class ZodSchemaFactory<
14761470
let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update');
14771471
schema = this.refineForSelectIncludeMutuallyExclusive(schema);
14781472
schema = this.refineForSelectOmitMutuallyExclusive(schema);
1473+
schema = this.refineForSelectHasTruthyField(schema);
14791474
return schema as ZodType<UpdateArgs<Schema, Model, Options, ExtQueryArgs>>;
14801475
}
14811476

@@ -1506,6 +1501,7 @@ export class ZodSchemaFactory<
15061501
omit: this.makeOmitSchema(model).optional().nullable(),
15071502
});
15081503
schema = this.refineForSelectOmitMutuallyExclusive(schema);
1504+
schema = this.refineForSelectHasTruthyField(schema);
15091505
return schema as ZodType<UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs>>;
15101506
}
15111507

@@ -1525,6 +1521,7 @@ export class ZodSchemaFactory<
15251521
let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert');
15261522
schema = this.refineForSelectIncludeMutuallyExclusive(schema);
15271523
schema = this.refineForSelectOmitMutuallyExclusive(schema);
1524+
schema = this.refineForSelectHasTruthyField(schema);
15281525
return schema as ZodType<UpsertArgs<Schema, Model, Options, ExtQueryArgs>>;
15291526
}
15301527

@@ -1666,6 +1663,7 @@ export class ZodSchemaFactory<
16661663
let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete');
16671664
schema = this.refineForSelectIncludeMutuallyExclusive(schema);
16681665
schema = this.refineForSelectOmitMutuallyExclusive(schema);
1666+
schema = this.refineForSelectHasTruthyField(schema);
16691667
return schema as ZodType<DeleteArgs<Schema, Model, Options, ExtQueryArgs>>;
16701668
}
16711669

@@ -2030,6 +2028,16 @@ export class ZodSchemaFactory<
20302028
);
20312029
}
20322030

2031+
private refineForSelectHasTruthyField(schema: ZodType) {
2032+
return schema.refine((value: any) => {
2033+
const select = value['select'];
2034+
if (!select || typeof select !== 'object') {
2035+
return true;
2036+
}
2037+
return Object.values(select).some((v) => v);
2038+
}, '"select" must have at least one truthy value');
2039+
}
2040+
20332041
private nullableIf(schema: ZodType, nullable: boolean) {
20342042
return nullable ? schema.nullable() : schema;
20352043
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
// https://github.com/zenstackhq/zenstack/issues/2344
5+
describe('Regression for issue 2344', () => {
6+
it('should reject select with only false fields', async () => {
7+
const db = await createTestClient(
8+
`
9+
model User {
10+
id String @id @default(cuid())
11+
email String @unique
12+
name String?
13+
}
14+
`,
15+
);
16+
17+
await db.user.create({
18+
data: { email: 'user1@test.com', name: 'User1' },
19+
});
20+
21+
// select with only false fields should be rejected by validation
22+
await expect(
23+
db.user.findMany({
24+
select: { id: false },
25+
}),
26+
).rejects.toThrow(/"select" must have at least one truthy value/);
27+
28+
// select with all fields false should also be rejected
29+
await expect(
30+
db.user.findFirst({
31+
select: { id: false, email: false, name: false },
32+
}),
33+
).rejects.toThrow(/"select" must have at least one truthy value/);
34+
35+
// mix of true and false should still work
36+
const r = await db.user.findFirst({
37+
select: { id: false, email: true },
38+
});
39+
expect(r).toBeTruthy();
40+
expect('id' in r!).toBeFalsy();
41+
expect(r!.email).toBe('user1@test.com');
42+
});
43+
});

0 commit comments

Comments
 (0)