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
package/src/EntityManager.ts
DELETED
|
@@ -1,1263 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
import DataLoader from "dataloader";
|
|
3
|
-
import Knex, { QueryBuilder } from "knex";
|
|
4
|
-
import hash from "object-hash";
|
|
5
|
-
import { AbstractRelationImpl } from "./collections/AbstractRelationImpl";
|
|
6
|
-
import { JoinRow } from "./collections/ManyToManyCollection";
|
|
7
|
-
import { createOrUpdatePartial } from "./createOrUpdatePartial";
|
|
8
|
-
import { flushEntities, flushJoinTables, getTodo, sortEntities, sortJoinRows, Todo } from "./EntityPersister";
|
|
9
|
-
import {
|
|
10
|
-
assertIdsAreTagged,
|
|
11
|
-
Collection,
|
|
12
|
-
ColumnSerde,
|
|
13
|
-
ConfigApi,
|
|
14
|
-
DeepPartialOrNull,
|
|
15
|
-
deTagIds,
|
|
16
|
-
EntityHook,
|
|
17
|
-
getEm,
|
|
18
|
-
keyToString,
|
|
19
|
-
LoadedCollection,
|
|
20
|
-
LoadedReference,
|
|
21
|
-
maybeResolveReferenceToId,
|
|
22
|
-
PartialOrNull,
|
|
23
|
-
Reference,
|
|
24
|
-
Relation,
|
|
25
|
-
setField,
|
|
26
|
-
setOpts,
|
|
27
|
-
tagIfNeeded,
|
|
28
|
-
ValidationError,
|
|
29
|
-
ValidationErrors,
|
|
30
|
-
} from "./index";
|
|
31
|
-
import { buildQuery, FilterAndSettings } from "./QueryBuilder";
|
|
32
|
-
import { fail, getOrSet, indexBy, NullOrDefinedOr } from "./utils";
|
|
33
|
-
|
|
34
|
-
export interface EntityConstructor<T> {
|
|
35
|
-
new (em: EntityManager, opts: any): T;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Return the `FooOpts` type a given `Foo` entity constructor. */
|
|
39
|
-
export type OptsOf<T> = T extends { __types: { optsType: infer O } } ? O : never;
|
|
40
|
-
|
|
41
|
-
export type OptIdsOf<T> = T extends { __types: { optIdsType: infer O } } ? O : never;
|
|
42
|
-
|
|
43
|
-
/** Return the `Foo` type for a given `Foo` entity constructor. */
|
|
44
|
-
export type EntityOf<C> = C extends new (em: EntityManager, opts: any) => infer T ? T : never;
|
|
45
|
-
|
|
46
|
-
/** Pulls the entity query type out of a given entity type T. */
|
|
47
|
-
export type FilterOf<T> = T extends { __types: { filterType: infer Q } } ? Q : never;
|
|
48
|
-
|
|
49
|
-
/** Pulls the entity GraphQL query type out of a given entity type T. */
|
|
50
|
-
export type GraphQLFilterOf<T> = T extends { __types: { gqlFilterType: infer Q } } ? Q : never;
|
|
51
|
-
|
|
52
|
-
/** Pulls the entity order type out of a given entity type T. */
|
|
53
|
-
export type OrderOf<T> = T extends { __types: { orderType: infer Q } } ? Q : never;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Returns the opts of the entity's `newEntity` factory method, as exists in the actual file.
|
|
57
|
-
*
|
|
58
|
-
* This is because `FactoryOpts` is a set of defaults, but the user can customize it if they want.
|
|
59
|
-
*/
|
|
60
|
-
export type ActualFactoryOpts<T> = T extends { __types: { factoryOptsType: infer Q } } ? Q : never;
|
|
61
|
-
|
|
62
|
-
/** Pulls the entity's id type out of a given entity type T. */
|
|
63
|
-
export type IdOf<T> = T extends { id: infer I | undefined } ? I : never;
|
|
64
|
-
|
|
65
|
-
/** The `__orm` metadata field we track on each instance. */
|
|
66
|
-
export interface EntityOrmField {
|
|
67
|
-
/** A point to our entity type's metadata. */
|
|
68
|
-
metadata: EntityMetadata<Entity>;
|
|
69
|
-
/** A bag for our primitives/fk column values. */
|
|
70
|
-
data: Record<any, any>;
|
|
71
|
-
/** A bag to keep the original values, lazily populated. */
|
|
72
|
-
originalData: Record<any, any>;
|
|
73
|
-
/** Whether our entity has been deleted or not. */
|
|
74
|
-
deleted?: "pending" | "deleted";
|
|
75
|
-
/** All entities must be associated to an `EntityManager` to handle lazy loading/etc. */
|
|
76
|
-
em: EntityManager;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export let currentlyInstantiatingEntity: Entity | undefined;
|
|
80
|
-
|
|
81
|
-
/** A marker/base interface for all of our entity types. */
|
|
82
|
-
export interface Entity {
|
|
83
|
-
id: string | undefined;
|
|
84
|
-
|
|
85
|
-
idOrFail: string;
|
|
86
|
-
|
|
87
|
-
__orm: EntityOrmField;
|
|
88
|
-
|
|
89
|
-
readonly isNewEntity: boolean;
|
|
90
|
-
|
|
91
|
-
readonly isDeletedEntity: boolean;
|
|
92
|
-
|
|
93
|
-
readonly isDirtyEntity: boolean;
|
|
94
|
-
|
|
95
|
-
readonly isPendingFlush: boolean;
|
|
96
|
-
|
|
97
|
-
readonly isPendingDelete: boolean;
|
|
98
|
-
|
|
99
|
-
set(opts: Partial<OptsOf<this>>): void;
|
|
100
|
-
|
|
101
|
-
setPartial(values: PartialOrNull<OptsOf<this>>): void;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Marks a given `T[P]` as the loaded/synchronous version of the collection. */
|
|
105
|
-
type MarkLoaded<T extends Entity, P, H = {}> = P extends Reference<T, infer U, infer N>
|
|
106
|
-
? LoadedReference<T, Loaded<U, H>, N>
|
|
107
|
-
: P extends Collection<T, infer U>
|
|
108
|
-
? LoadedCollection<T, Loaded<U, H>>
|
|
109
|
-
: unknown;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* A helper type for `New` that marks every `Reference` and `LoadedCollection` in `T` as loaded.
|
|
113
|
-
*
|
|
114
|
-
* We also look in opts `O` for the "`U`" type, i.e. the next level up/down in the graph,
|
|
115
|
-
* because the call site's opts may be using an also-marked loaded parent/child as an opt,
|
|
116
|
-
* so this will infer the type of that parent/child and use that for the `U` type.
|
|
117
|
-
*
|
|
118
|
-
* This means things like `entity.parent.get.grandParent.get` will work on the resulting
|
|
119
|
-
* type.
|
|
120
|
-
*
|
|
121
|
-
* Note that this is also purposefully broken out of `New` because of some weirdness
|
|
122
|
-
* around type narrowing that wasn't working when inlined into `New`.
|
|
123
|
-
*/
|
|
124
|
-
type MaybeUseOptsType<T extends Entity, O, K extends keyof T & keyof O> = O[K] extends NullOrDefinedOr<infer OK>
|
|
125
|
-
? OK extends Entity
|
|
126
|
-
? T[K] extends Reference<T, infer U, infer N>
|
|
127
|
-
? LoadedReference<T, OK, N>
|
|
128
|
-
: never
|
|
129
|
-
: OK extends Array<infer OU>
|
|
130
|
-
? OU extends Entity
|
|
131
|
-
? T[K] extends Collection<T, infer U>
|
|
132
|
-
? LoadedCollection<T, OU>
|
|
133
|
-
: never
|
|
134
|
-
: never
|
|
135
|
-
: T[K]
|
|
136
|
-
: never;
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Marks all references/collections of `T` as loaded, i.e. for newly instantiated entities where
|
|
140
|
-
* we know there are no already-existing rows with fk's to this new entity in the database.
|
|
141
|
-
*
|
|
142
|
-
* `O` is the generic from the call site so that if the caller passes `{ author: SomeLoadedAuthor }`,
|
|
143
|
-
* we'll prefer that type, as it might have more nested load hints that we can't otherwise assume.
|
|
144
|
-
*/
|
|
145
|
-
export type New<T extends Entity, O extends OptsOf<T> = OptsOf<T>> = T &
|
|
146
|
-
{
|
|
147
|
-
// K will be `keyof T` and `keyof O` for codegen'd relations, but custom relations
|
|
148
|
-
// line `hasOneThrough` and `hasOneDerived` will not pass `keyof O` and so use the
|
|
149
|
-
// `: MarkLoaded`.
|
|
150
|
-
//
|
|
151
|
-
// Note that the safest thing is to probably make this `: unknown` instead so that
|
|
152
|
-
// custom relations are not marked loaded, b/c they will very likely require a `.load`
|
|
153
|
-
// to work. However, we have some tests that currently expect `author.image.get` to work
|
|
154
|
-
// on a new author, so keeping the `MarkLoaded` behavior for now.
|
|
155
|
-
[K in keyof T]: K extends keyof O ? MaybeUseOptsType<T, O, K> : MarkLoaded<T, T[K]>;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
/** Given an entity `T` that is being populated with hints `H`, marks the `H` attributes as populated. */
|
|
159
|
-
export type Loaded<T extends Entity, H extends LoadHint<T>> = T &
|
|
160
|
-
{
|
|
161
|
-
[K in keyof T]: H extends NestedLoadHint<T>
|
|
162
|
-
? LoadedIfInNestedHint<T, K, H>
|
|
163
|
-
: H extends ReadonlyArray<infer U>
|
|
164
|
-
? LoadedIfInKeyHint<T, K, U>
|
|
165
|
-
: LoadedIfInKeyHint<T, K, H>;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// We can use unknown here because everything non-loaded is pulled in from `T &`
|
|
169
|
-
type LoadedIfInNestedHint<T extends Entity, K extends keyof T, H> = K extends keyof H
|
|
170
|
-
? MarkLoaded<T, T[K], H[K]>
|
|
171
|
-
: unknown;
|
|
172
|
-
|
|
173
|
-
type LoadedIfInKeyHint<T extends Entity, K extends keyof T, H> = K extends H ? MarkLoaded<T, T[K]> : unknown;
|
|
174
|
-
|
|
175
|
-
/** From any non-`Relations` field in `T`, i.e. for loader hints. */
|
|
176
|
-
export type RelationsIn<T extends Entity> = SubType<T, Relation<any, any>>;
|
|
177
|
-
|
|
178
|
-
// https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c
|
|
179
|
-
type SubType<T, C> = Pick<T, { [K in keyof T]: T[K] extends C ? K : never }[keyof T]>;
|
|
180
|
-
|
|
181
|
-
// We accept load hints as a string, or a string[], or a hash of { key: nested };
|
|
182
|
-
export type LoadHint<T extends Entity> = keyof RelationsIn<T> | ReadonlyArray<keyof RelationsIn<T>> | NestedLoadHint<T>;
|
|
183
|
-
|
|
184
|
-
type NestedLoadHint<T extends Entity> = {
|
|
185
|
-
[K in keyof RelationsIn<T>]?: T[K] extends Relation<T, infer U> ? LoadHint<U> : never;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export type LoaderCache = Record<string, DataLoader<any, any>>;
|
|
189
|
-
|
|
190
|
-
type MaybePromise<T> = T | PromiseLike<T>;
|
|
191
|
-
export type EntityManagerHook = "beforeTransaction";
|
|
192
|
-
type HookFn = (em: EntityManager, knex: Knex.Transaction) => MaybePromise<any>;
|
|
193
|
-
|
|
194
|
-
export type HasKnex = { knex: Knex };
|
|
195
|
-
|
|
196
|
-
export const currentFlushSecret = new AsyncLocalStorage<{ flushSecret: number }>();
|
|
197
|
-
|
|
198
|
-
export class EntityManager<C extends HasKnex = HasKnex> {
|
|
199
|
-
private readonly ctx: C;
|
|
200
|
-
public knex: Knex;
|
|
201
|
-
|
|
202
|
-
constructor(em: EntityManager<C>);
|
|
203
|
-
constructor(ctx: C);
|
|
204
|
-
constructor(emOrCtx: EntityManager<C> | C) {
|
|
205
|
-
if (emOrCtx instanceof EntityManager) {
|
|
206
|
-
const em = emOrCtx;
|
|
207
|
-
this.knex = em.knex;
|
|
208
|
-
this.hooks = { beforeTransaction: [...em.hooks.beforeTransaction] };
|
|
209
|
-
this.ctx = em.ctx!;
|
|
210
|
-
} else {
|
|
211
|
-
this.ctx = emOrCtx!;
|
|
212
|
-
this.knex = emOrCtx.knex;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private _entities: Entity[] = [];
|
|
217
|
-
// Indexes the currently loaded entities by their tagged ids. This fixes a real-world
|
|
218
|
-
// performance issue where `findExistingInstance` scanning `_entities` was an `O(n^2)`.
|
|
219
|
-
private _entityIndex: Map<string, Entity> = new Map();
|
|
220
|
-
private findLoaders: LoaderCache = {};
|
|
221
|
-
private flushSecret: number = 0;
|
|
222
|
-
private _isFlushing: boolean = false;
|
|
223
|
-
// This is attempting to be internal/module private
|
|
224
|
-
__data = {
|
|
225
|
-
loaders: {} as LoaderCache,
|
|
226
|
-
joinRows: {} as Record<string, JoinRow[]>,
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
private hooks: Record<EntityManagerHook, HookFn[]> = {
|
|
230
|
-
beforeTransaction: [],
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
get entities(): ReadonlyArray<Entity> {
|
|
234
|
-
return [...this._entities];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
public async find<T extends Entity>(type: EntityConstructor<T>, where: FilterOf<T>): Promise<T[]>;
|
|
238
|
-
public async find<
|
|
239
|
-
T extends Entity,
|
|
240
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
241
|
-
N extends Narrowable
|
|
242
|
-
>(
|
|
243
|
-
type: EntityConstructor<T>,
|
|
244
|
-
where: FilterOf<T>,
|
|
245
|
-
options?: { populate?: H; orderBy?: OrderOf<T>; limit?: number; offset?: number },
|
|
246
|
-
): Promise<Loaded<T, H>[]>;
|
|
247
|
-
async find<T extends Entity>(
|
|
248
|
-
type: EntityConstructor<T>,
|
|
249
|
-
where: FilterOf<T>,
|
|
250
|
-
options?: { populate?: any; orderBy?: OrderOf<T>; limit?: number; offset?: number },
|
|
251
|
-
): Promise<T[]> {
|
|
252
|
-
const rows = await this.loaderForFind(type).load({ where, ...options });
|
|
253
|
-
const result = rows.map((row: any) => this.hydrate(type, row, { overwriteExisting: false }));
|
|
254
|
-
if (options?.populate) {
|
|
255
|
-
await this.populate(result, options.populate);
|
|
256
|
-
}
|
|
257
|
-
return result;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Works exactly like `find` but accepts "less than greatly typed" GraphQL filters.
|
|
262
|
-
*
|
|
263
|
-
* I.e. filtering by `null` on fields that are non-`nullable`.
|
|
264
|
-
*/
|
|
265
|
-
public async findGql<T extends Entity>(type: EntityConstructor<T>, where: GraphQLFilterOf<T>): Promise<T[]>;
|
|
266
|
-
public async findGql<
|
|
267
|
-
T extends Entity,
|
|
268
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
269
|
-
N extends Narrowable
|
|
270
|
-
>(
|
|
271
|
-
type: EntityConstructor<T>,
|
|
272
|
-
where: GraphQLFilterOf<T>,
|
|
273
|
-
options?: { populate?: H; orderBy?: OrderOf<T>; limit?: number; offset?: number },
|
|
274
|
-
): Promise<Loaded<T, H>[]>;
|
|
275
|
-
async findGql<T extends Entity>(
|
|
276
|
-
type: EntityConstructor<T>,
|
|
277
|
-
where: FilterOf<T>,
|
|
278
|
-
options?: { populate?: any; orderBy?: OrderOf<T>; limit?: number; offset?: number },
|
|
279
|
-
): Promise<T[]> {
|
|
280
|
-
const rows = await this.loaderForFind(type).load({ where, ...options });
|
|
281
|
-
const result = rows.map((row: any) => this.hydrate(type, row, { overwriteExisting: false }));
|
|
282
|
-
if (options?.populate) {
|
|
283
|
-
await this.populate(result, options.populate);
|
|
284
|
-
}
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
public async findOne<T extends Entity>(type: EntityConstructor<T>, where: FilterOf<T>): Promise<T | undefined>;
|
|
289
|
-
public async findOne<
|
|
290
|
-
T extends Entity,
|
|
291
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
292
|
-
N extends Narrowable
|
|
293
|
-
>(type: EntityConstructor<T>, where: FilterOf<T>, options?: { populate: H }): Promise<Loaded<T, H> | undefined>;
|
|
294
|
-
async findOne<T extends Entity>(
|
|
295
|
-
type: EntityConstructor<T>,
|
|
296
|
-
where: FilterOf<T>,
|
|
297
|
-
options?: { populate: any },
|
|
298
|
-
): Promise<T | undefined> {
|
|
299
|
-
const list = await this.find(type, where, options);
|
|
300
|
-
if (list.length === 0) {
|
|
301
|
-
return undefined;
|
|
302
|
-
} else if (list.length === 1) {
|
|
303
|
-
return list[0];
|
|
304
|
-
} else {
|
|
305
|
-
throw new TooManyError(`Found more than one: ${list.map((e) => e.toString()).join(", ")}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Executes a given query filter and returns exactly one result, otherwise throws `NotFoundError` or `TooManyError`. */
|
|
310
|
-
public async findOneOrFail<T extends Entity>(type: EntityConstructor<T>, where: FilterOf<T>): Promise<T>;
|
|
311
|
-
public async findOneOrFail<
|
|
312
|
-
T extends Entity,
|
|
313
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
314
|
-
N extends Narrowable
|
|
315
|
-
>(type: EntityConstructor<T>, where: FilterOf<T>, options: { populate: H }): Promise<Loaded<T, H>>;
|
|
316
|
-
async findOneOrFail<T extends Entity>(
|
|
317
|
-
type: EntityConstructor<T>,
|
|
318
|
-
where: FilterOf<T>,
|
|
319
|
-
options?: { populate: any },
|
|
320
|
-
): Promise<T> {
|
|
321
|
-
const list = await this.find(type, where, options);
|
|
322
|
-
if (list.length === 0) {
|
|
323
|
-
throw new NotFoundError(`Did not find ${type.name} for given query`);
|
|
324
|
-
} else if (list.length > 1) {
|
|
325
|
-
throw new TooManyError(`Found more than one: ${list.map((e) => e.toString()).join(", ")}`);
|
|
326
|
-
}
|
|
327
|
-
return list[0];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Conditionally finds or creates an Entity.
|
|
332
|
-
*
|
|
333
|
-
* The types work out where the `where` + `ifNewOpts` are both subsets of the entity's `Opts`
|
|
334
|
-
* type, i.e. if we have to create the entity, the combintaion of `where` + `ifNewOpts` will
|
|
335
|
-
* have all of the necessary required fields.
|
|
336
|
-
*
|
|
337
|
-
* @param type the entity type to find/create
|
|
338
|
-
* @param where the fields to look up the existing entity by
|
|
339
|
-
* @param ifNew the fields to set if the entity is new
|
|
340
|
-
* @param upsert the fields to update if the entity is either existing or new
|
|
341
|
-
*/
|
|
342
|
-
async findOrCreate<
|
|
343
|
-
T extends Entity,
|
|
344
|
-
F extends Partial<OptsOf<T>>,
|
|
345
|
-
U extends Partial<OptsOf<T>> | {},
|
|
346
|
-
O extends Omit<OptsOf<T>, keyof F | keyof U>
|
|
347
|
-
>(type: EntityConstructor<T>, where: F, ifNew: O, upsert?: U): Promise<T>;
|
|
348
|
-
async findOrCreate<
|
|
349
|
-
T extends Entity,
|
|
350
|
-
F extends Partial<OptsOf<T>>,
|
|
351
|
-
U extends Partial<OptsOf<T>> | {},
|
|
352
|
-
O extends Omit<OptsOf<T>, keyof F | keyof U>,
|
|
353
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
354
|
-
N extends Narrowable
|
|
355
|
-
>(type: EntityConstructor<T>, where: F, ifNew: O, upsert?: U, populate?: H): Promise<Loaded<T, H>>;
|
|
356
|
-
async findOrCreate<
|
|
357
|
-
T extends Entity,
|
|
358
|
-
F extends Partial<OptsOf<T>>,
|
|
359
|
-
U extends Partial<OptsOf<T>> | {},
|
|
360
|
-
O extends Omit<OptsOf<T>, keyof F | keyof U>,
|
|
361
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
362
|
-
N extends Narrowable
|
|
363
|
-
>(type: EntityConstructor<T>, where: F, ifNew: O, upsert?: U, populate?: H): Promise<T> {
|
|
364
|
-
const entities = await this.find(type, where as FilterOf<T>);
|
|
365
|
-
let entity: T;
|
|
366
|
-
if (entities.length > 1) {
|
|
367
|
-
throw new TooManyError();
|
|
368
|
-
} else if (entities.length === 1) {
|
|
369
|
-
entity = entities[0];
|
|
370
|
-
} else {
|
|
371
|
-
entity = this.create(type, { ...where, ...ifNew } as OptsOf<T>);
|
|
372
|
-
}
|
|
373
|
-
if (upsert) {
|
|
374
|
-
entity.set(upsert);
|
|
375
|
-
}
|
|
376
|
-
if (populate) {
|
|
377
|
-
await this.populate(entity, populate);
|
|
378
|
-
}
|
|
379
|
-
return entity;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Creates a new `type` and marks it as loaded, i.e. we know its collections are all safe to access in memory. */
|
|
383
|
-
public create<T extends Entity, O extends OptsOf<T>>(type: EntityConstructor<T>, opts: O): New<T, O> {
|
|
384
|
-
// The constructor will run setOpts which handles defaulting collections to the right state.
|
|
385
|
-
return new type(this, opts) as New<T, O>;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** Creates a new `type` but with `opts` that are nullable, to accept partial-update-style input. */
|
|
389
|
-
public createPartial<T extends Entity>(type: EntityConstructor<T>, opts: PartialOrNull<OptsOf<T>>): T {
|
|
390
|
-
// We force some manual calls to setOpts to mimic `setUnsafe`'s behavior that `undefined` should
|
|
391
|
-
// mean "ignore" (and we assume validation rules will catch it later) but still set
|
|
392
|
-
// `calledFromConstructor` because this is _basically_ like calling `new`.
|
|
393
|
-
const entity = new type(this, undefined!);
|
|
394
|
-
// Could remove the `as OptsOf<T>` by adding a method overload on `partial: true`
|
|
395
|
-
setOpts(entity, opts as OptsOf<T>, { partial: true, calledFromConstructor: true });
|
|
396
|
-
return entity;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** Creates a new `type` but with `opts` that are nullable, to accept partial-update-style input. */
|
|
400
|
-
public createOrUpdatePartial<T extends Entity>(type: EntityConstructor<T>, opts: DeepPartialOrNull<T>): Promise<T> {
|
|
401
|
-
return createOrUpdatePartial(this, type, opts);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/** Returns an instance of `type` for the given `id`, resolving to an existing instance if in our Unit of Work. */
|
|
405
|
-
public async load<T extends Entity>(type: EntityConstructor<T>, id: string): Promise<T>;
|
|
406
|
-
public async load<T extends Entity, H extends LoadHint<T> & { [k: string]: N | T | [] }, N extends Narrowable>(
|
|
407
|
-
type: EntityConstructor<T>,
|
|
408
|
-
id: string,
|
|
409
|
-
populate: H,
|
|
410
|
-
): Promise<Loaded<T, H>>;
|
|
411
|
-
public async load<T extends Entity, H extends LoadHint<T> & (N | N[]), N extends Narrowable>(
|
|
412
|
-
type: EntityConstructor<T>,
|
|
413
|
-
id: string,
|
|
414
|
-
populate: H,
|
|
415
|
-
): Promise<Loaded<T, H>>;
|
|
416
|
-
async load<T extends Entity>(type: EntityConstructor<T>, id: string, hint?: any): Promise<T> {
|
|
417
|
-
if (typeof (id as any) !== "string") {
|
|
418
|
-
throw new Error(`Expected ${id} to be a string`);
|
|
419
|
-
}
|
|
420
|
-
const meta = getMetadata(type);
|
|
421
|
-
const tagged = tagIfNeeded(meta, id);
|
|
422
|
-
const entity = this.findExistingInstance<T>(tagged) || (await this.loaderForEntity(meta).load(tagged));
|
|
423
|
-
if (!entity) {
|
|
424
|
-
throw new Error(`${tagged} was not found`);
|
|
425
|
-
}
|
|
426
|
-
if (hint) {
|
|
427
|
-
await this.populate(entity, hint);
|
|
428
|
-
}
|
|
429
|
-
return entity;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/** Returns instances of `type` for the given `ids`, resolving to an existing instance if in our Unit of Work. */
|
|
433
|
-
public async loadAll<T extends Entity>(type: EntityConstructor<T>, ids: string[]): Promise<T[]>;
|
|
434
|
-
public async loadAll<
|
|
435
|
-
T extends Entity,
|
|
436
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
437
|
-
N extends Narrowable
|
|
438
|
-
>(type: EntityConstructor<T>, ids: string[], populate: H): Promise<Loaded<T, H>[]>;
|
|
439
|
-
async loadAll<T extends Entity>(type: EntityConstructor<T>, _ids: string[], hint?: any): Promise<T[]> {
|
|
440
|
-
const meta = getMetadata(type);
|
|
441
|
-
const ids = _ids.map((id) => tagIfNeeded(meta, id));
|
|
442
|
-
const entities = await Promise.all(
|
|
443
|
-
ids.map((id) => {
|
|
444
|
-
return this.findExistingInstance(id) || this.loaderForEntity(meta).load(id);
|
|
445
|
-
}),
|
|
446
|
-
);
|
|
447
|
-
const idsNotFound = ids.filter((id, i) => entities[i] === undefined);
|
|
448
|
-
if (idsNotFound.length > 0) {
|
|
449
|
-
throw new Error(`${idsNotFound.join(",")} were not found`);
|
|
450
|
-
}
|
|
451
|
-
if (hint) {
|
|
452
|
-
await this.populate(entities as T[], hint);
|
|
453
|
-
}
|
|
454
|
-
return entities as T[];
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Loads entities from a knex QueryBuilder. */
|
|
458
|
-
public async loadFromQuery<T extends Entity>(type: EntityConstructor<T>, query: QueryBuilder): Promise<T[]>;
|
|
459
|
-
public async loadFromQuery<T extends Entity, H extends LoadHint<T>>(
|
|
460
|
-
type: EntityConstructor<T>,
|
|
461
|
-
query: QueryBuilder,
|
|
462
|
-
populate: H,
|
|
463
|
-
): Promise<Loaded<T, H>[]>;
|
|
464
|
-
public async loadFromQuery<T extends Entity>(
|
|
465
|
-
type: EntityConstructor<T>,
|
|
466
|
-
query: QueryBuilder,
|
|
467
|
-
populate?: any,
|
|
468
|
-
): Promise<T[]> {
|
|
469
|
-
const rows = await query;
|
|
470
|
-
const entities = rows.map((row: any) => this.hydrate(type, row, { overwriteExisting: false }));
|
|
471
|
-
if (populate) {
|
|
472
|
-
await this.populate(entities, populate);
|
|
473
|
-
}
|
|
474
|
-
return entities;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/** Given a hint `H` (a field, array of fields, or nested hash), pre-load that data into `entity` for sync access. */
|
|
478
|
-
public async populate<
|
|
479
|
-
T extends Entity,
|
|
480
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
481
|
-
N extends Narrowable
|
|
482
|
-
>(entity: T, hint: H): Promise<Loaded<T, H>>;
|
|
483
|
-
public async populate<
|
|
484
|
-
T extends Entity,
|
|
485
|
-
H extends LoadHint<T> & ({ [k: string]: N | H | [] } | N | N[]),
|
|
486
|
-
N extends Narrowable
|
|
487
|
-
>(entities: ReadonlyArray<T>, hint: H): Promise<Loaded<T, H>[]>;
|
|
488
|
-
async populate<T extends Entity, H extends LoadHint<T>>(
|
|
489
|
-
entityOrList: T | T[],
|
|
490
|
-
hint: H,
|
|
491
|
-
): Promise<Loaded<T, H> | Array<Loaded<T, H>>> {
|
|
492
|
-
const list: T[] = Array.isArray(entityOrList) ? entityOrList : [entityOrList];
|
|
493
|
-
const promises = list
|
|
494
|
-
.filter((e) => e !== undefined && (e.isPendingDelete || !e.isDeletedEntity))
|
|
495
|
-
.flatMap((entity) => {
|
|
496
|
-
// This implementation is pretty simple b/c we just loop over the hint (which is a key / array of keys /
|
|
497
|
-
// hash of keys) and call `.load()` on the corresponding o2m/m2o/m2m reference/collection object. This
|
|
498
|
-
// will kick in the dataloader auto-batching and end up being smartly populated (granted via 1 query per
|
|
499
|
-
// entity type per "level" of resolution, instead of 1 single giant SQL query that inner joins everything
|
|
500
|
-
// in).
|
|
501
|
-
if (typeof hint === "string") {
|
|
502
|
-
return (entity as any)[hint].load();
|
|
503
|
-
} else if (Array.isArray(hint)) {
|
|
504
|
-
return (hint as string[]).map((key) => (entity as any)[key].load());
|
|
505
|
-
} else if (typeof hint === "object") {
|
|
506
|
-
return Object.entries(hint as object).map(async ([key, nestedHint]) => {
|
|
507
|
-
const relation = (entity as any)[key];
|
|
508
|
-
const result = await relation.load();
|
|
509
|
-
return this.populate(result, nestedHint);
|
|
510
|
-
});
|
|
511
|
-
} else {
|
|
512
|
-
throw new Error(`Unexpected hint ${hint}`);
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
await Promise.all(promises);
|
|
516
|
-
return entityOrList as any;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Executes `fn` with a transaction, and automatically calls `flush`/`commit` at the end.
|
|
521
|
-
*
|
|
522
|
-
* This ensures both any `.find` as well as `.flush` operations happen within the same
|
|
523
|
-
* transaction, which is useful for enforcing cross-table/application-level invariants that
|
|
524
|
-
* cannot be enforced with database-level constraints.
|
|
525
|
-
*/
|
|
526
|
-
public async transaction<T>(fn: (txn: Knex.Transaction) => Promise<T>): Promise<T> {
|
|
527
|
-
const originalKnex = this.knex;
|
|
528
|
-
const txn = await this.knex.transaction();
|
|
529
|
-
this.knex = txn;
|
|
530
|
-
try {
|
|
531
|
-
await txn.raw("set transaction isolation level serializable;");
|
|
532
|
-
await beforeTransaction(this, txn);
|
|
533
|
-
const result = await fn(txn);
|
|
534
|
-
// The lambda may have done some interstitial flushes (that would not
|
|
535
|
-
// have committed the transaction), but go ahead and do a final one
|
|
536
|
-
// in case they didn't explicitly call flush.
|
|
537
|
-
await this.flush();
|
|
538
|
-
await txn.commit();
|
|
539
|
-
return result;
|
|
540
|
-
} finally {
|
|
541
|
-
if (!txn.isCompleted()) {
|
|
542
|
-
txn.rollback().catch((e) => {
|
|
543
|
-
console.error(e, "Error rolling back");
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
this.knex = originalKnex;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/** Registers a newly-instantiated entity with our EntityManager; only called by entity constructors. */
|
|
551
|
-
register(meta: EntityMetadata<any>, entity: Entity): void {
|
|
552
|
-
if (entity.id && this.findExistingInstance(entity.id) !== undefined) {
|
|
553
|
-
throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
|
|
554
|
-
}
|
|
555
|
-
// Set a default createdAt/updatedAt that we'll keep if this is a new entity, or over-write if we're loaded an existing row
|
|
556
|
-
entity.__orm.data["createdAt"] = new Date();
|
|
557
|
-
entity.__orm.data["updatedAt"] = new Date();
|
|
558
|
-
|
|
559
|
-
this._entities.push(entity);
|
|
560
|
-
if (entity.id) {
|
|
561
|
-
assertIdsAreTagged([entity.id]);
|
|
562
|
-
this._entityIndex.set(entity.id, entity);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (this._entities.length >= entityLimit) {
|
|
566
|
-
throw new Error(`More than ${entityLimit} entities have been instantiated`);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
currentlyInstantiatingEntity = entity;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Marks an instance to be deleted.
|
|
574
|
-
*
|
|
575
|
-
* Any loaded collections that are currently "pointing to" this entity will be updated to
|
|
576
|
-
* no longer include this entity, i.e. if you `em.delete(b1)`, then `author.books` will have
|
|
577
|
-
* `b1` removed (if needed).
|
|
578
|
-
*
|
|
579
|
-
* This is done for all currently-loaded collections; i.e. technically unloaded collections
|
|
580
|
-
* may still point to this entity. We defer unsetting these not-currently-loaded references
|
|
581
|
-
* until `EntityManager.flush`, when we can make the async calls to load-and-unset them.
|
|
582
|
-
*/
|
|
583
|
-
delete(deletedEntity: Entity): void {
|
|
584
|
-
// Early return if already deleted.
|
|
585
|
-
if (deletedEntity.__orm.deleted) {
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
deletedEntity.__orm.deleted = "pending";
|
|
589
|
-
|
|
590
|
-
Object.values(deletedEntity)
|
|
591
|
-
.filter((v) => v instanceof AbstractRelationImpl)
|
|
592
|
-
.map((relation: AbstractRelationImpl<any>) => {
|
|
593
|
-
relation.onEntityDelete();
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Flushes the SQL for any changed entities to the database.
|
|
599
|
-
*
|
|
600
|
-
* If this is run outside of an existing transaction, it will `BEGIN` and `COMMIT`
|
|
601
|
-
* a new transaction on every `.flush()` call so that all of the `INSERT`s/etc.
|
|
602
|
-
* happen atomically.
|
|
603
|
-
*
|
|
604
|
-
* If this is run within an existing transaction, i.e. `EntityManager.transaction`,
|
|
605
|
-
* then it will only issue `INSERT`s/etc. and defer to the caller to `COMMIT`
|
|
606
|
-
* the transaction.
|
|
607
|
-
*/
|
|
608
|
-
async flush(): Promise<void> {
|
|
609
|
-
if (this.isFlushing) {
|
|
610
|
-
throw new Error("Cannot flush while another flush is already in progress");
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
this._isFlushing = true;
|
|
614
|
-
|
|
615
|
-
const entitiesToFlush: Entity[] = [];
|
|
616
|
-
let pendingEntities = this.entities.filter((e) => e.isPendingFlush);
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
while (pendingEntities.length > 0) {
|
|
620
|
-
await new Promise((resolve, reject) => {
|
|
621
|
-
currentFlushSecret.run({ flushSecret: this.flushSecret }, async () => {
|
|
622
|
-
try {
|
|
623
|
-
const todos = sortEntities(pendingEntities);
|
|
624
|
-
|
|
625
|
-
// add objects to todos that have reactive hooks
|
|
626
|
-
await addReactiveAsyncDerivedValues(todos);
|
|
627
|
-
await addReactiveValidations(todos);
|
|
628
|
-
|
|
629
|
-
// run our hooks
|
|
630
|
-
await beforeDelete(this.ctx, todos);
|
|
631
|
-
// We defer doing this cascade logic until flush() so that delete() can remain synchronous.
|
|
632
|
-
await cascadeDeletesIntoRelations(todos);
|
|
633
|
-
await beforeFlush(this.ctx, todos);
|
|
634
|
-
recalcDerivedFields(todos);
|
|
635
|
-
await recalcAsyncDerivedFields(this, todos);
|
|
636
|
-
await validate(todos);
|
|
637
|
-
await afterValidation(this.ctx, todos);
|
|
638
|
-
|
|
639
|
-
entitiesToFlush.push(...pendingEntities);
|
|
640
|
-
pendingEntities = this.entities.filter((e) => e.isPendingFlush && !entitiesToFlush.includes(e));
|
|
641
|
-
this.flushSecret += 1;
|
|
642
|
-
|
|
643
|
-
resolve();
|
|
644
|
-
} catch (e) {
|
|
645
|
-
reject(e);
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const entityTodos = sortEntities(entitiesToFlush);
|
|
652
|
-
const joinRowTodos = sortJoinRows(this.__data.joinRows);
|
|
653
|
-
|
|
654
|
-
if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos).length > 0) {
|
|
655
|
-
const alreadyInTxn = "commit" in this.knex;
|
|
656
|
-
if (!alreadyInTxn) {
|
|
657
|
-
await this.knex.transaction(async (knex) => {
|
|
658
|
-
await beforeTransaction(this, knex);
|
|
659
|
-
await flushEntities(knex, entityTodos);
|
|
660
|
-
await flushJoinTables(knex, joinRowTodos);
|
|
661
|
-
// When using `.transaction` with a lambda, we don't explicitly call commit
|
|
662
|
-
// await knex.commit();
|
|
663
|
-
});
|
|
664
|
-
} else {
|
|
665
|
-
await flushEntities(this.knex, entityTodos);
|
|
666
|
-
await flushJoinTables(this.knex, joinRowTodos);
|
|
667
|
-
// Defer to the caller to commit the transaction
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// TODO: This is really "after flush" if we're being called from a transaction that
|
|
671
|
-
// is going to make multiple `em.flush()` calls?
|
|
672
|
-
await afterCommit(this.ctx, entityTodos);
|
|
673
|
-
|
|
674
|
-
Object.values(entityTodos).forEach((todo) => {
|
|
675
|
-
todo.inserts.forEach((e) => this._entityIndex.set(e.id!, e));
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// Reset the find caches b/c data will have changed in the db
|
|
679
|
-
this.findLoaders = {};
|
|
680
|
-
this.__data.loaders = {};
|
|
681
|
-
}
|
|
682
|
-
} finally {
|
|
683
|
-
this._isFlushing = false;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
get isFlushing(): boolean {
|
|
688
|
-
return this._isFlushing;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* A very simple toJSON.
|
|
693
|
-
*
|
|
694
|
-
* This is not really meant to be useful, it's to prevent huge/circular output if
|
|
695
|
-
* an EntityManager accidentally ends up getting logged to something like pino that
|
|
696
|
-
* over-zealous toJSONs anything it touches.
|
|
697
|
-
*/
|
|
698
|
-
public toJSON(): string {
|
|
699
|
-
return `<EntityManager ${this.entities.length}>`;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* For all entities in the current `EntityManager`, load their latest data from the database.
|
|
704
|
-
*
|
|
705
|
-
* This is primarily useful in tests, i.e. having 1 `EntityManager` with some test data, running business
|
|
706
|
-
* logic in a dedicated `EntityManager`, and then `refresh`-ing the test data `EntityManager` to assert
|
|
707
|
-
* against the latest values.
|
|
708
|
-
*
|
|
709
|
-
* This works with primitive fields as well as references and collections.
|
|
710
|
-
*
|
|
711
|
-
* TODO Newly-found collection entries will not have prior load hints applied to this.
|
|
712
|
-
*/
|
|
713
|
-
async refresh(): Promise<void>;
|
|
714
|
-
async refresh(entity: Entity): Promise<void>;
|
|
715
|
-
async refresh(entities: ReadonlyArray<Entity>): Promise<void>;
|
|
716
|
-
async refresh(entityOrListOrUndefined?: Entity | ReadonlyArray<Entity>): Promise<void> {
|
|
717
|
-
this.findLoaders = {};
|
|
718
|
-
const list =
|
|
719
|
-
entityOrListOrUndefined === undefined
|
|
720
|
-
? this._entities
|
|
721
|
-
: Array.isArray(entityOrListOrUndefined)
|
|
722
|
-
? entityOrListOrUndefined
|
|
723
|
-
: [entityOrListOrUndefined];
|
|
724
|
-
await Promise.all(
|
|
725
|
-
list.map(async (entity) => {
|
|
726
|
-
if (entity.id) {
|
|
727
|
-
// Clear the original cached loader result and fetch the new primitives
|
|
728
|
-
const loader = this.loaderForEntity(getMetadata(entity));
|
|
729
|
-
loader.clear(entity.id);
|
|
730
|
-
await loader.load(entity.id);
|
|
731
|
-
if (entity.__orm.deleted === undefined) {
|
|
732
|
-
// Then refresh any loaded collections
|
|
733
|
-
await Promise.all(
|
|
734
|
-
Object.values(entity).map((c) => {
|
|
735
|
-
if (c instanceof AbstractRelationImpl) {
|
|
736
|
-
return c.refreshIfLoaded();
|
|
737
|
-
}
|
|
738
|
-
return undefined;
|
|
739
|
-
}),
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}),
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
public get numberOfEntities(): number {
|
|
748
|
-
return this.entities.length;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
private loaderForFind<T extends Entity>(type: EntityConstructor<T>): DataLoader<FilterAndSettings<T>, unknown[]> {
|
|
752
|
-
return getOrSet(this.findLoaders, type.name, () => {
|
|
753
|
-
return new DataLoader<FilterAndSettings<T>, unknown[], string>(
|
|
754
|
-
async (queries) => {
|
|
755
|
-
function ensureUnderLimit(rows: unknown[]): unknown[] {
|
|
756
|
-
if (rows.length >= entityLimit) {
|
|
757
|
-
throw new Error(`Query returned more than ${entityLimit} rows`);
|
|
758
|
-
}
|
|
759
|
-
return rows;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// If there is only 1 query, we can skip the tagging step.
|
|
763
|
-
if (queries.length === 1) {
|
|
764
|
-
return [ensureUnderLimit(await buildQuery(this.knex, type, queries[0]))];
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const { knex } = this;
|
|
768
|
-
|
|
769
|
-
// Map each incoming query[i] to itself or a previous dup
|
|
770
|
-
const uniqueQueries: FilterAndSettings<T>[] = [];
|
|
771
|
-
const queryToUnique: Record<number, number> = {};
|
|
772
|
-
queries.forEach((q, i) => {
|
|
773
|
-
let j = uniqueQueries.findIndex((uq) => whereFilterHash(uq) === whereFilterHash(q));
|
|
774
|
-
if (j === -1) {
|
|
775
|
-
uniqueQueries.push(q);
|
|
776
|
-
j = uniqueQueries.length - 1;
|
|
777
|
-
}
|
|
778
|
-
queryToUnique[i] = j;
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
// There are duplicate queries, but only one unique query, so we can execute just it w/o tagging.
|
|
782
|
-
if (uniqueQueries.length === 1) {
|
|
783
|
-
const rows = ensureUnderLimit(await buildQuery(this.knex, type, queries[0]));
|
|
784
|
-
// Reuse this same result for however many callers asked for it.
|
|
785
|
-
return queries.map(() => rows);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// TODO: Instead of this tagged approach, we could probably check if the each
|
|
789
|
-
// where cause: a) has the same structure for joins, and b) has conditions that
|
|
790
|
-
// we can evaluate client-side, and then combine it into a query like:
|
|
791
|
-
//
|
|
792
|
-
// SELECT entity.*, t1.foo as condition1, t2.bar as condition2 FROM ...
|
|
793
|
-
// WHERE t1.foo (union of each queries condition)
|
|
794
|
-
//
|
|
795
|
-
// And then use the `condition1` and `condition2` to tease the combined result set
|
|
796
|
-
// back apart into each condition's result list.
|
|
797
|
-
|
|
798
|
-
// For each query, add an additional `__tag` column that will identify that query's
|
|
799
|
-
// corresponding rows in the combined/UNION ALL'd result set.
|
|
800
|
-
//
|
|
801
|
-
// We also add a `__row` column with that queries order, so that after we `UNION ALL`,
|
|
802
|
-
// we can order by `__tag` + `__row` and ensure we're getting back the combined rows
|
|
803
|
-
// exactly as they would be in done individually (i.e. per the docs `UNION ALL` does
|
|
804
|
-
// not gaurantee order).
|
|
805
|
-
const tagged = uniqueQueries.map((queryAndSettings, i) => {
|
|
806
|
-
const query = buildQuery(this.knex, type, queryAndSettings) as QueryBuilder;
|
|
807
|
-
return query.select(knex.raw(`${i} as __tag`), knex.raw("row_number() over () as __row"));
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
const meta = getMetadata(type);
|
|
811
|
-
|
|
812
|
-
// Kind of dumb, but make a dummy row to start our query with
|
|
813
|
-
let query = knex
|
|
814
|
-
.select("*", knex.raw("-1 as __tag"), knex.raw("-1 as __row"))
|
|
815
|
-
.from(meta.tableName)
|
|
816
|
-
.orderBy("__tag", "__row")
|
|
817
|
-
.where({ id: -1 });
|
|
818
|
-
|
|
819
|
-
// Use the dummy query as a base, then `UNION ALL` in all the rest
|
|
820
|
-
tagged.forEach((add) => {
|
|
821
|
-
query = query.unionAll(add, true);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
// Issue a single SQL statement for all of them
|
|
825
|
-
const rows = ensureUnderLimit(await query);
|
|
826
|
-
|
|
827
|
-
const resultForUniques: any[][] = [];
|
|
828
|
-
uniqueQueries.forEach((q, i) => {
|
|
829
|
-
resultForUniques[i] = [];
|
|
830
|
-
});
|
|
831
|
-
rows.forEach((row: any) => {
|
|
832
|
-
resultForUniques[row["__tag"]].push(row);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
// We return an array-of-arrays, where result[i] is the rows for queries[i]
|
|
836
|
-
const result: any[][] = [];
|
|
837
|
-
queries.forEach((q, i) => {
|
|
838
|
-
result[i] = resultForUniques[queryToUnique[i]];
|
|
839
|
-
});
|
|
840
|
-
return result;
|
|
841
|
-
},
|
|
842
|
-
{
|
|
843
|
-
// Our filter/order tuple is a complex object, so object-hash it to ensure caching works
|
|
844
|
-
cacheKeyFn: whereFilterHash,
|
|
845
|
-
},
|
|
846
|
-
);
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
private loaderForEntity<T extends Entity>(meta: EntityMetadata<T>): DataLoader<string, T | undefined> {
|
|
851
|
-
return getOrSet(this.__data.loaders, meta.type, () => {
|
|
852
|
-
return new DataLoader<string, T | undefined>(async (_keys) => {
|
|
853
|
-
assertIdsAreTagged(_keys);
|
|
854
|
-
const keys = deTagIds(meta, _keys);
|
|
855
|
-
|
|
856
|
-
const rows = await this.knex.select("*").from(meta.tableName).whereIn("id", keys);
|
|
857
|
-
|
|
858
|
-
// Pass overwriteExisting (which is the default anyway) because it might be EntityManager.refresh calling us.
|
|
859
|
-
const entities = rows.map((row) => this.hydrate(meta.cstr, row, { overwriteExisting: true }));
|
|
860
|
-
const entitiesById = indexBy(entities, (e) => e.id!);
|
|
861
|
-
|
|
862
|
-
// Return the results back in the same order as the keys
|
|
863
|
-
return _keys.map((k) => {
|
|
864
|
-
const entity = entitiesById.get(k);
|
|
865
|
-
// We generally expect all of our entities to be found, but they may not for API calls like
|
|
866
|
-
// `findOneOrFail` or for `EntityManager.refresh` when the entity has been deleted out from
|
|
867
|
-
// under us.
|
|
868
|
-
if (entity === undefined) {
|
|
869
|
-
const existingEntity = this.findExistingInstance<T>(k);
|
|
870
|
-
if (existingEntity) {
|
|
871
|
-
existingEntity.__orm.deleted = "deleted";
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return entity;
|
|
875
|
-
});
|
|
876
|
-
});
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Handles our Unit of Work-style look up / deduplication of entity instances.
|
|
881
|
-
private findExistingInstance<T>(id: string): T | undefined {
|
|
882
|
-
assertIdsAreTagged([id]);
|
|
883
|
-
return this._entityIndex.get(id) as T | undefined;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Takes a result `row` from a custom query and maps the db values into a new-or-existing domain object for that row.
|
|
888
|
-
*
|
|
889
|
-
* The `overwriteExisting` controls whether `row`'s values should overwrite the existing fields on
|
|
890
|
-
* an entity. By default this is true, as we assume the user calling this means they know the DB has
|
|
891
|
-
* updated values that should be put into the entities. A few internal callers set this to false,
|
|
892
|
-
* i.e. when we're loading collections and have db results that are potentially stale compared to
|
|
893
|
-
* the WIP entity state.
|
|
894
|
-
*/
|
|
895
|
-
public hydrate<T extends Entity>(type: EntityConstructor<T>, row: any, options?: { overwriteExisting?: boolean }): T {
|
|
896
|
-
const meta = getMetadata(type);
|
|
897
|
-
const id = keyToString(meta, row["id"]) || fail("No id column was available");
|
|
898
|
-
// See if this is already in our UoW
|
|
899
|
-
let entity = this.findExistingInstance(id) as T;
|
|
900
|
-
if (!entity) {
|
|
901
|
-
// Pass id as a hint that we're in hydrate mode
|
|
902
|
-
entity = new type(this, id);
|
|
903
|
-
meta.columns.forEach((c) => c.serde.setOnEntity(entity!.__orm.data, row));
|
|
904
|
-
} else if (options?.overwriteExisting !== false) {
|
|
905
|
-
// Usually if the entity alrady exists, we don't write over it, but in this case
|
|
906
|
-
// we assume that `EntityManager.refresh` is telling us to explicitly load the
|
|
907
|
-
// latest data.
|
|
908
|
-
meta.columns.forEach((c) => c.serde.setOnEntity(entity!.__orm.data, row));
|
|
909
|
-
}
|
|
910
|
-
return entity;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
public beforeTransaction(fn: HookFn) {
|
|
914
|
-
this.hooks.beforeTransaction.push(fn);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
public toString(): string {
|
|
918
|
-
return "EntityManager";
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
export let entityLimit = 10_000;
|
|
923
|
-
|
|
924
|
-
export function setEntityLimit(limit: number) {
|
|
925
|
-
entityLimit = limit;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
export function setDefaultEntityLimit() {
|
|
929
|
-
entityLimit = 10_000;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
export interface EntityMetadata<T extends Entity> {
|
|
933
|
-
cstr: EntityConstructor<T>;
|
|
934
|
-
type: string;
|
|
935
|
-
tableName: string;
|
|
936
|
-
tagName: string;
|
|
937
|
-
// Eventually our dbType should go away to support N-column fields
|
|
938
|
-
columns: Array<ColumnMeta>;
|
|
939
|
-
fields: Array<Field>;
|
|
940
|
-
config: ConfigApi<T, any>;
|
|
941
|
-
factory: (em: EntityManager, opts?: any) => New<T>;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
export type ColumnMeta = { fieldName: string; columnName: string; dbType: string; serde: ColumnSerde };
|
|
945
|
-
|
|
946
|
-
export type Field =
|
|
947
|
-
| PrimaryKeyField
|
|
948
|
-
| PrimitiveField
|
|
949
|
-
| EnumField
|
|
950
|
-
| OneToManyField
|
|
951
|
-
| ManyToOneField
|
|
952
|
-
| ManyToManyField
|
|
953
|
-
| OneToOneField;
|
|
954
|
-
|
|
955
|
-
export type PrimaryKeyField = {
|
|
956
|
-
kind: "primaryKey";
|
|
957
|
-
fieldName: string;
|
|
958
|
-
fieldIdName: undefined;
|
|
959
|
-
required: true;
|
|
960
|
-
};
|
|
961
|
-
|
|
962
|
-
export type PrimitiveField = {
|
|
963
|
-
kind: "primitive";
|
|
964
|
-
fieldName: string;
|
|
965
|
-
fieldIdName: undefined;
|
|
966
|
-
required: boolean;
|
|
967
|
-
derived: "orm" | "sync" | "async" | false;
|
|
968
|
-
protected: boolean;
|
|
969
|
-
type: string | Function;
|
|
970
|
-
};
|
|
971
|
-
|
|
972
|
-
export type EnumField = {
|
|
973
|
-
kind: "enum";
|
|
974
|
-
fieldName: string;
|
|
975
|
-
fieldIdName: undefined;
|
|
976
|
-
required: boolean;
|
|
977
|
-
enumDetailType: { getValues(): ReadonlyArray<unknown> };
|
|
978
|
-
};
|
|
979
|
-
|
|
980
|
-
export type OneToManyField = {
|
|
981
|
-
kind: "o2m";
|
|
982
|
-
fieldName: string;
|
|
983
|
-
fieldIdName: string;
|
|
984
|
-
required: boolean;
|
|
985
|
-
otherMetadata: () => EntityMetadata<any>;
|
|
986
|
-
otherFieldName: string;
|
|
987
|
-
};
|
|
988
|
-
|
|
989
|
-
export type ManyToOneField = {
|
|
990
|
-
kind: "m2o";
|
|
991
|
-
fieldName: string;
|
|
992
|
-
fieldIdName: string;
|
|
993
|
-
required: boolean;
|
|
994
|
-
otherMetadata: () => EntityMetadata<any>;
|
|
995
|
-
otherFieldName: string;
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
export type ManyToManyField = {
|
|
999
|
-
kind: "m2m";
|
|
1000
|
-
fieldName: string;
|
|
1001
|
-
fieldIdName: string;
|
|
1002
|
-
required: boolean;
|
|
1003
|
-
otherMetadata: () => EntityMetadata<any>;
|
|
1004
|
-
otherFieldName: string;
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
export type OneToOneField = {
|
|
1008
|
-
kind: "o2o";
|
|
1009
|
-
fieldName: string;
|
|
1010
|
-
fieldIdName: string;
|
|
1011
|
-
required: boolean;
|
|
1012
|
-
otherMetadata: () => EntityMetadata<any>;
|
|
1013
|
-
otherFieldName: string;
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
export function isEntity(maybeEntity: any): maybeEntity is Entity {
|
|
1017
|
-
return maybeEntity && typeof maybeEntity === "object" && "id" in maybeEntity && "__orm" in maybeEntity;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
export function isKey(k: any): k is string {
|
|
1021
|
-
return typeof k === "string";
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/** Compares `a` to `b`, where `b` might be an id. B/c ids can overlap, we need to know `b`'s metadata type. */
|
|
1025
|
-
export function sameEntity(a: Entity, bMeta: EntityMetadata<any>, bCurrent: Entity | string | undefined): boolean {
|
|
1026
|
-
if (a === undefined || bCurrent === undefined) {
|
|
1027
|
-
return false;
|
|
1028
|
-
}
|
|
1029
|
-
return (
|
|
1030
|
-
a === bCurrent || (getMetadata(a) === bMeta && maybeResolveReferenceToId(a) === maybeResolveReferenceToId(bCurrent))
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
export function getMetadata<T extends Entity>(entity: T): EntityMetadata<T>;
|
|
1035
|
-
export function getMetadata<T extends Entity>(type: EntityConstructor<T>): EntityMetadata<T>;
|
|
1036
|
-
export function getMetadata<T extends Entity>(entityOrType: T | EntityConstructor<T>): EntityMetadata<T> {
|
|
1037
|
-
return (typeof entityOrType === "function"
|
|
1038
|
-
? (entityOrType as any).metadata
|
|
1039
|
-
: entityOrType.__orm.metadata) as EntityMetadata<T>;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/** Thrown by `findOneOrFail` if an entity is not found. */
|
|
1043
|
-
export class NotFoundError extends Error {}
|
|
1044
|
-
|
|
1045
|
-
/** Thrown by `findOne` and `findOneOrFail` if more than one entity is found. */
|
|
1046
|
-
export class TooManyError extends Error {}
|
|
1047
|
-
|
|
1048
|
-
/**
|
|
1049
|
-
* For the entities currently in `todos`, find any reactive validation rules that point
|
|
1050
|
-
* from the currently-changed entities back to each rule's originally-defined-in entity,
|
|
1051
|
-
* and ensure those entities are added to `todos`.
|
|
1052
|
-
*/
|
|
1053
|
-
async function addReactiveValidations(todos: Record<string, Todo>): Promise<void> {
|
|
1054
|
-
const p: Promise<void>[] = Object.values(todos).flatMap((todo) => {
|
|
1055
|
-
const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
|
|
1056
|
-
// Find each statically-declared reactive rule for the given entity type
|
|
1057
|
-
return todo.metadata.config.__data.reactiveRules.map(async (reverseHint) => {
|
|
1058
|
-
// Add the resulting "found" entities to the right todos to be validated
|
|
1059
|
-
(await followReverseHint(entities, reverseHint)).forEach((entity) => {
|
|
1060
|
-
const todo = getTodo(todos, entity);
|
|
1061
|
-
if (!todo.inserts.includes(entity) && !todo.updates.includes(entity) && !entity.isDeletedEntity) {
|
|
1062
|
-
todo.validates.push(entity);
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
});
|
|
1066
|
-
});
|
|
1067
|
-
await Promise.all(p);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
/**
|
|
1071
|
-
* Given the current changed entities in `todos`, use the static metadata of `reactiveDerivedValues`
|
|
1072
|
-
* to find any potentially-unloaded entities we should now re-calc, and add them to `todos`.
|
|
1073
|
-
*/
|
|
1074
|
-
async function addReactiveAsyncDerivedValues(todos: Record<string, Todo>): Promise<void> {
|
|
1075
|
-
const p: Promise<void>[] = Object.values(todos).flatMap((todo) => {
|
|
1076
|
-
const entities = [...todo.inserts, ...todo.updates];
|
|
1077
|
-
return todo.metadata.config.__data.reactiveDerivedValues.map(async (reverseHint) => {
|
|
1078
|
-
(await followReverseHint(entities, reverseHint)).forEach((entity) => {
|
|
1079
|
-
const todo = getTodo(todos, entity);
|
|
1080
|
-
if (!todo.inserts.includes(entity) && !todo.updates.includes(entity) && !entity.isDeletedEntity) {
|
|
1081
|
-
todo.updates.push(entity);
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
});
|
|
1085
|
-
});
|
|
1086
|
-
await Promise.all(p);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/** Find all deleted entities and ensure their references all know about their deleted-ness. */
|
|
1090
|
-
async function cascadeDeletesIntoRelations(todos: Record<string, Todo>): Promise<void> {
|
|
1091
|
-
const entities = Object.values(todos).flatMap((todo) => todo.deletes);
|
|
1092
|
-
await Promise.all(
|
|
1093
|
-
entities
|
|
1094
|
-
.flatMap((e) => Object.values(e))
|
|
1095
|
-
.filter((v) => v instanceof AbstractRelationImpl)
|
|
1096
|
-
.map((relation: AbstractRelationImpl<any>) => {
|
|
1097
|
-
return relation.onEntityDeletedAndFlushing();
|
|
1098
|
-
}),
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
async function validate(todos: Record<string, Todo>): Promise<void> {
|
|
1103
|
-
const p = Object.values(todos).flatMap((todo) => {
|
|
1104
|
-
const rules = todo.metadata.config.__data.rules;
|
|
1105
|
-
return [...todo.inserts, ...todo.updates, ...todo.validates]
|
|
1106
|
-
.filter((e) => !e.isDeletedEntity)
|
|
1107
|
-
.flatMap((entity) => {
|
|
1108
|
-
return rules.flatMap(async (rule) => coerceError(entity, await rule(entity)));
|
|
1109
|
-
});
|
|
1110
|
-
});
|
|
1111
|
-
const errors = (await Promise.all(p)).flat();
|
|
1112
|
-
if (errors.length > 0) {
|
|
1113
|
-
throw new ValidationErrors(errors);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
async function beforeTransaction(em: EntityManager, knex: Knex.Transaction): Promise<void> {
|
|
1118
|
-
await Promise.all(em["hooks"].beforeTransaction.map((fn) => fn(em, knex)));
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
async function runHook(
|
|
1122
|
-
ctx: unknown,
|
|
1123
|
-
hook: EntityHook,
|
|
1124
|
-
todos: Record<string, Todo>,
|
|
1125
|
-
keys: ("inserts" | "deletes" | "updates" | "validates")[],
|
|
1126
|
-
): Promise<void> {
|
|
1127
|
-
const p = Object.values(todos).flatMap((todo) => {
|
|
1128
|
-
const hookFns = todo.metadata.config.__data.hooks[hook];
|
|
1129
|
-
|
|
1130
|
-
return keys
|
|
1131
|
-
.flatMap((k) => todo[k].filter((e) => k === "deletes" || !e.isDeletedEntity))
|
|
1132
|
-
.flatMap((entity) => {
|
|
1133
|
-
return hookFns.map(async (fn) => fn(entity, ctx as any));
|
|
1134
|
-
});
|
|
1135
|
-
});
|
|
1136
|
-
await Promise.all(p);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
async function beforeDelete(ctx: unknown, todos: Record<string, Todo>): Promise<void> {
|
|
1140
|
-
await runHook(ctx, "beforeDelete", todos, ["deletes"]);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
async function beforeFlush(ctx: unknown, todos: Record<string, Todo>): Promise<void> {
|
|
1144
|
-
await runHook(ctx, "beforeFlush", todos, ["inserts", "updates"]);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
async function afterValidation(ctx: unknown, todos: Record<string, Todo>): Promise<void> {
|
|
1148
|
-
await runHook(ctx, "afterValidation", todos, ["inserts", "updates"]);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
async function afterCommit(ctx: unknown, todos: Record<string, Todo>): Promise<void> {
|
|
1152
|
-
await runHook(ctx, "afterCommit", todos, ["inserts", "updates"]);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
function coerceError(
|
|
1156
|
-
entity: Entity,
|
|
1157
|
-
maybeError: string | ValidationError | ValidationError[] | undefined,
|
|
1158
|
-
): ValidationError[] {
|
|
1159
|
-
if (maybeError === undefined) {
|
|
1160
|
-
return [];
|
|
1161
|
-
} else if (typeof maybeError === "string") {
|
|
1162
|
-
return [{ entity, message: maybeError }];
|
|
1163
|
-
} else if (Array.isArray(maybeError)) {
|
|
1164
|
-
return maybeError as ValidationError[];
|
|
1165
|
-
} else {
|
|
1166
|
-
return [maybeError];
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};
|
|
1171
|
-
|
|
1172
|
-
/**
|
|
1173
|
-
* Evaluates each derived field to see if it's value has changed.
|
|
1174
|
-
*
|
|
1175
|
-
* This is a) not at all reactive, b) only works for primitives, c) doesn't work
|
|
1176
|
-
* with async/promise-based logic, and d) doesn't support passing an app-specific
|
|
1177
|
-
* context, but it's a start.
|
|
1178
|
-
*/
|
|
1179
|
-
function recalcDerivedFields(todos: Record<string, Todo>) {
|
|
1180
|
-
const entities = Object.values(todos)
|
|
1181
|
-
.flatMap((todo) => [...todo.inserts, ...todo.updates])
|
|
1182
|
-
.filter((e) => !e.isDeletedEntity);
|
|
1183
|
-
const derivedFieldsByMeta = new Map(
|
|
1184
|
-
[...new Set(entities.map(getMetadata))].map((m) => {
|
|
1185
|
-
return [m, m.fields.filter((f) => f.kind === "primitive" && f.derived === "sync").map((f) => f.fieldName)];
|
|
1186
|
-
}),
|
|
1187
|
-
);
|
|
1188
|
-
|
|
1189
|
-
for (const entity of entities) {
|
|
1190
|
-
const derivedFields = derivedFieldsByMeta.get(entity.__orm.metadata);
|
|
1191
|
-
derivedFields?.forEach((fieldName) => {
|
|
1192
|
-
// setField will intelligently mark/not mark the field as dirty.
|
|
1193
|
-
setField(entity, fieldName, (entity as any)[fieldName]);
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
/**
|
|
1199
|
-
* Calcs async derived fields for inserts and updates.
|
|
1200
|
-
*
|
|
1201
|
-
* We assume that `addReactiveAsyncDerivedValues` has already found any "reactive"
|
|
1202
|
-
* entities that need fields re-calced, and has already added them to `todos`.
|
|
1203
|
-
*/
|
|
1204
|
-
async function recalcAsyncDerivedFields(em: EntityManager, todos: Record<string, Todo>): Promise<void> {
|
|
1205
|
-
const p = Object.values(todos).map(async (todo) => {
|
|
1206
|
-
const { asyncDerivedFields } = todo.metadata.config.__data;
|
|
1207
|
-
const changed = [...todo.inserts, ...todo.updates];
|
|
1208
|
-
const p = Object.entries(asyncDerivedFields).map(async ([key, entry]) => {
|
|
1209
|
-
if (entry) {
|
|
1210
|
-
const [hint, fn] = entry;
|
|
1211
|
-
await em.populate(changed, hint);
|
|
1212
|
-
await Promise.all(changed.map((entity) => setField(entity, key, fn(entity))));
|
|
1213
|
-
}
|
|
1214
|
-
});
|
|
1215
|
-
await Promise.all(p);
|
|
1216
|
-
});
|
|
1217
|
-
await Promise.all(p);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// If a where clause includes an entity, object-hash cannot hash it, so just use the id.
|
|
1221
|
-
const replacer = (v: any) => (isEntity(v) ? v.id : v);
|
|
1222
|
-
|
|
1223
|
-
function whereFilterHash(where: FilterAndSettings<any>): string {
|
|
1224
|
-
return hash(where, { replacer });
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
/**
|
|
1228
|
-
* Walks `reverseHint` for every entity in `entities`.
|
|
1229
|
-
*
|
|
1230
|
-
* I.e. given `[book1, book2]` and `["author", 'publisher"]`, will return all of the books' authors' publishers.
|
|
1231
|
-
*/
|
|
1232
|
-
async function followReverseHint(entities: Entity[], reverseHint: string[]): Promise<Entity[]> {
|
|
1233
|
-
// Start at the current entities
|
|
1234
|
-
let current = [...entities];
|
|
1235
|
-
const paths = [...reverseHint];
|
|
1236
|
-
// And "walk backwards" through the reverse hint
|
|
1237
|
-
while (paths.length) {
|
|
1238
|
-
const fieldName = paths.shift()!;
|
|
1239
|
-
// The path might touch either a reference or a collection
|
|
1240
|
-
const entitiesOrLists = await Promise.all(
|
|
1241
|
-
current.flatMap((c) => {
|
|
1242
|
-
const currentValuePromise = (c as any)[fieldName].load();
|
|
1243
|
-
// If we're going from Book.author back to Author to re-validate the Author.books collection,
|
|
1244
|
-
// see if Book.author has changed so we can re-validate both the old author's books and the
|
|
1245
|
-
// new author's books.
|
|
1246
|
-
const isReference = getMetadata(c).fields.find((f) => f.fieldName === fieldName)?.kind === "m2o";
|
|
1247
|
-
const hasChanged = isReference && (c as any).changes[fieldName].hasChanged;
|
|
1248
|
-
const originalValue = (c as any).changes[fieldName].originalValue;
|
|
1249
|
-
if (hasChanged && originalValue) {
|
|
1250
|
-
const originalEntityMaybePromise = isEntity(originalValue)
|
|
1251
|
-
? originalValue
|
|
1252
|
-
: getEm(c).load((c as any)[fieldName].otherMeta.cstr, originalValue);
|
|
1253
|
-
return [currentValuePromise, originalEntityMaybePromise];
|
|
1254
|
-
}
|
|
1255
|
-
return [currentValuePromise];
|
|
1256
|
-
}),
|
|
1257
|
-
);
|
|
1258
|
-
// Use flat() to get them all as entities
|
|
1259
|
-
const entities = entitiesOrLists.flat().filter((e) => e !== undefined);
|
|
1260
|
-
current = entities as Entity[];
|
|
1261
|
-
}
|
|
1262
|
-
return current;
|
|
1263
|
-
}
|