@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.
- package/.github/workflows/bump.yml +36 -0
- package/.github/workflows/publish.yml +49 -0
- package/CHANGELOG.md +18 -0
- package/lib/cacheMap.d.ts +23 -0
- package/lib/cacheMap.d.ts.map +1 -0
- package/lib/cacheMap.js +115 -0
- package/lib/cacheMap.js.map +1 -0
- package/lib/database.d.ts +10 -0
- package/lib/database.d.ts.map +1 -0
- package/lib/database.js +35 -0
- package/lib/database.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +21 -0
- package/lib/index.js.map +1 -0
- package/lib/store.d.ts +60 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +383 -0
- package/lib/store.js.map +1 -0
- package/lib/updateMap.d.ts +19 -0
- package/lib/updateMap.d.ts.map +1 -0
- package/lib/updateMap.js +67 -0
- package/lib/updateMap.js.map +1 -0
- package/lib/utils.d.ts +3 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +58 -0
- package/lib/utils.js.map +1 -0
- package/package.json +24 -0
- package/src/cacheMap.ts +135 -0
- package/src/database.ts +40 -0
- package/src/index.ts +2 -0
- package/src/store.ts +474 -0
- package/src/updateMap.ts +74 -0
- package/src/utils.ts +49 -0
- package/tsconfig.json +21 -0
package/src/cacheMap.ts
ADDED
|
@@ -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
|
+
}
|
package/src/database.ts
ADDED
|
@@ -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
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
|
+
}
|