Skip to content

Commit c38add4

Browse files
committed
fix(polymorphism): include submodel relations when reading a concrete model
1 parent df07830 commit c38add4

File tree

2 files changed

+96
-73
lines changed

2 files changed

+96
-73
lines changed

packages/runtime/src/enhancements/delegate.ts

Lines changed: 84 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import deepcopy from 'deepcopy';
3-
import deepmerge from 'deepmerge';
3+
import deepmerge, { type ArrayMergeOptions } from 'deepmerge';
44
import { lowerCaseFirst } from 'lower-case-first';
55
import { DELEGATE_AUX_RELATION_PREFIX } from '../constants';
66
import {
@@ -11,7 +11,6 @@ import {
1111
getIdFields,
1212
getModelInfo,
1313
isDelegateModel,
14-
requireField,
1514
resolveField,
1615
} from '../cross';
1716
import type { CrudContract, DbClientContract } from '../types';
@@ -204,7 +203,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
204203
}
205204

206205
if (!args.select) {
206+
// include base models upwards
207207
this.injectBaseIncludeRecursively(model, args);
208+
209+
// include sub models downwards
210+
this.injectConcreteIncludeRecursively(model, args);
208211
}
209212
}
210213

@@ -302,6 +305,30 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
302305
this.injectBaseIncludeRecursively(base.name, selectInclude.include[baseRelationName]);
303306
}
304307

308+
private injectConcreteIncludeRecursively(model: string, selectInclude: any) {
309+
const modelInfo = getModelInfo(this.options.modelMeta, model);
310+
if (!modelInfo) {
311+
return;
312+
}
313+
314+
// get sub models of this model
315+
const subModels = Object.values(this.options.modelMeta.models).filter((m) =>
316+
m.baseTypes?.includes(modelInfo.name)
317+
);
318+
319+
for (const subModel of subModels) {
320+
// include sub model relation field
321+
const subRelationName = this.makeAuxRelationName(subModel);
322+
if (selectInclude.select) {
323+
selectInclude.include = { [subRelationName]: {}, ...selectInclude.select };
324+
delete selectInclude.select;
325+
} else {
326+
selectInclude.include = { [subRelationName]: {}, ...selectInclude.include };
327+
}
328+
this.injectConcreteIncludeRecursively(subModel.name, selectInclude.include[subRelationName]);
329+
}
330+
}
331+
305332
// #endregion
306333

307334
// #region create
@@ -1038,6 +1065,31 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10381065
return entity;
10391066
}
10401067

1068+
const upMerged = this.assembleUp(model, entity);
1069+
const downMerged = this.assembleDown(model, entity);
1070+
1071+
// https://www.npmjs.com/package/deepmerge#arraymerge-example-combine-arrays
1072+
const combineMerge = (target: any[], source: any[], options: ArrayMergeOptions) => {
1073+
const destination = target.slice();
1074+
source.forEach((item, index) => {
1075+
if (typeof destination[index] === 'undefined') {
1076+
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options);
1077+
} else if (options.isMergeableObject(item)) {
1078+
destination[index] = deepmerge(target[index], item, options);
1079+
} else if (target.indexOf(item) === -1) {
1080+
destination.push(item);
1081+
}
1082+
});
1083+
return destination;
1084+
};
1085+
1086+
const result = deepmerge(upMerged, downMerged, {
1087+
arrayMerge: combineMerge,
1088+
});
1089+
return result;
1090+
}
1091+
1092+
private assembleUp(model: string, entity: any) {
10411093
const result: any = {};
10421094
const base = this.getBaseModel(model);
10431095

@@ -1046,7 +1098,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10461098
const baseRelationName = this.makeAuxRelationName(base);
10471099
const baseData = entity[baseRelationName];
10481100
if (baseData && typeof baseData === 'object') {
1049-
const baseAssembled = this.assembleHierarchy(base.name, baseData);
1101+
const baseAssembled = this.assembleUp(base.name, baseData);
10501102
Object.assign(result, baseAssembled);
10511103
}
10521104
}
@@ -1063,9 +1115,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10631115
const fieldValue = entity[field.name];
10641116
if (field.isDataModel) {
10651117
if (Array.isArray(fieldValue)) {
1066-
result[field.name] = fieldValue.map((item) => this.assembleHierarchy(field.type, item));
1118+
result[field.name] = fieldValue.map((item) => this.assembleUp(field.type, item));
10671119
} else {
1068-
result[field.name] = this.assembleHierarchy(field.type, fieldValue);
1120+
result[field.name] = this.assembleUp(field.type, fieldValue);
10691121
}
10701122
} else {
10711123
result[field.name] = fieldValue;
@@ -1076,66 +1128,39 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10761128
return result;
10771129
}
10781130

1079-
// #endregion
1080-
1081-
// #region backup
1082-
1083-
private transformWhereHierarchy(where: any, contextModel: ModelInfo, forModel: ModelInfo) {
1084-
if (!where || typeof where !== 'object') {
1085-
return where;
1086-
}
1087-
1088-
let curr: ModelInfo | undefined = contextModel;
1089-
const inheritStack: ModelInfo[] = [];
1090-
while (curr) {
1091-
inheritStack.unshift(curr);
1092-
curr = this.getBaseModel(curr.name);
1093-
}
1094-
1095-
let result: any = {};
1096-
for (const [key, value] of Object.entries(where)) {
1097-
const fieldInfo = requireField(this.options.modelMeta, contextModel.name, key);
1098-
const fieldHierarchy = this.transformFieldHierarchy(fieldInfo, value, contextModel, forModel, inheritStack);
1099-
result = deepmerge(result, fieldHierarchy);
1100-
}
1101-
1102-
return result;
1103-
}
1104-
1105-
private transformFieldHierarchy(
1106-
fieldInfo: FieldInfo,
1107-
value: unknown,
1108-
contextModel: ModelInfo,
1109-
forModel: ModelInfo,
1110-
inheritStack: ModelInfo[]
1111-
): any {
1112-
const fieldModel = fieldInfo.inheritedFrom ? this.getModelInfo(fieldInfo.inheritedFrom) : contextModel;
1113-
if (fieldModel === forModel) {
1114-
return { [fieldInfo.name]: value };
1115-
}
1116-
1117-
const fieldModelPos = inheritStack.findIndex((m) => m === fieldModel);
1118-
const forModelPos = inheritStack.findIndex((m) => m === forModel);
1131+
private assembleDown(model: string, entity: any) {
11191132
const result: any = {};
1120-
let curr = result;
1133+
const modelInfo = getModelInfo(this.options.modelMeta, model, true);
11211134

1122-
if (fieldModelPos > forModelPos) {
1123-
// walk down hierarchy
1124-
for (let i = forModelPos + 1; i <= fieldModelPos; i++) {
1125-
const rel = this.makeAuxRelationName(inheritStack[i]);
1126-
curr[rel] = {};
1127-
curr = curr[rel];
1135+
if (modelInfo.discriminator) {
1136+
// model is a delegate, merge sub model fields
1137+
const subModelName = entity[modelInfo.discriminator];
1138+
if (subModelName) {
1139+
const subModel = getModelInfo(this.options.modelMeta, subModelName, true);
1140+
const subRelationName = this.makeAuxRelationName(subModel);
1141+
const subData = entity[subRelationName];
1142+
if (subData && typeof subData === 'object') {
1143+
const subAssembled = this.assembleDown(subModel.name, subData);
1144+
Object.assign(result, subAssembled);
1145+
}
11281146
}
1129-
} else {
1130-
// walk up hierarchy
1131-
for (let i = forModelPos - 1; i >= fieldModelPos; i--) {
1132-
const rel = this.makeAuxRelationName(inheritStack[i]);
1133-
curr[rel] = {};
1134-
curr = curr[rel];
1147+
}
1148+
1149+
for (const field of Object.values(modelInfo.fields)) {
1150+
if (field.name in entity) {
1151+
const fieldValue = entity[field.name];
1152+
if (field.isDataModel) {
1153+
if (Array.isArray(fieldValue)) {
1154+
result[field.name] = fieldValue.map((item) => this.assembleDown(field.type, item));
1155+
} else {
1156+
result[field.name] = this.assembleDown(field.type, fieldValue);
1157+
}
1158+
} else {
1159+
result[field.name] = fieldValue;
1160+
}
11351161
}
11361162
}
11371163

1138-
curr[fieldInfo.name] = value;
11391164
return result;
11401165
}
11411166

tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -201,24 +201,22 @@ describe('Polymorphism Test', () => {
201201

202202
let video = await db.video.findFirst({ where: { duration: r.duration }, include: { owner: true } });
203203
expect(video).toMatchObject({
204-
id: video.id,
205-
createdAt: r.createdAt,
206-
viewCount: r.viewCount,
207-
url: r.url,
208-
duration: r.duration,
204+
...r,
209205
assetType: 'Video',
210206
videoType: 'RatedVideo',
211207
});
212-
expect(video.rating).toBeUndefined();
213208
expect(video.owner).toMatchObject(user);
214209

215210
const asset = await db.asset.findFirst({ where: { viewCount: r.viewCount }, include: { owner: true } });
216-
expect(asset).toMatchObject({ id: r.id, createdAt: r.createdAt, assetType: 'Video', viewCount: r.viewCount });
217-
expect(asset.url).toBeUndefined();
218-
expect(asset.duration).toBeUndefined();
219-
expect(asset.rating).toBeUndefined();
220-
expect(asset.videoType).toBeUndefined();
221-
expect(asset.owner).toMatchObject(user);
211+
expect(asset).toMatchObject({
212+
...r,
213+
assetType: 'Video',
214+
videoType: 'RatedVideo',
215+
owner: expect.objectContaining(user),
216+
});
217+
218+
const userWithAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } });
219+
expect(userWithAssets.assets[0]).toMatchObject(r);
222220

223221
const image = await db.image.create({
224222
data: { owner: { connect: { id: 1 } }, viewCount: 1, format: 'png' },
@@ -230,9 +228,9 @@ describe('Polymorphism Test', () => {
230228
createdAt: image.createdAt,
231229
assetType: 'Image',
232230
viewCount: image.viewCount,
231+
format: 'png',
232+
owner: expect.objectContaining(user),
233233
});
234-
expect(imgAsset.format).toBeUndefined();
235-
expect(imgAsset.owner).toMatchObject(user);
236234
});
237235

238236
it('order by base fields', async () => {

0 commit comments

Comments
 (0)