joist-orm 0.1.536 → 1.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/build/{BaseEntity.d.ts → src/BaseEntity.d.ts} +2 -1
- package/build/{BaseEntity.js → src/BaseEntity.js} +13 -9
- package/build/src/BaseEntity.js.map +1 -0
- package/build/{EntityManager.d.ts → src/EntityManager.d.ts} +139 -110
- package/build/{EntityManager.js → src/EntityManager.js} +281 -262
- package/build/src/EntityManager.js.map +1 -0
- package/build/{QueryBuilder.d.ts → src/QueryBuilder.d.ts} +53 -3
- package/build/src/QueryBuilder.js +341 -0
- package/build/src/QueryBuilder.js.map +1 -0
- package/build/src/Todo.d.ts +25 -0
- package/build/src/Todo.js +52 -0
- package/build/src/Todo.js.map +1 -0
- package/build/src/changes.d.ts +34 -0
- package/build/src/changes.js +37 -0
- package/build/src/changes.js.map +1 -0
- package/build/src/config.d.ts +43 -0
- package/build/src/config.js +114 -0
- package/build/src/config.js.map +1 -0
- package/build/{createOrUpdatePartial.d.ts → src/createOrUpdatePartial.d.ts} +2 -1
- package/build/{createOrUpdatePartial.js → src/createOrUpdatePartial.js} +42 -10
- package/build/src/createOrUpdatePartial.js.map +1 -0
- package/build/src/dataloaders/findDataLoader.d.ts +5 -0
- package/build/src/dataloaders/findDataLoader.js +28 -0
- package/build/src/dataloaders/findDataLoader.js.map +1 -0
- package/build/src/dataloaders/loadDataLoader.d.ts +3 -0
- package/build/src/dataloaders/loadDataLoader.js +37 -0
- package/build/src/dataloaders/loadDataLoader.js.map +1 -0
- package/build/src/dataloaders/manyToManyDataLoader.d.ts +5 -0
- package/build/src/dataloaders/manyToManyDataLoader.js +78 -0
- package/build/src/dataloaders/manyToManyDataLoader.js.map +1 -0
- package/build/src/dataloaders/manyToManyFindDataLoader.d.ts +5 -0
- package/build/src/dataloaders/manyToManyFindDataLoader.js +33 -0
- package/build/src/dataloaders/manyToManyFindDataLoader.js.map +1 -0
- package/build/src/dataloaders/oneToManyDataLoader.d.ts +4 -0
- package/build/src/dataloaders/oneToManyDataLoader.js +40 -0
- package/build/src/dataloaders/oneToManyDataLoader.js.map +1 -0
- package/build/src/dataloaders/oneToManyFindDataLoader.d.ts +5 -0
- package/build/src/dataloaders/oneToManyFindDataLoader.js +32 -0
- package/build/src/dataloaders/oneToManyFindDataLoader.js.map +1 -0
- package/build/src/dataloaders/oneToOneDataLoader.d.ts +4 -0
- package/build/src/dataloaders/oneToOneDataLoader.js +40 -0
- package/build/src/dataloaders/oneToOneDataLoader.js.map +1 -0
- package/build/src/drivers/IdAssigner.d.ts +33 -0
- package/build/src/drivers/IdAssigner.js +106 -0
- package/build/src/drivers/IdAssigner.js.map +1 -0
- package/build/src/drivers/InMemoryDriver.d.ts +29 -0
- package/build/src/drivers/InMemoryDriver.js +306 -0
- package/build/src/drivers/InMemoryDriver.js.map +1 -0
- package/build/src/drivers/PostgresDriver.d.ts +40 -0
- package/build/src/drivers/PostgresDriver.js +376 -0
- package/build/src/drivers/PostgresDriver.js.map +1 -0
- package/build/src/drivers/driver.d.ts +23 -0
- package/build/src/drivers/driver.js +3 -0
- package/build/src/drivers/driver.js.map +1 -0
- package/build/src/drivers/index.d.ts +4 -0
- package/build/src/drivers/index.js +17 -0
- package/build/src/drivers/index.js.map +1 -0
- package/build/{getProperties.d.ts → src/getProperties.d.ts} +0 -0
- package/build/{getProperties.js → src/getProperties.js} +1 -1
- package/build/src/getProperties.js.map +1 -0
- package/build/src/index.d.ts +62 -0
- package/build/src/index.js +263 -0
- package/build/src/index.js.map +1 -0
- package/build/src/keys.d.ts +30 -0
- package/build/{keys.js → src/keys.js} +48 -16
- package/build/src/keys.js.map +1 -0
- package/build/{loadLens.d.ts → src/loadLens.d.ts} +2 -2
- package/build/{loadLens.js → src/loadLens.js} +1 -1
- package/build/src/loadLens.js.map +1 -0
- package/build/src/loaded.d.ts +49 -0
- package/build/src/loaded.js +9 -0
- package/build/src/loaded.js.map +1 -0
- package/build/{newTestInstance.d.ts → src/newTestInstance.d.ts} +37 -3
- package/build/src/newTestInstance.js +342 -0
- package/build/src/newTestInstance.js.map +1 -0
- package/build/{collections → src/relations}/AbstractRelationImpl.d.ts +6 -5
- package/build/{collections → src/relations}/AbstractRelationImpl.js +0 -0
- package/build/src/relations/AbstractRelationImpl.js.map +1 -0
- package/build/src/relations/Collection.d.ts +26 -0
- package/build/src/relations/Collection.js +19 -0
- package/build/src/relations/Collection.js.map +1 -0
- package/build/{collections → src/relations}/CustomCollection.d.ts +6 -2
- package/build/{collections → src/relations}/CustomCollection.js +17 -9
- package/build/src/relations/CustomCollection.js.map +1 -0
- package/build/{collections → src/relations}/CustomReference.d.ts +7 -2
- package/build/{collections → src/relations}/CustomReference.js +16 -9
- package/build/src/relations/CustomReference.js.map +1 -0
- package/build/src/relations/LargeCollection.d.ts +17 -0
- package/build/src/relations/LargeCollection.js +3 -0
- package/build/src/relations/LargeCollection.js.map +1 -0
- package/build/{collections → src/relations}/ManyToManyCollection.d.ts +9 -2
- package/build/src/relations/ManyToManyCollection.js +249 -0
- package/build/src/relations/ManyToManyCollection.js.map +1 -0
- package/build/src/relations/ManyToManyLargeCollection.d.ts +25 -0
- package/build/src/relations/ManyToManyLargeCollection.js +97 -0
- package/build/src/relations/ManyToManyLargeCollection.js.map +1 -0
- package/build/src/relations/ManyToOneReference.d.ts +77 -0
- package/build/{collections → src/relations}/ManyToOneReference.js +101 -48
- package/build/src/relations/ManyToOneReference.js.map +1 -0
- package/build/{collections → src/relations}/OneToManyCollection.d.ts +10 -2
- package/build/{collections → src/relations}/OneToManyCollection.js +54 -59
- package/build/src/relations/OneToManyCollection.js.map +1 -0
- package/build/src/relations/OneToManyLargeCollection.d.ts +25 -0
- package/build/src/relations/OneToManyLargeCollection.js +83 -0
- package/build/src/relations/OneToManyLargeCollection.js.map +1 -0
- package/build/src/relations/OneToOneReference.d.ts +82 -0
- package/build/src/relations/OneToOneReference.js +168 -0
- package/build/src/relations/OneToOneReference.js.map +1 -0
- package/build/src/relations/PolymorphicReference.d.ts +69 -0
- package/build/src/relations/PolymorphicReference.js +210 -0
- package/build/src/relations/PolymorphicReference.js.map +1 -0
- package/build/src/relations/Reference.d.ts +29 -0
- package/build/src/relations/Reference.js +23 -0
- package/build/src/relations/Reference.js.map +1 -0
- package/build/src/relations/Relation.d.ts +10 -0
- package/build/src/relations/Relation.js +13 -0
- package/build/src/relations/Relation.js.map +1 -0
- package/build/src/relations/hasAsyncProperty.d.ts +36 -0
- package/build/src/relations/hasAsyncProperty.js +55 -0
- package/build/src/relations/hasAsyncProperty.js.map +1 -0
- package/build/{collections → src/relations}/hasManyDerived.d.ts +2 -1
- package/build/{collections → src/relations}/hasManyDerived.js +1 -1
- package/build/src/relations/hasManyDerived.js.map +1 -0
- package/build/{collections → src/relations}/hasManyThrough.d.ts +0 -0
- package/build/{collections → src/relations}/hasManyThrough.js +2 -2
- package/build/src/relations/hasManyThrough.js.map +1 -0
- package/build/{collections → src/relations}/hasOneDerived.d.ts +3 -2
- package/build/{collections → src/relations}/hasOneDerived.js +1 -1
- package/build/src/relations/hasOneDerived.js.map +1 -0
- package/build/{collections → src/relations}/hasOneThrough.d.ts +0 -0
- package/build/{collections → src/relations}/hasOneThrough.js +2 -2
- package/build/src/relations/hasOneThrough.js.map +1 -0
- package/build/src/relations/index.d.ts +18 -0
- package/build/src/relations/index.js +53 -0
- package/build/src/relations/index.js.map +1 -0
- package/build/{reverseHint.d.ts → src/reverseHint.d.ts} +2 -1
- package/build/{reverseHint.js → src/reverseHint.js} +13 -9
- package/build/src/reverseHint.js.map +1 -0
- package/build/src/rules.d.ts +23 -0
- package/build/src/rules.js +23 -0
- package/build/src/rules.js.map +1 -0
- package/build/src/serde.d.ts +121 -0
- package/build/src/serde.js +190 -0
- package/build/src/serde.js.map +1 -0
- package/build/{utils.d.ts → src/utils.d.ts} +2 -0
- package/build/{utils.js → src/utils.js} +10 -1
- package/build/src/utils.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +30 -15
- package/build/BaseEntity.js.map +0 -1
- package/build/EntityManager.js.map +0 -1
- package/build/EntityPersister.d.ts +0 -30
- package/build/EntityPersister.js +0 -197
- package/build/EntityPersister.js.map +0 -1
- package/build/QueryBuilder.js +0 -195
- package/build/QueryBuilder.js.map +0 -1
- package/build/changes.d.ts +0 -23
- package/build/changes.js +0 -14
- package/build/changes.js.map +0 -1
- package/build/collections/AbstractRelationImpl.js.map +0 -1
- package/build/collections/CustomCollection.js.map +0 -1
- package/build/collections/CustomReference.js.map +0 -1
- package/build/collections/ManyToManyCollection.js +0 -288
- package/build/collections/ManyToManyCollection.js.map +0 -1
- package/build/collections/ManyToOneReference.d.ts +0 -50
- package/build/collections/ManyToOneReference.js.map +0 -1
- package/build/collections/OneToManyCollection.js.map +0 -1
- package/build/collections/OneToOneReference.d.ts +0 -51
- package/build/collections/OneToOneReference.js +0 -132
- package/build/collections/OneToOneReference.js.map +0 -1
- package/build/collections/hasManyDerived.js.map +0 -1
- package/build/collections/hasManyThrough.js.map +0 -1
- package/build/collections/hasOneDerived.js.map +0 -1
- package/build/collections/hasOneThrough.js.map +0 -1
- package/build/collections/index.d.ts +0 -19
- package/build/collections/index.js +0 -49
- package/build/collections/index.js.map +0 -1
- package/build/createOrUpdatePartial.js.map +0 -1
- package/build/getProperties.js.map +0 -1
- package/build/index.d.ts +0 -140
- package/build/index.js +0 -278
- package/build/index.js.map +0 -1
- package/build/keys.d.ts +0 -21
- package/build/keys.js.map +0 -1
- package/build/loadLens.js.map +0 -1
- package/build/newTestInstance.js +0 -153
- package/build/newTestInstance.js.map +0 -1
- package/build/reverseHint.js.map +0 -1
- package/build/serde.d.ts +0 -47
- package/build/serde.js +0 -93
- package/build/serde.js.map +0 -1
- package/build/utils.js.map +0 -1
- package/package.json.bak +0 -27
- package/src/BaseEntity.ts +0 -104
- package/src/EntityManager.ts +0 -1263
- package/src/EntityPersister.ts +0 -240
- package/src/QueryBuilder.ts +0 -289
- package/src/changes.ts +0 -40
- package/src/collections/AbstractRelationImpl.ts +0 -28
- package/src/collections/CustomCollection.ts +0 -152
- package/src/collections/CustomReference.ts +0 -138
- package/src/collections/ManyToManyCollection.ts +0 -346
- package/src/collections/ManyToOneReference.ts +0 -215
- package/src/collections/OneToManyCollection.ts +0 -254
- package/src/collections/OneToOneReference.ts +0 -153
- package/src/collections/hasManyDerived.ts +0 -29
- package/src/collections/hasManyThrough.ts +0 -20
- package/src/collections/hasOneDerived.ts +0 -26
- package/src/collections/hasOneThrough.ts +0 -20
- package/src/collections/index.ts +0 -74
- package/src/createOrUpdatePartial.ts +0 -144
- package/src/getProperties.ts +0 -27
- package/src/index.ts +0 -400
- package/src/keys.ts +0 -75
- package/src/loadLens.ts +0 -126
- package/src/newTestInstance.ts +0 -205
- package/src/reverseHint.ts +0 -43
- package/src/serde.ts +0 -97
- package/src/utils.ts +0 -63
- package/tsconfig.json +0 -21
- package/tsconfig.tsbuildinfo +0 -2646
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { Entity, IdOf } from "../EntityManager";
|
|
2
|
-
import { Collection, ensureNotDeleted, fail } from "../index";
|
|
3
|
-
import { AbstractRelationImpl } from "./AbstractRelationImpl";
|
|
4
|
-
|
|
5
|
-
export type CustomCollectionOpts<T extends Entity, U extends Entity> = {
|
|
6
|
-
// We purposefully don't capture the return value of `load` b/c we want `get` to re-calc from `entity`
|
|
7
|
-
// each time it's invoked so that it reflects any changed values.
|
|
8
|
-
load: (entity: T) => Promise<any>;
|
|
9
|
-
get: (entity: T) => readonly U[];
|
|
10
|
-
set?: (entity: T, other: U[]) => void;
|
|
11
|
-
find?: (entity: T, id: IdOf<U>) => U | undefined;
|
|
12
|
-
add?: (entity: T, other: U) => void;
|
|
13
|
-
remove?: (entity: T, other: U) => void;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Allows user-defined collections that will work in `populate` / preload hints.
|
|
18
|
-
*
|
|
19
|
-
* This works because Joist's `populate`/preloading is not based on creating giant SQL joins,
|
|
20
|
-
* but instead by just walking "1 dataloader per level". Given this, we don't really care
|
|
21
|
-
* what happens at each "level" of resolution, as long as: a) it's a promise, and b) the
|
|
22
|
-
* caller uses dataloader to be batch friendly.
|
|
23
|
-
*
|
|
24
|
-
* This `CustomCollection` API is fairly low-level; users should more likely prefer higher-level
|
|
25
|
-
* abstractions like `hasManyThrough`, which are built on `CustomCollection.
|
|
26
|
-
*/
|
|
27
|
-
export class CustomCollection<T extends Entity, U extends Entity> extends AbstractRelationImpl<U[]>
|
|
28
|
-
implements Collection<T, U> {
|
|
29
|
-
// We keep both a promise+loaded flag and not an actual `this.loaded = await load` because
|
|
30
|
-
// the values can become stale; we want to each `.get` call to repeatedly evaluate the latest values.
|
|
31
|
-
private loadPromise: Promise<any> | undefined;
|
|
32
|
-
private _isLoaded = false;
|
|
33
|
-
|
|
34
|
-
constructor(public entity: T, private opts: CustomCollectionOpts<T, U>) {
|
|
35
|
-
super();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
get get(): readonly U[] {
|
|
39
|
-
return this.doGet({ withDeleted: false });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get getWithDeleted(): readonly U[] {
|
|
43
|
-
return this.doGet({ withDeleted: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get isLoaded(): boolean {
|
|
47
|
-
return this._isLoaded;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async load(opts?: { withDeleted?: boolean }): Promise<readonly U[]> {
|
|
51
|
-
ensureNotDeleted(this.entity, { ignore: "pending" });
|
|
52
|
-
if (!this.isLoaded) {
|
|
53
|
-
if (this.loadPromise === undefined) {
|
|
54
|
-
this.loadPromise = this.opts.load(this.entity);
|
|
55
|
-
await this.loadPromise;
|
|
56
|
-
this.loadPromise = undefined;
|
|
57
|
-
this._isLoaded = true;
|
|
58
|
-
} else {
|
|
59
|
-
await this.loadPromise;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return this.doGet(opts);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
initializeForNewEntity(): void {
|
|
67
|
-
// Normally we flag relations as loaded if created on a new entity, however CustomCollections
|
|
68
|
-
// might require crawling N-layers down from our initial opts, i.e. if creating a BookReview
|
|
69
|
-
// with a book, accessing review.author maybe not necessarily be safe to do immediately b/c
|
|
70
|
-
// we need to load book.author to successfully evaluated review -> book -> author synchronously.
|
|
71
|
-
// this._isLoaded = true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
set(values: U[]): void {
|
|
75
|
-
ensureNewOrLoaded(this);
|
|
76
|
-
const { set, add, remove } = this.opts;
|
|
77
|
-
if (set !== undefined) {
|
|
78
|
-
set(this.entity, values);
|
|
79
|
-
} else if (add !== undefined && remove !== undefined) {
|
|
80
|
-
const current = this.get;
|
|
81
|
-
current.filter((value) => !values.includes(value)).forEach((value) => this.remove(value));
|
|
82
|
-
values.filter((value) => !current.includes(value)).forEach((value) => this.add(value));
|
|
83
|
-
} else {
|
|
84
|
-
fail(`'set' not implemented and not inferrable from 'add'/'remove' on ${this}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
setFromOpts(values: U[]): void {
|
|
89
|
-
this.set(values);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async find(id: IdOf<U>): Promise<U | undefined> {
|
|
93
|
-
if (!this.isLoaded) {
|
|
94
|
-
await this.load();
|
|
95
|
-
}
|
|
96
|
-
const { find } = this.opts;
|
|
97
|
-
if (find === undefined) {
|
|
98
|
-
return this.doGet().find((other) => other.id === id);
|
|
99
|
-
} else {
|
|
100
|
-
return find(this.entity, id);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
add(other: U): void {
|
|
105
|
-
ensureNewOrLoaded(this);
|
|
106
|
-
const { add } = this.opts;
|
|
107
|
-
if (add === undefined) {
|
|
108
|
-
fail(`'add' not implemented on ${this}`);
|
|
109
|
-
}
|
|
110
|
-
add(this.entity, other);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
remove(other: U): void {
|
|
114
|
-
ensureNewOrLoaded(this);
|
|
115
|
-
const { remove } = this.opts;
|
|
116
|
-
if (remove === undefined) {
|
|
117
|
-
fail(`'add' not implemented on ${this}`);
|
|
118
|
-
}
|
|
119
|
-
remove(this.entity, other);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// these callbacks should be no-ops as they ought to be handled by the underlying relations
|
|
123
|
-
async onEntityDeletedAndFlushing(): Promise<void> {}
|
|
124
|
-
onEntityDelete(): void {}
|
|
125
|
-
async refreshIfLoaded(): Promise<void> {}
|
|
126
|
-
|
|
127
|
-
/** Finds this CustomCollections field name by looking in the entity for the key that we're assigned to. */
|
|
128
|
-
get fieldName(): string {
|
|
129
|
-
return Object.entries(this.entity).filter((e) => e[1] === this)[0][0];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
toString(): string {
|
|
133
|
-
return `CustomCollection(entity: ${this.entity}, fieldName: ${this.fieldName})`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private doGet(opts?: { withDeleted?: boolean }): readonly U[] {
|
|
137
|
-
ensureNotDeleted(this.entity, { ignore: "pending" });
|
|
138
|
-
ensureNewOrLoaded(this);
|
|
139
|
-
return this.filterDeleted(this.opts.get(this.entity), opts);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private filterDeleted(entities: readonly U[], opts?: { withDeleted?: boolean }): readonly U[] {
|
|
143
|
-
return entities.filter((entity) => opts?.withDeleted === true || !entity.isDeletedEntity);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function ensureNewOrLoaded(reference: CustomCollection<any, any>) {
|
|
148
|
-
if (!(reference.isLoaded || reference.entity.isNewEntity)) {
|
|
149
|
-
// This should only be callable in the type system if we've already resolved this to an instance
|
|
150
|
-
fail(`${reference.entity}.${reference.fieldName} was not loaded`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { Entity, IdOf } from "../EntityManager";
|
|
2
|
-
import { deTagIds, ensureNotDeleted, fail, Reference, unsafeDeTagIds } from "../index";
|
|
3
|
-
import { AbstractRelationImpl } from "./AbstractRelationImpl";
|
|
4
|
-
|
|
5
|
-
export type CustomReferenceOpts<T extends Entity, U extends Entity, N extends never | undefined> = {
|
|
6
|
-
// We purposefully don't capture the return value of `load` b/c we want `get` to re-calc from `entity`
|
|
7
|
-
// each time it's invoked so that it reflects any changed values.
|
|
8
|
-
load: (entity: T) => Promise<void>;
|
|
9
|
-
get: (entity: T) => U | N;
|
|
10
|
-
set?: (entity: T, other: U) => void;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Allows user-defined references that will work in `populate` / preload hints.
|
|
15
|
-
*
|
|
16
|
-
* This works because Joist's `populate`/preloading is not based on creating giant SQL joins,
|
|
17
|
-
* but instead by just walking "1 dataloader per level". Given this, we don't really care
|
|
18
|
-
* what happens at each "level" of resolution, as long as: a) it's a promise, and b) the
|
|
19
|
-
* caller uses dataloader to be batch friendly.
|
|
20
|
-
*
|
|
21
|
-
* This `CustomReference` API is fairly low-level; users should more likely prefer higher-level
|
|
22
|
-
* abstractions like `hasOneThrough`, which are built on `CustomReference.
|
|
23
|
-
*/
|
|
24
|
-
export class CustomReference<T extends Entity, U extends Entity, N extends never | undefined>
|
|
25
|
-
extends AbstractRelationImpl<U>
|
|
26
|
-
implements Reference<T, U, N> {
|
|
27
|
-
// We keep both a promise+loaded flag and not an actual `this.loaded = await load` because
|
|
28
|
-
// the value can become stale; we want to each `.get` call to repeatedly evaluate the latest value.
|
|
29
|
-
private loadPromise: Promise<void> | undefined;
|
|
30
|
-
private _isLoaded = false;
|
|
31
|
-
|
|
32
|
-
constructor(public entity: T, private opts: CustomReferenceOpts<T, U, N>) {
|
|
33
|
-
super();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get get(): U | N {
|
|
37
|
-
return this.doGet({ withDeleted: false });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get getWithDeleted(): U | N {
|
|
41
|
-
return this.doGet({ withDeleted: true });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get isLoaded(): boolean {
|
|
45
|
-
return this._isLoaded;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async load(opts?: { withDeleted?: boolean }): Promise<U | N> {
|
|
49
|
-
ensureNotDeleted(this.entity, { ignore: "pending" });
|
|
50
|
-
if (!this.isLoaded) {
|
|
51
|
-
if (this.loadPromise === undefined) {
|
|
52
|
-
this.loadPromise = this.opts.load(this.entity);
|
|
53
|
-
await this.loadPromise;
|
|
54
|
-
this.loadPromise = undefined;
|
|
55
|
-
this._isLoaded = true;
|
|
56
|
-
} else {
|
|
57
|
-
await this.loadPromise;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return this.doGet(opts);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
initializeForNewEntity(): void {
|
|
65
|
-
// Normally we flag relations as loaded if created on a new entity, however CustomReferences
|
|
66
|
-
// might require crawling N-layers down from our initial opts, i.e. if creating a BookReview
|
|
67
|
-
// with a book, accessing review.author maybe not necessarily be safe to do immediately b/c
|
|
68
|
-
// we need to load book.author to successfully evaluated review -> book -> author synchronously.
|
|
69
|
-
// this._isLoaded = true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
get id(): IdOf<U> | undefined {
|
|
73
|
-
return fail(`CustomReference cannot resolve 'id'`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
get idOrFail(): IdOf<U> {
|
|
77
|
-
return fail(`CustomReference cannot resolve 'idOrFail'`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
get idUntagged(): string | undefined {
|
|
81
|
-
// We don't know the meta here but that is probably a feature in case this is polymorphic
|
|
82
|
-
return this.id && unsafeDeTagIds([this.id])[0];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get idUntaggedOrFail(): string {
|
|
86
|
-
return this.idUntagged || fail("Reference is unset or assigned to a new entity");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
get isSet(): boolean {
|
|
90
|
-
ensureNewOrLoaded(this);
|
|
91
|
-
const { get } = this.opts;
|
|
92
|
-
return get(this.entity) !== undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
set(value: U): void {
|
|
96
|
-
ensureNewOrLoaded(this);
|
|
97
|
-
const { set } = this.opts;
|
|
98
|
-
if (set === undefined) {
|
|
99
|
-
throw new Error(`'set' not implemented on ${this}`);
|
|
100
|
-
}
|
|
101
|
-
set(this.entity, value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
setFromOpts(value: U): void {
|
|
105
|
-
this.set(value);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// these callbacks should be no-ops as they ought to be handled by the underlying relations
|
|
109
|
-
async onEntityDeletedAndFlushing(): Promise<void> {}
|
|
110
|
-
onEntityDelete(): void {}
|
|
111
|
-
async refreshIfLoaded(): Promise<void> {}
|
|
112
|
-
|
|
113
|
-
/** Finds this CustomReferences field name by looking in the entity for the key that we're assigned to. */
|
|
114
|
-
get fieldName(): string {
|
|
115
|
-
return Object.entries(this.entity).filter((e) => e[1] === this)[0][0];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
toString(): string {
|
|
119
|
-
return `CustomReference(entity: ${this.entity}, fieldName: ${this.fieldName})`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private doGet(opts?: { withDeleted?: boolean }): U | N {
|
|
123
|
-
ensureNotDeleted(this.entity, { ignore: "pending" });
|
|
124
|
-
ensureNewOrLoaded(this);
|
|
125
|
-
return this.filterDeleted(this.opts.get(this.entity), opts);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private filterDeleted(entity: U | N, opts?: { withDeleted?: boolean }): U | N {
|
|
129
|
-
return opts?.withDeleted === true || entity === undefined || !entity.isDeletedEntity ? entity : (undefined as N);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function ensureNewOrLoaded(reference: CustomReference<any, any, any>) {
|
|
134
|
-
if (!(reference.isLoaded || reference.entity.isNewEntity)) {
|
|
135
|
-
// This should only be callable in the type system if we've already resolved this to an instance
|
|
136
|
-
throw new Error(`${reference.entity}.${reference.fieldName} was not loaded`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import DataLoader from "dataloader";
|
|
2
|
-
import {
|
|
3
|
-
Collection,
|
|
4
|
-
ensureNotDeleted,
|
|
5
|
-
Entity,
|
|
6
|
-
EntityMetadata,
|
|
7
|
-
getEm,
|
|
8
|
-
getMetadata,
|
|
9
|
-
IdOf,
|
|
10
|
-
keyToNumber,
|
|
11
|
-
keyToString,
|
|
12
|
-
} from "../";
|
|
13
|
-
import { getOrSet, remove } from "../utils";
|
|
14
|
-
import { AbstractRelationImpl } from "./AbstractRelationImpl";
|
|
15
|
-
|
|
16
|
-
export class ManyToManyCollection<T extends Entity, U extends Entity> extends AbstractRelationImpl<U[]>
|
|
17
|
-
implements Collection<T, U> {
|
|
18
|
-
private loaded: U[] | undefined;
|
|
19
|
-
private addedBeforeLoaded: U[] = [];
|
|
20
|
-
private removedBeforeLoaded: U[] = [];
|
|
21
|
-
private isCascadeDelete: boolean;
|
|
22
|
-
|
|
23
|
-
constructor(
|
|
24
|
-
public joinTableName: string,
|
|
25
|
-
// I.e. when entity = Book:
|
|
26
|
-
// fieldName == tags, because it's our collection to tags
|
|
27
|
-
// columnName = book_id, what we use as the `where book_id = us` to find our join table rows
|
|
28
|
-
// otherFieldName = books, how tags points to us
|
|
29
|
-
// otherColumnName = tag_id, how the other side finds its join table rows
|
|
30
|
-
public entity: T,
|
|
31
|
-
public fieldName: keyof T,
|
|
32
|
-
public columnName: string,
|
|
33
|
-
public otherMeta: EntityMetadata<U>,
|
|
34
|
-
public otherFieldName: keyof U,
|
|
35
|
-
public otherColumnName: string,
|
|
36
|
-
) {
|
|
37
|
-
super();
|
|
38
|
-
this.isCascadeDelete = otherMeta.config.__data.cascadeDeleteFields.includes(fieldName as any);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private filterDeleted(entities: U[], opts?: { withDeleted?: boolean }): U[] {
|
|
42
|
-
return opts?.withDeleted === true ? [...entities] : entities.filter((e) => !e.isDeletedEntity);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async load(opts?: { withDeleted?: boolean }): Promise<ReadonlyArray<U>> {
|
|
46
|
-
ensureNotDeleted(this.entity, { ignore: "pending" });
|
|
47
|
-
if (this.loaded === undefined) {
|
|
48
|
-
// TODO This key is basically a Reference, whenever we have that.
|
|
49
|
-
// TODO Unsaved entities should never get here
|
|
50
|
-
const key = `${this.columnName}=${this.entity.id}`;
|
|
51
|
-
this.loaded = await loaderForJoinTable(this).load(key);
|
|
52
|
-
this.maybeApplyAddedAndRemovedBeforeLoaded();
|
|
53
|
-
}
|
|
54
|
-
return this.filterDeleted(this.loaded!, opts) as ReadonlyArray<U>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async find(id: IdOf<U>): Promise<U | undefined> {
|
|
58
|
-
return (await this.load()).find((u) => u.id === id);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
add(other: U, percolated = false): void {
|
|
62
|
-
ensureNotDeleted(this.entity);
|
|
63
|
-
|
|
64
|
-
if (this.loaded !== undefined) {
|
|
65
|
-
if (this.loaded.includes(other)) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
this.loaded.push(other);
|
|
69
|
-
} else {
|
|
70
|
-
if (this.addedBeforeLoaded.includes(other)) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
this.addedBeforeLoaded.push(other);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!percolated) {
|
|
77
|
-
const joinRow: JoinRow = {
|
|
78
|
-
id: undefined,
|
|
79
|
-
m2m: this,
|
|
80
|
-
[this.columnName]: this.entity,
|
|
81
|
-
[this.otherColumnName]: other,
|
|
82
|
-
};
|
|
83
|
-
getOrSet(getEm(this.entity).__data.joinRows, this.joinTableName, []).push(joinRow);
|
|
84
|
-
((other[this.otherFieldName] as any) as ManyToManyCollection<U, T>).add(this.entity, true);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
remove(other: U, percolated = false): void {
|
|
89
|
-
ensureNotDeleted(this.entity, { ignore: "pending" });
|
|
90
|
-
|
|
91
|
-
if (!percolated) {
|
|
92
|
-
const joinRows = getOrSet(getEm(this.entity).__data.joinRows, this.joinTableName, []);
|
|
93
|
-
const row = joinRows.find((r) => r[this.columnName] === this.entity && r[this.otherColumnName] === other);
|
|
94
|
-
if (row) {
|
|
95
|
-
row.deleted = true;
|
|
96
|
-
} else {
|
|
97
|
-
const joinRow: JoinRow = {
|
|
98
|
-
// Use -1 to force the sortJoinRows to notice us as dirty ("delete: true but id is set")
|
|
99
|
-
id: -1,
|
|
100
|
-
m2m: this,
|
|
101
|
-
[this.columnName]: this.entity,
|
|
102
|
-
[this.otherColumnName]: other,
|
|
103
|
-
deleted: true,
|
|
104
|
-
};
|
|
105
|
-
getOrSet(getEm(this.entity).__data.joinRows, this.joinTableName, []).push(joinRow);
|
|
106
|
-
}
|
|
107
|
-
((other[this.otherFieldName] as any) as ManyToManyCollection<U, T>).remove(this.entity, true);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (this.loaded !== undefined) {
|
|
111
|
-
remove(this.loaded, other);
|
|
112
|
-
} else {
|
|
113
|
-
remove(this.addedBeforeLoaded, other);
|
|
114
|
-
this.removedBeforeLoaded.push(other);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private doGet(): U[] {
|
|
119
|
-
ensureNotDeleted(this.entity);
|
|
120
|
-
if (this.loaded === undefined) {
|
|
121
|
-
if (this.entity.id === undefined) {
|
|
122
|
-
return this.addedBeforeLoaded;
|
|
123
|
-
} else {
|
|
124
|
-
// This should only be callable in the type system if we've already resolved this to an instance
|
|
125
|
-
throw new Error("get was called when not loaded");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return this.loaded;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
get getWithDeleted(): U[] {
|
|
132
|
-
return this.filterDeleted(this.doGet(), { withDeleted: true });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
get get(): U[] {
|
|
136
|
-
return this.filterDeleted(this.doGet(), { withDeleted: false });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
set(values: U[]): void {
|
|
140
|
-
ensureNotDeleted(this.entity);
|
|
141
|
-
if (this.loaded === undefined) {
|
|
142
|
-
throw new Error("set was called when not loaded");
|
|
143
|
-
}
|
|
144
|
-
// Make a copy for safe iteration
|
|
145
|
-
const loaded = [...this.loaded];
|
|
146
|
-
// Remove old values
|
|
147
|
-
for (const other of loaded) {
|
|
148
|
-
if (!values.includes(other)) {
|
|
149
|
-
this.remove(other);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
for (const other of values) {
|
|
153
|
-
if (!loaded.includes(other)) {
|
|
154
|
-
this.add(other);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
removeAll(): void {
|
|
160
|
-
ensureNotDeleted(this.entity);
|
|
161
|
-
if (this.loaded === undefined) {
|
|
162
|
-
throw new Error("removeAll was called when not loaded");
|
|
163
|
-
}
|
|
164
|
-
for (const other of [...this.loaded]) {
|
|
165
|
-
this.remove(other);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// impl details
|
|
170
|
-
|
|
171
|
-
setFromOpts(others: U[]): void {
|
|
172
|
-
this.loaded = [];
|
|
173
|
-
others.forEach((o) => this.add(o));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
initializeForNewEntity(): void {
|
|
177
|
-
// Don't overwrite any opts values
|
|
178
|
-
if (this.loaded === undefined) {
|
|
179
|
-
this.loaded = [];
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async refreshIfLoaded(): Promise<void> {
|
|
184
|
-
ensureNotDeleted(this.entity);
|
|
185
|
-
// TODO We should remember what load hints have been applied to this collection and re-apply them.
|
|
186
|
-
if (this.loaded !== undefined && this.entity.id !== undefined) {
|
|
187
|
-
const key = `${this.columnName}=${this.entity.id}`;
|
|
188
|
-
const loader = loaderForJoinTable(this);
|
|
189
|
-
loader.clear(key);
|
|
190
|
-
this.loaded = await loader.load(key);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
onEntityDelete() {
|
|
195
|
-
if (this.isCascadeDelete) {
|
|
196
|
-
this.current({ withDeleted: true }).forEach(getEm(this.entity).delete);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async onEntityDeletedAndFlushing(): Promise<void> {
|
|
201
|
-
const entities = await this.load({ withDeleted: true });
|
|
202
|
-
entities.forEach((other) => {
|
|
203
|
-
const m2m = (other[this.otherFieldName] as any) as ManyToManyCollection<U, T>;
|
|
204
|
-
m2m.remove(this.entity);
|
|
205
|
-
});
|
|
206
|
-
this.loaded = [];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private maybeApplyAddedAndRemovedBeforeLoaded(): void {
|
|
210
|
-
if (this.loaded) {
|
|
211
|
-
// this.loaded.unshift(...this.addedBeforeLoaded);
|
|
212
|
-
// this.addedBeforeLoaded = [];
|
|
213
|
-
this.removedBeforeLoaded.forEach((e) => {
|
|
214
|
-
remove(this.loaded!, e);
|
|
215
|
-
const em = getEm(this.entity);
|
|
216
|
-
const row = em.__data.joinRows[this.joinTableName].find(
|
|
217
|
-
(r) => r[this.columnName] === this.entity && r[this.otherColumnName] === e,
|
|
218
|
-
);
|
|
219
|
-
if (row) {
|
|
220
|
-
row.deleted = true;
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
this.removedBeforeLoaded = [];
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
current(opts?: { withDeleted?: boolean }): U[] {
|
|
228
|
-
return this.filterDeleted(this.loaded || this.addedBeforeLoaded, opts);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
public get meta(): EntityMetadata<T> {
|
|
232
|
-
return getMetadata(this.entity);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
public toString(): string {
|
|
236
|
-
return `OneToManyCollection(entity: ${this.entity}, fieldName: ${this.fieldName}, otherType: ${this.otherMeta.type}, otherFieldName: ${this.otherFieldName})`;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export type JoinRow = {
|
|
241
|
-
id: number | undefined;
|
|
242
|
-
m2m: ManyToManyCollection<any, any>;
|
|
243
|
-
created_at?: Date;
|
|
244
|
-
[column: string]: number | Entity | undefined | boolean | Date | ManyToManyCollection<any, any>;
|
|
245
|
-
deleted?: boolean;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
function loaderForJoinTable<T extends Entity, U extends Entity>(collection: ManyToManyCollection<T, U>) {
|
|
249
|
-
const { joinTableName } = collection;
|
|
250
|
-
const em = getEm(collection.entity);
|
|
251
|
-
return getOrSet(em.__data.loaders, joinTableName, () => {
|
|
252
|
-
return new DataLoader<string, Entity[]>(async (keys) => loadFromJoinTable(collection, keys));
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Loads join rows (batched).
|
|
258
|
-
*
|
|
259
|
-
* I.e. we can load the `books_to_tags` join rows for multiple `Book`s at a time, or even
|
|
260
|
-
* load `books_to_tags` for several `Book`s and several `Tag`s in a single SQL query.
|
|
261
|
-
*/
|
|
262
|
-
async function loadFromJoinTable<T extends Entity, U extends Entity>(
|
|
263
|
-
collection: ManyToManyCollection<T, U>,
|
|
264
|
-
keys: ReadonlyArray<string>,
|
|
265
|
-
): Promise<Entity[][]> {
|
|
266
|
-
const { joinTableName } = collection;
|
|
267
|
-
const em = getEm(collection.entity);
|
|
268
|
-
|
|
269
|
-
// Break out `column_id=string` keys out
|
|
270
|
-
const columns: Record<string, string[]> = {};
|
|
271
|
-
keys.forEach((key) => {
|
|
272
|
-
const [columnId, id] = key.split("=");
|
|
273
|
-
getOrSet(columns, columnId, []).push(id);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Or together `where tag_id in (...)` and `book_id in (...)`
|
|
277
|
-
let query = em.knex.select("*").from(joinTableName);
|
|
278
|
-
Object.entries(columns).forEach(([columnId, values]) => {
|
|
279
|
-
// Pick the right meta i.e. tag_id --> TagMeta or book_id --> BookMeta
|
|
280
|
-
const meta = collection.columnName == columnId ? getMetadata(collection.entity) : collection.otherMeta;
|
|
281
|
-
query = query.orWhereIn(
|
|
282
|
-
columnId,
|
|
283
|
-
values.map((id) => keyToNumber(meta, id)!),
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const rows: JoinRow[] = await query.orderBy("id");
|
|
288
|
-
|
|
289
|
-
// Make a map that will be both `tag_id=2 -> [...]` and `book_id=3 -> [...]`
|
|
290
|
-
const rowsByKey: Record<string, JoinRow[]> = {};
|
|
291
|
-
|
|
292
|
-
// Keep a reference to our row to track updates/deletes
|
|
293
|
-
const emJoinRows = getOrSet(em.__data.joinRows, joinTableName, []);
|
|
294
|
-
|
|
295
|
-
// The order of column1/column2 doesn't really matter, i.e. if the opposite-side collection is later used
|
|
296
|
-
const column1 = collection.columnName;
|
|
297
|
-
const meta1 = collection.entity.__orm.metadata;
|
|
298
|
-
const column2 = collection.otherColumnName;
|
|
299
|
-
const meta2 = collection.otherMeta;
|
|
300
|
-
|
|
301
|
-
// For each join table row, we use `EntityManager.load` to get both entities loaded.
|
|
302
|
-
// This will be another 1 or 2 queries (depending on whether we're loading just
|
|
303
|
-
// `book.getTags` (1 query to load new tags) or both `book.getTags` and `tag.getBooks
|
|
304
|
-
// (1 query to load the new tags and 1 query to look the new books)).
|
|
305
|
-
//
|
|
306
|
-
// Eventually we could have this query join into the entity tables themselves, i.e.
|
|
307
|
-
// `books` and `tags`, and use those results to hydrate the newly-found entities.
|
|
308
|
-
await Promise.all(
|
|
309
|
-
rows.map(async (dbRow) => {
|
|
310
|
-
// We may have already loaded this join row in a prior load of the opposite side of this m2m.
|
|
311
|
-
let emRow = emJoinRows.find((jr) => {
|
|
312
|
-
return (
|
|
313
|
-
(jr[column1] as Entity).id === keyToString(meta1, dbRow[column1]) &&
|
|
314
|
-
(jr[column2] as Entity).id === keyToString(meta2, dbRow[column2])
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
if (!emRow) {
|
|
319
|
-
// For this join table row, load the entities of both foreign keys. Because we are `EntityManager.load`,
|
|
320
|
-
// this is N+1 safe (and will check the Unit of Work for already-loaded entities), but per ^ comment
|
|
321
|
-
// we chould pull these from the row itself if we did a fancier join.
|
|
322
|
-
const p1 = em.load(meta1.cstr, keyToString(meta1, dbRow[column1])!);
|
|
323
|
-
const p2 = em.load(meta2.cstr, keyToString(meta2, dbRow[column2])!);
|
|
324
|
-
const [e1, e2] = await Promise.all([p1, p2]);
|
|
325
|
-
emRow = { id: dbRow.id, m2m: collection, [column1]: e1, [column2]: e2, created_at: dbRow.created_at };
|
|
326
|
-
emJoinRows.push(emRow);
|
|
327
|
-
} else {
|
|
328
|
-
// If a placeholder row was created while a ManyToManyCollection was unloaded, and we find it during
|
|
329
|
-
// a subsequent load/query, update its id to be what is in the database.
|
|
330
|
-
emRow.id = dbRow.id;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Put this row into the map for both join table columns, i.e. `book_id=2` and `tag_id=3`
|
|
334
|
-
getOrSet(rowsByKey, `${column1}=${(emRow[column1] as Entity).id}`, []).push(emRow);
|
|
335
|
-
getOrSet(rowsByKey, `${column2}=${(emRow[column2] as Entity).id}`, []).push(emRow);
|
|
336
|
-
}),
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
// Map the requested keys, i.e. book_id=2 back to "the tags for book 2".
|
|
340
|
-
return keys.map((key) => {
|
|
341
|
-
const [column] = key.split("=");
|
|
342
|
-
const joinRows = rowsByKey[key] || [];
|
|
343
|
-
const otherColumn = column === collection.columnName ? collection.otherColumnName : collection.columnName;
|
|
344
|
-
return joinRows.map((joinRow) => joinRow[otherColumn] as Entity);
|
|
345
|
-
});
|
|
346
|
-
}
|