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
24 changes: 17 additions & 7 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,11 +1431,22 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
query: SelectQueryBuilder<any, any, any>,
omit: Record<string, boolean | undefined> | undefined | null,
modelAlias: string,
options?: {
/**
* When false, query-level, client-level, and schema-level omit settings are all
* ignored for the model's own fields and every field is selected. Omission is
* still applied to the JSON packed from delegate descendants, since those fields
* are never referenced by joins or ordering and the packed JSON is copied to
* the final result as-is.
*/
applyOmit?: boolean;
},
) {
let result = query;
const applyOmit = options?.applyOmit ?? true;

for (const fieldDef of getModelFields(this.schema, model, { inherited: true, computed: true })) {
if (this.shouldOmitField(omit, model, fieldDef.name)) {
if (applyOmit && this.shouldOmitField(omit, model, fieldDef.name)) {
continue;
}
// parameterized computed fields can't be auto-selected — they require
Expand Down Expand Up @@ -1498,12 +1509,11 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
let subQuery = this.buildSelectModel(model, subQueryAlias);

if (selectAllFields) {
subQuery = this.buildSelectAllFields(
model,
subQuery,
typeof payload === 'object' ? payload?.omit : undefined,
subQueryAlias,
);
// omission (query-level, client-level, or schema-level) is intentionally not
// applied here: this select feeds a derived subquery whose columns are needed by
// nested relation joins (PK/FK fields) and by ordering of the aggregated result,
// and the outer JSON object projection handles omission on its own
subQuery = this.buildSelectAllFields(model, subQuery, undefined, subQueryAlias, { applyOmit: false });
}

if (payload && typeof payload === 'object') {
Expand Down
157 changes: 157 additions & 0 deletions tests/regression/test/issue-2830.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { createTestClient } from '@zenstackhq/testtools';
import { describe, expect, it } from 'vitest';

// https://github.com/zenstackhq/zenstack/issues/2830
describe('Regression for issue #2830', () => {
const schema = `
model Parent {
id Int @id @default(autoincrement())
name String
children Child[]
}

model Child {
id Int @id @default(autoincrement())
position Int
parent Parent @relation(fields: [parentId], references: [id])
parentId Int
grandchildren Grandchild[]
}

model Grandchild {
id Int @id @default(autoincrement())
name String
child Child @relation(fields: [childId], references: [id])
childId Int
}
`;

async function seed(db: any) {
await db.parent.create({
data: {
name: 'p1',
children: {
create: [
{ position: 2, grandchildren: { create: [{ name: 'g2' }] } },
{ position: 1, grandchildren: { create: [{ name: 'g1a' }, { name: 'g1b' }] } },
],
},
},
});
}

it('keeps nested includes working when the PK is omitted on an ordered relation', async () => {
const db = await createTestClient(schema);
await seed(db);

const result = await db.parent.findMany({
include: {
children: {
omit: { id: true },
orderBy: { position: 'asc' },
include: { grandchildren: true },
},
},
});

expect(result).toHaveLength(1);
const children = result[0]!.children;
expect(children.map((c: any) => c.position)).toEqual([1, 2]);
expect(children[0]).not.toHaveProperty('id');
expect(children[0]!.grandchildren.map((g: any) => g.name).sort()).toEqual(['g1a', 'g1b']);
expect(children[1]!.grandchildren.map((g: any) => g.name)).toEqual(['g2']);
// grandchildren are not affected by the parent-level omit
expect(children[0]!.grandchildren[0]).toHaveProperty('id');
});

it('keeps nested includes working when the FK is omitted on a paginated relation', async () => {
const db = await createTestClient(schema);
await seed(db);

const result = await db.parent.findMany({
include: {
children: {
omit: { id: true, parentId: true },
orderBy: { position: 'desc' },
take: 1,
include: { grandchildren: { omit: { childId: true } } },
},
},
});

expect(result[0]!.children).toHaveLength(1);
expect(result[0]!.children[0]!.position).toBe(2);
expect(result[0]!.children[0]).not.toHaveProperty('id');
expect(result[0]!.children[0]).not.toHaveProperty('parentId');
expect(result[0]!.children[0]!.grandchildren).toEqual([expect.objectContaining({ name: 'g2' })]);
expect(result[0]!.children[0]!.grandchildren[0]).not.toHaveProperty('childId');
});

it('respects schema-level @omit on an ordered relation with nested include', async () => {
const db = await createTestClient(schema.replace('position Int', 'position Int @omit'));
await seed(db);

const result = await db.parent.findMany({
include: {
children: {
orderBy: { position: 'asc' },
include: { grandchildren: true },
},
},
});
const children = result[0]!.children;
expect(children).toHaveLength(2);
expect(children[0]).not.toHaveProperty('position');
expect(children[0]!.grandchildren).toHaveLength(2);
expect(children[1]!.grandchildren).toHaveLength(1);
});

it('still omits delegate-descendant fields on an ordered relation', async () => {
const db = await createTestClient(
`
model Parent {
id Int @id @default(autoincrement())
name String
items Item[]
}

model Item {
id Int @id @default(autoincrement())
position Int
kind String
parent Parent @relation(fields: [parentId], references: [id])
parentId Int
@@delegate(kind)
}

model SecretItem extends Item {
secret String @omit
public String
}
`,
);
const parent = await db.parent.create({ data: { name: 'p1' } });
await db.secretItem.create({
data: { parentId: parent.id, position: 2, secret: 's2', public: 'pub2' },
});
await db.secretItem.create({
data: { parentId: parent.id, position: 1, secret: 's1', public: 'pub1' },
});

const result = await db.parent.findMany({
include: {
items: {
omit: { id: true },
orderBy: { position: 'asc' },
},
},
});

const items = result[0]!.items;
expect(items.map((i: any) => i.position)).toEqual([1, 2]);
expect(items[0]).toMatchObject({ kind: 'SecretItem', public: 'pub1' });
// schema-level @omit on the delegate descendant must still be honored
expect(items[0]).not.toHaveProperty('secret');
expect(items[1]).not.toHaveProperty('secret');
});
});
Loading