Skip to content

Commit c826ab9

Browse files
committed
feat(repository): upgrade hydrateMany so that it batches queries per entityType
#9
1 parent 9663935 commit c826ab9

File tree

1 file changed

+96
-44
lines changed

1 file changed

+96
-44
lines changed

src/polymorphic.repository.ts

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FindManyOptions,
66
FindOneOptions,
77
getMetadataArgsStorage,
8+
In,
89
ObjectLiteral,
910
Repository,
1011
SaveOptions,
@@ -25,7 +26,11 @@ import { POLYMORPHIC_REPOSITORY } from './constants';
2526
type PolymorphicHydrationType = {
2627
key: string;
2728
type: 'children' | 'parent';
28-
values: PolymorphicChildInterface[] | PolymorphicChildInterface;
29+
hasMany: boolean;
30+
valueKeyMap: Record<
31+
string,
32+
(PolymorphicChildInterface | PolymorphicChildInterface[])[]
33+
>;
2934
};
3035

3136
const entityTypeColumn = (options: PolymorphicMetadataInterface): string =>
@@ -105,94 +110,142 @@ export abstract class AbstractPolymorphicRepository<
105110
}
106111

107112
public async hydrateMany(entities: E[]): Promise<E[]> {
108-
return Promise.all(entities.map((ent) => this.hydrateOne(ent)));
113+
const metadata = this.getPolymorphicMetadata();
114+
return this.hydratePolymorphs(entities, metadata);
109115
}
110116

111117
public async hydrateOne(entity: E): Promise<E> {
112-
const metadata = this.getPolymorphicMetadata();
113-
114-
return this.hydratePolymorphs(entity, metadata);
118+
const result = await this.hydrateMany([entity]);
119+
return result[0];
115120
}
116121

117122
private async hydratePolymorphs(
118-
entity: E,
123+
entities: E[],
119124
options: PolymorphicMetadataInterface[],
120-
): Promise<E> {
125+
): Promise<E[]> {
121126
const values = await Promise.all(
122127
options.map((option: PolymorphicMetadataInterface) =>
123-
this.hydrateEntities(entity, option),
128+
this.hydrateEntities(entities, option),
124129
),
125130
);
126131

127-
return values.reduce<E>((e: E, vals: PolymorphicHydrationType) => {
128-
const values =
129-
vals.type === 'parent' && Array.isArray(vals.values)
130-
? vals.values.filter((v) => typeof v !== 'undefined' && v !== null)
131-
: vals.values;
132-
const polys =
133-
vals.type === 'parent' && Array.isArray(values) ? values[0] : values; // TODO should be condition for !hasMany
134-
type EntityKey = keyof E;
135-
const key = vals.key as EntityKey;
136-
e[key] = polys as (typeof e)[typeof key];
137-
138-
return e;
139-
}, entity);
132+
const results: E[] = [];
133+
for (let entity of entities) {
134+
const result = values.reduce<E>(
135+
(e: E, vals: PolymorphicHydrationType) => {
136+
const polyKey = `${e.entityType}:${e.entityId}`;
137+
const polys = vals.hasMany
138+
? vals.valueKeyMap[polyKey]
139+
: vals.valueKeyMap[polyKey][0];
140+
141+
type EntityKey = keyof E;
142+
const key = vals.key as EntityKey;
143+
e[key] = polys as (typeof e)[typeof key];
144+
return e;
145+
},
146+
entity,
147+
);
148+
149+
results.push(result);
150+
}
151+
152+
return results;
140153
}
141154

142155
private async hydrateEntities(
143-
entity: E,
156+
entities: E[],
144157
options: PolymorphicMetadataInterface,
145158
): Promise<PolymorphicHydrationType> {
159+
const typeColumn = entityTypeColumn(options);
146160
const entityTypes: (Function | string)[] =
147161
options.type === 'parent'
148-
? [entity[entityTypeColumn(options)]]
162+
? [...new Set(entities.map((e) => e[typeColumn]))]
149163
: Array.isArray(options.classType)
150164
? options.classType
151165
: [options.classType];
152166

153167
// TODO if not hasMany, should I return if one is found?
154168
const results = await Promise.all(
155169
entityTypes.map((type: Function) =>
156-
this.findPolymorphs(entity, type, options),
170+
this.findPolymorphs(entities, type, options),
157171
),
158172
);
159173

174+
const idColumn = entityIdColumn(options);
175+
const isParent = this.isParent(options);
176+
const primaryColumn = PrimaryColumn(options);
177+
178+
const entitiesResultMap = results
179+
// flatten all the results
180+
.reduce<PolymorphicChildInterface[]>((acc, val) => {
181+
if (Array.isArray(val)) {
182+
acc.push(...val);
183+
} else {
184+
acc.push(val);
185+
}
186+
return acc;
187+
}, [])
188+
// map the results to a keyed map by entityType & entityId
189+
.reduce<
190+
Record<
191+
string,
192+
(PolymorphicChildInterface | PolymorphicChildInterface[])[]
193+
>
194+
>((acc, val) => {
195+
let key: string;
196+
if (isParent) {
197+
const [pColumnVal, entityType] = Array.isArray(val)
198+
? [val[0][primaryColumn], val[0].constructor.name]
199+
: [val[primaryColumn], val.constructor.name];
200+
201+
key = `${entityType}:${pColumnVal}`;
202+
} else {
203+
const [idColumnVal, typeColumnVal] = Array.isArray(val)
204+
? [val[0][idColumn], val[0][typeColumn]]
205+
: [val[idColumn], val[typeColumn]];
206+
207+
key = `${typeColumnVal}:${idColumnVal}`;
208+
}
209+
210+
acc[key] = acc[key] || [];
211+
acc[key].push(val);
212+
return acc;
213+
}, {});
160214
return {
161215
key: options.propertyKey,
162216
type: options.type,
163-
values: (options.hasMany &&
164-
Array.isArray(results) &&
165-
results.length > 0 &&
166-
Array.isArray(results[0])
167-
? results.reduce<PolymorphicChildInterface[]>(
168-
(
169-
resultEntities: PolymorphicChildInterface[],
170-
entities: PolymorphicChildInterface[],
171-
) => entities.concat(...resultEntities),
172-
results as PolymorphicChildInterface[],
173-
)
174-
: results) as PolymorphicChildInterface | PolymorphicChildInterface[],
217+
hasMany: options.hasMany,
218+
valueKeyMap: entitiesResultMap,
175219
};
176220
}
177221

178222
private async findPolymorphs(
179-
parent: E,
223+
entities: E[],
180224
entityType: Function,
181225
options: PolymorphicMetadataInterface,
182226
): Promise<PolymorphicChildInterface[] | PolymorphicChildInterface | never> {
183227
const repository = this.findRepository(entityType);
228+
const idColumn = entityIdColumn(options);
229+
const primaryColumn = PrimaryColumn(options);
184230

185-
return repository[options.hasMany ? 'find' : 'findOne'](
231+
// filter out any entities that don't match the given entityType
232+
const filteredEntities = entities.filter((e) => {
233+
return repository.target.toString() === e.entityType;
234+
});
235+
236+
const method =
237+
options.hasMany || filteredEntities.length > 1 ? 'find' : 'findOne';
238+
return repository[method](
186239
options.type === 'parent'
187240
? {
188241
where: {
189242
// TODO: Not sure about this change (key was just id before)
190-
[PrimaryColumn(options)]: parent[entityIdColumn(options)],
243+
[primaryColumn]: In(filteredEntities.map((p) => p[idColumn])),
191244
},
192245
}
193246
: {
194247
where: {
195-
[entityIdColumn(options)]: parent[PrimaryColumn(options)],
248+
[idColumn]: In(filteredEntities.map((p) => p[primaryColumn])),
196249
[entityTypeColumn(options)]: entityType,
197250
},
198251
},
@@ -338,9 +391,7 @@ export abstract class AbstractPolymorphicRepository<
338391

339392
const metadata = this.getPolymorphicMetadata();
340393

341-
return Promise.all(
342-
results.map((entity) => this.hydratePolymorphs(entity, metadata)),
343-
);
394+
return this.hydratePolymorphs(results, metadata);
344395
}
345396

346397
public async findOne(options?: FindOneOptions<E>): Promise<E | null> {
@@ -356,7 +407,8 @@ export abstract class AbstractPolymorphicRepository<
356407
return entity;
357408
}
358409

359-
return this.hydratePolymorphs(entity, polymorphicMetadata);
410+
const results = await this.hydratePolymorphs([entity], polymorphicMetadata);
411+
return results[0];
360412
}
361413

362414
create(): E;

0 commit comments

Comments
 (0)