Skip to content

Commit 99a9835

Browse files
committed
fix(delegate): support _count select of base fields
1 parent 2eecae5 commit 99a9835

File tree

2 files changed

+135
-39
lines changed

2 files changed

+135
-39
lines changed

packages/runtime/src/enhancements/node/delegate.ts

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -180,47 +180,82 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
180180
return;
181181
}
182182

183-
for (const kind of ['select', 'include'] as const) {
184-
if (args[kind] && typeof args[kind] === 'object') {
185-
for (const [field, value] of Object.entries<any>(args[kind])) {
186-
const fieldInfo = resolveField(this.options.modelMeta, model, field);
187-
if (!fieldInfo) {
188-
continue;
189-
}
183+
const selectors = [
184+
(payload: any) => ({ data: payload.select, kind: 'select' as const, isCount: false }),
185+
(payload: any) => ({ data: payload.include, kind: 'include' as const, isCount: false }),
186+
(payload: any) => ({
187+
data: payload.select?._count?.select,
188+
kind: 'select' as const,
189+
isCount: true,
190+
}),
191+
(payload: any) => ({
192+
data: payload.include?._count?.select,
193+
kind: 'include' as const,
194+
isCount: true,
195+
}),
196+
];
197+
198+
for (const selector of selectors) {
199+
const { data, kind, isCount } = selector(args);
200+
if (!data || typeof data !== 'object') {
201+
continue;
202+
}
190203

191-
if (this.isDelegateOrDescendantOfDelegate(fieldInfo?.type) && value) {
192-
// delegate model, recursively inject hierarchy
193-
if (args[kind][field]) {
194-
if (args[kind][field] === true) {
195-
// make sure the payload is an object
196-
args[kind][field] = {};
197-
}
198-
await this.injectSelectIncludeHierarchy(fieldInfo.type, args[kind][field]);
204+
for (const [field, value] of Object.entries<any>(data)) {
205+
const fieldInfo = resolveField(this.options.modelMeta, model, field);
206+
if (!fieldInfo) {
207+
continue;
208+
}
209+
210+
if (this.isDelegateOrDescendantOfDelegate(fieldInfo?.type) && value) {
211+
// delegate model, recursively inject hierarchy
212+
if (data[field]) {
213+
if (data[field] === true) {
214+
// make sure the payload is an object
215+
data[field] = {};
199216
}
217+
await this.injectSelectIncludeHierarchy(fieldInfo.type, data[field]);
200218
}
219+
}
201220

202-
// refetch the field select/include value because it may have been
203-
// updated during injection
204-
const fieldValue = args[kind][field];
221+
// refetch the field select/include value because it may have been
222+
// updated during injection
223+
const fieldValue = data[field];
205224

206-
if (fieldValue !== undefined) {
207-
if (fieldValue.orderBy) {
208-
// `orderBy` may contain fields from base types
209-
enumerate(fieldValue.orderBy).forEach((item) =>
210-
this.injectWhereHierarchy(fieldInfo.type, item)
211-
);
212-
}
225+
if (fieldValue !== undefined) {
226+
if (fieldValue.orderBy) {
227+
// `orderBy` may contain fields from base types
228+
enumerate(fieldValue.orderBy).forEach((item) =>
229+
this.injectWhereHierarchy(fieldInfo.type, item)
230+
);
231+
}
213232

214-
if (this.injectBaseFieldSelect(model, field, fieldValue, args, kind)) {
215-
delete args[kind][field];
216-
} else if (fieldInfo.isDataModel) {
217-
let nextValue = fieldValue;
218-
if (nextValue === true) {
219-
// make sure the payload is an object
220-
args[kind][field] = nextValue = {};
233+
let injected = false;
234+
if (!isCount) {
235+
injected = await this.injectBaseFieldSelect(model, field, fieldValue, args, kind);
236+
if (injected) {
237+
delete data[field];
238+
}
239+
} else {
240+
const injectTarget = { [kind]: {} };
241+
injected = await this.injectBaseFieldSelect(model, field, fieldValue, injectTarget, kind, true);
242+
if (injected) {
243+
delete data[field];
244+
if (Object.keys(data).length === 0) {
245+
delete args[kind]['_count'];
221246
}
222-
await this.injectSelectIncludeHierarchy(fieldInfo.type, nextValue);
247+
const merged = deepmerge(args[kind], injectTarget[kind]);
248+
args[kind] = merged;
249+
}
250+
}
251+
252+
if (!injected && fieldInfo.isDataModel) {
253+
let nextValue = fieldValue;
254+
if (nextValue === true) {
255+
// make sure the payload is an object
256+
data[field] = nextValue = {};
223257
}
258+
await this.injectSelectIncludeHierarchy(fieldInfo.type, nextValue);
224259
}
225260
}
226261
}
@@ -272,7 +307,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
272307
field: string,
273308
value: any,
274309
selectInclude: any,
275-
context: 'select' | 'include'
310+
context: 'select' | 'include',
311+
forCount = false
276312
) {
277313
const fieldInfo = resolveField(this.options.modelMeta, model, field);
278314
if (!fieldInfo?.inheritedFrom) {
@@ -286,24 +322,33 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
286322
const baseRelationName = this.makeAuxRelationName(base);
287323

288324
// prepare base layer select/include
289-
// let selectOrInclude = 'select';
290325
let thisLayer: any;
291326
if (target.include) {
292-
// selectOrInclude = 'include';
293327
thisLayer = target.include;
294328
} else if (target.select) {
295-
// selectOrInclude = 'select';
296329
thisLayer = target.select;
297330
} else {
298-
// selectInclude = 'include';
299331
thisLayer = target.select = {};
300332
}
301333

302334
if (base.name === fieldInfo.inheritedFrom) {
303335
if (!thisLayer[baseRelationName]) {
304336
thisLayer[baseRelationName] = { [context]: {} };
305337
}
306-
thisLayer[baseRelationName][context][field] = value;
338+
if (forCount) {
339+
if (
340+
!thisLayer[baseRelationName][context]['_count'] ||
341+
typeof thisLayer[baseRelationName][context] !== 'object'
342+
) {
343+
thisLayer[baseRelationName][context]['_count'] = {};
344+
}
345+
thisLayer[baseRelationName][context]['_count'] = deepmerge(
346+
thisLayer[baseRelationName][context]['_count'],
347+
{ select: { [field]: value } }
348+
);
349+
} else {
350+
thisLayer[baseRelationName][context][field] = value;
351+
}
307352
break;
308353
} else {
309354
if (!thisLayer[baseRelationName]) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('issue 1467', () => {
4+
it('regression', async () => {
5+
const { enhance } = await loadSchema(
6+
`
7+
model User {
8+
id Int @id @default(autoincrement())
9+
type String
10+
@@allow('all', true)
11+
}
12+
13+
model Container {
14+
id Int @id @default(autoincrement())
15+
drink Drink @relation(fields: [drinkId], references: [id])
16+
drinkId Int
17+
@@allow('all', true)
18+
}
19+
20+
model Drink {
21+
id Int @id @default(autoincrement())
22+
name String @unique
23+
containers Container[]
24+
type String
25+
26+
@@delegate(type)
27+
@@allow('all', true)
28+
}
29+
30+
model Beer extends Drink {
31+
@@allow('all', true)
32+
}
33+
`
34+
);
35+
36+
const db = enhance();
37+
38+
await db.beer.create({
39+
data: { id: 1, name: 'Beer1' },
40+
});
41+
42+
await db.container.create({ data: { drink: { connect: { id: 1 } } } });
43+
await db.container.create({ data: { drink: { connect: { id: 1 } } } });
44+
45+
const beers = await db.beer.findFirst({
46+
select: { id: true, name: true, _count: { select: { containers: true } } },
47+
orderBy: { name: 'asc' },
48+
});
49+
expect(beers).toMatchObject({ _count: { containers: 2 } });
50+
});
51+
});

0 commit comments

Comments
 (0)