@belopash/typeorm-store 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ import {Entity} from '@subsquid/typeorm-store'
2
+ import assert from 'assert'
3
+ import {EntityManager, EntityTarget, FindOptionsRelations} from 'typeorm'
4
+ import {copy} from './utils'
5
+
6
+ export class CachedEntity<E extends Entity> {
7
+ value: E | null
8
+ relations: {[key: string]: boolean}
9
+
10
+ constructor() {
11
+ this.value = null
12
+ this.relations = {}
13
+ }
14
+ }
15
+
16
+ export class CacheMap {
17
+ private map: Map<string, Map<string, CachedEntity<any>>> = new Map()
18
+
19
+ constructor(private em: () => EntityManager) {}
20
+
21
+ exist<E extends Entity>(entityClass: EntityTarget<E>, id: string) {
22
+ const cacheMap = this.getEntityCache(entityClass)
23
+ const cachedEntity = cacheMap.get(id)
24
+ return cachedEntity?.value != null
25
+ }
26
+
27
+ get<E extends Entity>(entityClass: EntityTarget<E>, id: string) {
28
+ const cacheMap = this.getEntityCache(entityClass)
29
+ return cacheMap.get(id) as CachedEntity<E> | undefined
30
+ }
31
+
32
+ ensure<E extends Entity>(entityClass: EntityTarget<E>, id: string) {
33
+ const cacheMap = this.getEntityCache(entityClass)
34
+
35
+ let cachedEntity = cacheMap.get(id)
36
+ if (cachedEntity == null) {
37
+ cachedEntity = new CachedEntity()
38
+ cacheMap.set(id, cachedEntity)
39
+ }
40
+ }
41
+
42
+ delete<E extends Entity>(entityClass: EntityTarget<E>, id: string) {
43
+ const cacheMap = this.getEntityCache(entityClass)
44
+
45
+ const cachedEntity = new CachedEntity()
46
+ cacheMap.set(id, cachedEntity)
47
+ }
48
+
49
+ add<E extends Entity>(entity: E, mask?: FindOptionsRelations<any>): void
50
+ add<E extends Entity>(entities: E[], mask?: FindOptionsRelations<any>): void
51
+ add<E extends Entity>(e: E | E[], mask: FindOptionsRelations<any> = {}) {
52
+ const em = this.em()
53
+
54
+ const entities = Array.isArray(e) ? e : [e]
55
+ if (entities.length == 0) return
56
+
57
+ const entityClass = entities[0].constructor
58
+ const metadata = em.connection.getMetadata(entities[0].constructor)
59
+
60
+ const cacheMap = this.getEntityCache(metadata.target)
61
+
62
+ for (const entity of entities) {
63
+ let cachedEntity = cacheMap.get(entity.id)
64
+ if (cachedEntity == null) {
65
+ cachedEntity = new CachedEntity()
66
+ cacheMap.set(entity.id, cachedEntity)
67
+ }
68
+
69
+ if (cachedEntity.value == null) {
70
+ cachedEntity.value = em.create(entityClass)
71
+ }
72
+
73
+ for (const column of metadata.nonVirtualColumns) {
74
+ const objectColumnValue = column.getEntityValue(entity)
75
+ if (objectColumnValue !== undefined) {
76
+ column.setEntityValue(cachedEntity.value, copy(objectColumnValue))
77
+ }
78
+ }
79
+
80
+ for (const relation of metadata.relations) {
81
+ const relatedMetadata = relation.inverseEntityMetadata
82
+ const relatedEntity = relation.getEntityValue(entity) as Entity | null | undefined
83
+
84
+ const relatedMask = mask[relation.propertyName]
85
+ if (relatedMask) {
86
+ if (relation.isOneToMany || relation.isManyToMany) {
87
+ if (Array.isArray(relatedEntity)) {
88
+ for (const r of relatedEntity) {
89
+ this.add(r, typeof relatedMask === 'boolean' ? {} : relatedMask)
90
+ }
91
+ }
92
+ } else if (relatedEntity != null) {
93
+ this.add(relatedEntity, typeof relatedMask === 'boolean' ? {} : relatedMask)
94
+ }
95
+ }
96
+
97
+ if (relation.isOwning && relatedMask) {
98
+ if (relatedEntity == null) {
99
+ relation.setEntityValue(cachedEntity.value, null)
100
+ } else {
101
+ const _relationCacheMap = this.getEntityCache(relatedMetadata.target)
102
+ const cachedRelation = _relationCacheMap.get(relatedEntity.id)
103
+ assert(
104
+ cachedRelation != null,
105
+ `missing entity ${relatedMetadata.name} with id ${relatedEntity.id}`
106
+ )
107
+
108
+ const relatedEntityIdOnly = em.create(relatedMetadata.target, {id: relatedEntity.id})
109
+ relation.setEntityValue(cachedEntity.value, relatedEntityIdOnly)
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ clear() {
117
+ for (const item of this.map.values()) {
118
+ item.clear()
119
+ }
120
+ this.map.clear()
121
+ }
122
+
123
+ private getEntityCache(entityClass: EntityTarget<any>) {
124
+ const em = this.em()
125
+ const metadata = em.connection.getMetadata(entityClass)
126
+
127
+ let map = this.map.get(metadata.name)
128
+ if (map == null) {
129
+ map = new Map()
130
+ this.map.set(metadata.name, map)
131
+ }
132
+
133
+ return map
134
+ }
135
+ }
@@ -0,0 +1,40 @@
1
+ import {IsolationLevel, TypeormDatabase, TypeormDatabaseOptions} from '@subsquid/typeorm-store'
2
+ import {ChangeTracker} from '@subsquid/typeorm-store/lib/hot'
3
+ import {FinalTxInfo, HotTxInfo, HashAndHeight} from '@subsquid/typeorm-store/lib/interfaces'
4
+ import assert from 'assert'
5
+ import {EntityManager} from 'typeorm'
6
+ import {StoreWithCache} from './store'
7
+
8
+ export {IsolationLevel, TypeormDatabaseOptions}
9
+
10
+ // @ts-ignore
11
+ export class TypeormDatabaseWithCache extends TypeormDatabase {
12
+ // @ts-ignore
13
+ transact(info: FinalTxInfo, cb: (store: StoreWithCache) => Promise<void>): Promise<void> {
14
+ return super.transact(info, cb as any)
15
+ }
16
+
17
+ // @ts-ignore
18
+ transactHot(info: HotTxInfo, cb: (store: StoreWithCache, block: HashAndHeight) => Promise<void>): Promise<void> {
19
+ return super.transactHot(info, cb as any)
20
+ }
21
+
22
+ private async performUpdates(
23
+ cb: (store: StoreWithCache) => Promise<void>,
24
+ em: EntityManager,
25
+ changeTracker?: ChangeTracker
26
+ ): Promise<void> {
27
+ let running = true
28
+
29
+ let store = new StoreWithCache(() => {
30
+ assert(running, `too late to perform db updates, make sure you haven't forgot to await on db query`)
31
+ return em
32
+ }, changeTracker)
33
+
34
+ try {
35
+ await cb(store)
36
+ } finally {
37
+ running = false
38
+ }
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './database'
2
+ export {EntityClass, Entity, FindManyOptions, FindOneOptions, StoreWithCache} from './store'
package/src/store.ts ADDED
@@ -0,0 +1,474 @@
1
+ import {Entity as _Entity, Entity, EntityClass, FindManyOptions, FindOneOptions, Store} from '@subsquid/typeorm-store'
2
+ import {ChangeTracker} from '@subsquid/typeorm-store/lib/hot'
3
+ import {def} from '@subsquid/util-internal'
4
+ import assert from 'assert'
5
+ import {Graph} from 'graph-data-structure'
6
+ import {EntityManager, EntityTarget, FindOptionsRelations, FindOptionsWhere, In} from 'typeorm'
7
+ import {copy, splitIntoBatches} from './utils'
8
+ import {CacheMap} from './cacheMap'
9
+ import {UpdateMap, UpdateType} from './updateMap'
10
+ import {RelationMetadata} from 'typeorm/metadata/RelationMetadata'
11
+
12
+ export {EntityClass, FindManyOptions, FindOneOptions, Entity}
13
+
14
+ export type DeferMap = Map<string, {ids: Set<string>; relations: FindOptionsRelations<any>}>
15
+ export interface ChangeSet {
16
+ inserts: Entity[]
17
+ upserts: Entity[]
18
+ delayedUpserts: Entity[]
19
+ removes: Entity[]
20
+ }
21
+
22
+ // @ts-ignore
23
+ export class StoreWithCache extends Store {
24
+ private deferMap: DeferMap = new Map()
25
+ private updates: Map<string, UpdateMap> = new Map()
26
+ private cache: CacheMap
27
+
28
+ constructor(private em: () => EntityManager, changes?: ChangeTracker) {
29
+ super(em, changes)
30
+ this.cache = new CacheMap(em)
31
+ }
32
+
33
+ async insert<E extends _Entity>(entity: E): Promise<void>
34
+ async insert<E extends _Entity>(entities: E[]): Promise<void>
35
+ async insert<E extends _Entity>(e: E | E[]): Promise<void> {
36
+ const em = this.em()
37
+
38
+ const entities = Array.isArray(e) ? e : [e]
39
+ if (entities.length == 0) return
40
+
41
+ const entityClass = entities[0].constructor
42
+ const metadata = em.connection.getMetadata(entityClass)
43
+
44
+ const relationMask: FindOptionsRelations<any> = {}
45
+ for (const relation of metadata.relations) {
46
+ if (relation.isOwning) {
47
+ relationMask[relation.propertyName] = true
48
+ }
49
+ }
50
+
51
+ const updateMap = this.getUpdateMap(entityClass)
52
+ for (const entity of entities) {
53
+ updateMap.insert(entity.id)
54
+ this.cache.add(entity, relationMask)
55
+ }
56
+ }
57
+
58
+ async upsert<E extends _Entity>(entity: E): Promise<void>
59
+ async upsert<E extends _Entity>(entities: E[]): Promise<void>
60
+ async upsert<E extends _Entity>(e: E | E[]): Promise<void> {
61
+ const em = this.em()
62
+
63
+ let entities = Array.isArray(e) ? e : [e]
64
+ if (entities.length == 0) return
65
+
66
+ const entityClass = entities[0].constructor
67
+ const metadata = em.connection.getMetadata(entityClass)
68
+
69
+ const updateMap = this.getUpdateMap(entityClass)
70
+ for (const entity of entities) {
71
+ const relationMask: FindOptionsRelations<any> = {}
72
+ for (const relation of metadata.relations) {
73
+ const relatedEntity = relation.getEntityValue(entity) as Entity | null | undefined
74
+
75
+ if (relation.isOwning && relatedEntity !== undefined) {
76
+ relationMask[relation.propertyName] = true
77
+ }
78
+ }
79
+
80
+ updateMap.upsert(entity.id)
81
+ this.cache.add(entity, relationMask)
82
+ }
83
+ }
84
+
85
+ async save<E extends _Entity>(entity: E): Promise<void>
86
+ async save<E extends _Entity>(entities: E[]): Promise<void>
87
+ async save<E extends _Entity>(e: E | E[]): Promise<void> {
88
+ return await this.upsert(e as any)
89
+ }
90
+
91
+ async remove<E extends Entity>(entity: E): Promise<void>
92
+ async remove<E extends Entity>(entities: E[]): Promise<void>
93
+ async remove<E extends Entity>(entityClass: EntityTarget<E>, id: string | string[]): Promise<void>
94
+ async remove<E extends Entity>(e: E | E[] | EntityTarget<E>, id?: string | string[]): Promise<void> {
95
+ const em = this.em()
96
+
97
+ if (id == null) {
98
+ const entities = Array.isArray(e) ? e : [e as E]
99
+ if (entities.length == 0) return
100
+
101
+ const entityClass = entities[0].constructor
102
+ const updateMap = this.getUpdateMap(entityClass)
103
+
104
+ for (const entity of entities) {
105
+ updateMap.remove(entity.id)
106
+ this.cache.delete(entityClass, entity.id)
107
+ }
108
+ } else {
109
+ const ids = Array.isArray(id) ? id : [id]
110
+ if (ids.length == 0) return
111
+
112
+ const entityClass = e as EntityTarget<E>
113
+ const updateMap = this.getUpdateMap(entityClass)
114
+
115
+ for (const i of ids) {
116
+ updateMap.remove(i)
117
+ this.cache.delete(entityClass, i)
118
+ }
119
+ }
120
+ }
121
+
122
+ async count<E extends Entity>(entityClass: EntityTarget<E>, options?: FindManyOptions<E>): Promise<number> {
123
+ await this.persist()
124
+ return await super.count(entityClass as EntityClass<E>, options)
125
+ }
126
+
127
+ async countBy<E extends Entity>(
128
+ entityClass: EntityTarget<E>,
129
+ where: FindOptionsWhere<E> | FindOptionsWhere<E>[]
130
+ ): Promise<number> {
131
+ await this.persist()
132
+ return await super.countBy(entityClass as EntityClass<E>, where)
133
+ }
134
+
135
+ async find<E extends Entity>(entityClass: EntityTarget<E>, options: FindManyOptions<E>): Promise<E[]> {
136
+ await this.persist()
137
+ const res = await super.find(entityClass as EntityClass<E>, options)
138
+ if (res != null) this.cache.add(res, options.relations)
139
+ return res
140
+ }
141
+
142
+ async findBy<E extends Entity>(
143
+ entityClass: EntityTarget<E>,
144
+ where: FindOptionsWhere<E> | FindOptionsWhere<E>[]
145
+ ): Promise<E[]> {
146
+ await this.persist()
147
+ const res = await super.findBy(entityClass as EntityClass<E>, where)
148
+ if (res != null) this.cache.add(res)
149
+ return res
150
+ }
151
+
152
+ async findOne<E extends Entity>(entityClass: EntityTarget<E>, options: FindOneOptions<E>): Promise<E | undefined> {
153
+ await this.persist()
154
+ const res = await super.findOne(entityClass as EntityClass<E>, options)
155
+ if (res != null) this.cache.add(res, options.relations)
156
+ return res
157
+ }
158
+
159
+ async findOneOrFail<E extends Entity>(entityClass: EntityTarget<E>, options: FindOneOptions<E>): Promise<E> {
160
+ await this.persist()
161
+ const res = await super.findOneOrFail(entityClass as EntityClass<E>, options)
162
+ if (res != null) this.cache.add(res, options.relations)
163
+ return res
164
+ }
165
+
166
+ async findOneBy<E extends Entity>(
167
+ entityClass: EntityTarget<E>,
168
+ where: FindOptionsWhere<E> | FindOptionsWhere<E>[]
169
+ ): Promise<E | undefined> {
170
+ await this.persist()
171
+ const res = await super.findOneBy(entityClass as EntityClass<E>, where)
172
+ if (res != null) this.cache.add(res)
173
+ return res
174
+ }
175
+
176
+ async findOneByOrFail<E extends Entity>(
177
+ entityClass: EntityTarget<E>,
178
+ where: FindOptionsWhere<E> | FindOptionsWhere<E>[]
179
+ ): Promise<E> {
180
+ await this.persist()
181
+ const res = await super.findOneByOrFail(entityClass as EntityClass<E>, where)
182
+ if (res != null) this.cache.add(res)
183
+ return res
184
+ }
185
+
186
+ async get<E extends Entity>(
187
+ entityClass: EntityTarget<E>,
188
+ id: string,
189
+ relations?: FindOptionsRelations<E>
190
+ ): Promise<E | undefined> {
191
+ await this.load()
192
+
193
+ const entity = this.getCached(entityClass, id, relations)
194
+
195
+ if (entity !== undefined) {
196
+ return entity == null ? undefined : entity
197
+ } else {
198
+ return await this.findOne(entityClass, {where: {id} as any, relations})
199
+ }
200
+ }
201
+
202
+ async getOrFail<E extends Entity>(
203
+ entityClass: EntityTarget<E>,
204
+ id: string,
205
+ relations?: FindOptionsRelations<E>
206
+ ): Promise<E> {
207
+ let e = await this.get(entityClass, id, relations)
208
+
209
+ if (e == null) {
210
+ const metadata = this.em().connection.getMetadata(entityClass)
211
+ throw new Error(`Missing entity ${metadata.name} with id "${id}"`)
212
+ }
213
+
214
+ return e
215
+ }
216
+
217
+ private getCached<E extends Entity>(entityClass: EntityTarget<E>, id: string, mask: FindOptionsRelations<E> = {}) {
218
+ const em = this.em()
219
+ const metadata = em.connection.getMetadata(entityClass)
220
+
221
+ const cachedEntity = this.cache.get(entityClass, id)
222
+
223
+ if (cachedEntity == null) {
224
+ return undefined
225
+ } else if (cachedEntity.value == null) {
226
+ return null
227
+ } else {
228
+ const clonedEntity = em.create(entityClass)
229
+
230
+ for (const column of metadata.nonVirtualColumns) {
231
+ const objectColumnValue = column.getEntityValue(cachedEntity.value)
232
+ if (objectColumnValue !== undefined) {
233
+ column.setEntityValue(clonedEntity, copy(objectColumnValue))
234
+ }
235
+ }
236
+
237
+ for (const relation of metadata.relations) {
238
+ let relatedMask = mask[relation.propertyName as keyof E]
239
+ if (!relatedMask) continue
240
+
241
+ const relatedEntity = relation.getEntityValue(cachedEntity.value)
242
+
243
+ if (relatedEntity === undefined) {
244
+ return undefined // relation is missing, but required
245
+ } else if (relatedEntity == null) {
246
+ relation.setEntityValue(clonedEntity, null)
247
+ } else {
248
+ const cachedRelatedEntity = this.getCached(
249
+ relation.inverseEntityMetadata.target,
250
+ relatedEntity.id,
251
+ typeof relatedMask === 'boolean' ? {} : relatedMask
252
+ )
253
+ assert(cachedRelatedEntity != null)
254
+
255
+ relation.setEntityValue(clonedEntity, cachedRelatedEntity)
256
+ }
257
+ }
258
+
259
+ return clonedEntity
260
+ }
261
+ }
262
+
263
+ defer<E extends Entity>(
264
+ entityClass: EntityTarget<E>,
265
+ id: string,
266
+ relations?: FindOptionsRelations<E>
267
+ ): DeferredEntity<E> {
268
+ const _deferredList = this.getDeferData(entityClass)
269
+
270
+ _deferredList.ids.add(id)
271
+
272
+ if (relations != null) {
273
+ _deferredList.relations = mergeRelataions(_deferredList.relations, relations)
274
+ }
275
+
276
+ return new DeferredEntity({
277
+ get: async () => this.get(entityClass, id, relations),
278
+ getOrFail: async () => this.getOrFail(entityClass, id, relations),
279
+ })
280
+ }
281
+
282
+ private async persist(): Promise<void> {
283
+ const em = this.em()
284
+
285
+ const entityOrder = this.getTopologicalOrder()
286
+ const entityOrderReversed = [...entityOrder].reverse()
287
+
288
+ const changeSets: Map<string, ChangeSet> = new Map()
289
+ for (const name of entityOrder) {
290
+ const updateMap = this.getUpdateMap(name)
291
+
292
+ const inserts: Entity[] = []
293
+ const upserts: Entity[] = []
294
+ const delayedUpserts: Entity[] = []
295
+ const removes: Entity[] = []
296
+ for (const {id, type} of updateMap) {
297
+ const cached = this.cache.get(name, id)
298
+
299
+ switch (type) {
300
+ case UpdateType.Insert: {
301
+ assert(cached != null && cached.value != null)
302
+ inserts.push(cached.value)
303
+ break
304
+ }
305
+ case UpdateType.Upsert: {
306
+ assert(cached != null && cached.value != null)
307
+
308
+ let isDelayed = false
309
+ for (const relation of this.getSelfRelations(name)) {
310
+ const relatedEntity = relation.getEntityValue(cached.value)
311
+ const relatedUpdateType = updateMap.get(relatedEntity.id)
312
+
313
+ if (relatedUpdateType === UpdateType.Insert) {
314
+ isDelayed = true
315
+ break
316
+ }
317
+ }
318
+
319
+ if (isDelayed) {
320
+ delayedUpserts.push(cached.value)
321
+ } else {
322
+ upserts.push(cached.value)
323
+ }
324
+ break
325
+ }
326
+ case UpdateType.Remove: {
327
+ const e = em.create(name, {id})
328
+ removes.push(e)
329
+ break
330
+ }
331
+ }
332
+ }
333
+
334
+ changeSets.set(name, {
335
+ inserts,
336
+ upserts,
337
+ delayedUpserts,
338
+ removes,
339
+ })
340
+ }
341
+
342
+ for (const name of entityOrder) {
343
+ const changeSet = changeSets.get(name)
344
+ if (changeSet == null) continue
345
+
346
+ await super.upsert(changeSet.upserts)
347
+ await super.insert(changeSet.inserts)
348
+ await super.upsert(changeSet.delayedUpserts)
349
+ }
350
+
351
+ for (const name of entityOrderReversed) {
352
+ const changeSet = changeSets.get(name)
353
+ if (changeSet == null) continue
354
+
355
+ await super.remove(changeSet.removes)
356
+ }
357
+
358
+ this.updates.clear()
359
+ }
360
+
361
+ async flush(): Promise<void> {
362
+ await this.persist()
363
+ this.cache.clear()
364
+ }
365
+
366
+ private async load(): Promise<void> {
367
+ const em = this.em()
368
+
369
+ for (const [name, deferData] of this.deferMap) {
370
+ const metadata = em.connection.getMetadata(name)
371
+
372
+ for (const id of deferData.ids) {
373
+ this.cache.ensure(metadata.target, id)
374
+ }
375
+
376
+ for (let batch of splitIntoBatches([...deferData.ids], 30000)) {
377
+ if (batch.length == 0) continue
378
+ await this.find<any>(metadata.target, {where: {id: In(batch)}, relations: deferData.relations})
379
+ }
380
+ }
381
+
382
+ this.deferMap.clear()
383
+ }
384
+
385
+ private knownSelfRelations: Record<string, RelationMetadata[]> = {}
386
+ private getSelfRelations<E extends Entity>(entityClass: EntityTarget<E>) {
387
+ const em = this.em()
388
+ const metadata = em.connection.getMetadata(entityClass)
389
+
390
+ if (this.knownSelfRelations[metadata.name] == null) {
391
+ this.knownSelfRelations[metadata.name] = metadata.relations.filter(
392
+ (r) => r.inverseEntityMetadata.name === metadata.name
393
+ )
394
+ }
395
+ return this.knownSelfRelations[metadata.name]
396
+ }
397
+
398
+ @def
399
+ private getTopologicalOrder() {
400
+ const em = this.em()
401
+ const graph = Graph()
402
+ for (const metadata of em.connection.entityMetadatas) {
403
+ graph.addNode(metadata.name)
404
+ for (const foreignKey of metadata.foreignKeys) {
405
+ if (foreignKey.referencedEntityMetadata === metadata) continue // don't add self-relations
406
+
407
+ graph.addEdge(metadata.name, foreignKey.referencedEntityMetadata.name)
408
+ }
409
+ }
410
+
411
+ return graph.topologicalSort().reverse()
412
+ }
413
+
414
+ private getDeferData(entityClass: EntityTarget<any>) {
415
+ const em = this.em()
416
+ const metadata = em.connection.getMetadata(entityClass)
417
+
418
+ let list = this.deferMap.get(metadata.name)
419
+ if (list == null) {
420
+ list = {ids: new Set(), relations: {}}
421
+ this.deferMap.set(metadata.name, list)
422
+ }
423
+
424
+ return list
425
+ }
426
+
427
+ private getUpdateMap(entityClass: EntityTarget<any>) {
428
+ const em = this.em()
429
+ const metadata = em.connection.getMetadata(entityClass)
430
+
431
+ let list = this.updates.get(metadata.name)
432
+ if (list == null) {
433
+ list = new UpdateMap()
434
+ this.updates.set(metadata.name, list)
435
+ }
436
+
437
+ return list
438
+ }
439
+ }
440
+
441
+ function mergeRelataions<E extends Entity>(
442
+ a: FindOptionsRelations<E>,
443
+ b: FindOptionsRelations<E>
444
+ ): FindOptionsRelations<E> {
445
+ const mergedObject: FindOptionsRelations<E> = {}
446
+
447
+ for (const key in a) {
448
+ mergedObject[key] = a[key]
449
+ }
450
+
451
+ for (const key in b) {
452
+ const bValue = b[key]
453
+ const value = mergedObject[key]
454
+ if (typeof bValue === 'object') {
455
+ mergedObject[key] = (typeof value === 'object' ? mergeRelataions(value, bValue) : bValue) as any
456
+ } else {
457
+ mergedObject[key] = value || bValue
458
+ }
459
+ }
460
+
461
+ return mergedObject
462
+ }
463
+
464
+ export class DeferredEntity<E extends Entity> {
465
+ constructor(private opts: {get: () => Promise<E | undefined>; getOrFail: () => Promise<E>}) {}
466
+
467
+ async get(): Promise<E | undefined> {
468
+ return await this.opts.get()
469
+ }
470
+
471
+ async getOrFail(): Promise<E> {
472
+ return await this.opts.getOrFail()
473
+ }
474
+ }