Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/orm/src/client/zod/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,13 @@ export class ZodSchemaFactory<
fields[field] = this.makeRelationSelectIncludeSchema(model, field, options).optional();
}
} else {
fields[field] = z.boolean().optional();
if (this.options.allowQueryTimeOmitOverride === false && this.isFieldOmittedByConfig(model, field)) {
// when query-time omit override is disallowed, an omitted field cannot be
// un-omitted by explicitly selecting it, so only allow `false`
fields[field] = z.literal(false).optional();
} else {
fields[field] = z.boolean().optional();
}
}
}

Expand Down Expand Up @@ -1261,6 +1267,24 @@ export class ZodSchemaFactory<
return result;
}

/**
* Determines whether a field is configured to be omitted at the schema or client-options level
* (query-level omit is excluded as it's mutually exclusive with `select`).
*/
private isFieldOmittedByConfig(model: string, field: string): boolean {
// options-level omit
const omitConfig =
(this.options.omit as Record<string, any> | undefined)?.[lowerCaseFirst(model)] ??
(this.options.omit as Record<string, any> | undefined)?.[model];
if (omitConfig && typeof omitConfig[field] === 'boolean') {
return omitConfig[field];
}

// schema-level omit
const fieldDef = requireField(this.schema, model, field);
return !!fieldDef.omit;
}

@cache()
private makeOmitSchema(model: string) {
const fields: Record<string, ZodType> = {};
Expand Down
59 changes: 59 additions & 0 deletions tests/regression/test/issue-2671.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createTestClient } from '@zenstackhq/testtools';
import { describe, expect, it } from 'vitest';

// https://github.com/zenstackhq/zenstack/issues/2671
describe('Regression for issue 2671', () => {
const schema = `
model User {
id String @id @default(cuid())
email String @unique
passwordHash String @omit
}
`;

it('allows selecting omitted fields when override is allowed (default)', async () => {
const db = await createTestClient(schema);

await db.user.create({
data: { email: 'user1@test.com', passwordHash: 'secret-hash' },
});

// by default query-time override is allowed, so explicit select returns the field
const selected = await db.user.findFirst({
where: { email: 'user1@test.com' },
select: { passwordHash: true },
});
expect(selected?.passwordHash).toBe('secret-hash');
});

it('forbids selecting omitted fields when allowQueryTimeOmitOverride is false', async () => {
const base = await createTestClient(schema);
const db = base.$setOptions({ ...base.$options, allowQueryTimeOmitOverride: false });

await db.user.create({
data: { email: 'user1@test.com', passwordHash: 'secret-hash' },
});

// explicitly selecting the omitted field must be rejected
await expect(
db.user.findFirst({
where: { email: 'user1@test.com' },
select: { passwordHash: true },
}),
).toBeRejectedByValidation();

// selecting non-omitted fields still works
const ok = await db.user.findFirst({
where: { email: 'user1@test.com' },
select: { email: true },
});
expect(ok).toEqual({ email: 'user1@test.com' });

// explicitly excluding the omitted field via select is fine
const excluded = await db.user.findFirst({
where: { email: 'user1@test.com' },
select: { email: true, passwordHash: false },
});
expect(excluded).toEqual({ email: 'user1@test.com' });
});
});
Loading