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.
Files changed (221) hide show
  1. package/build/{BaseEntity.d.ts → src/BaseEntity.d.ts} +2 -1
  2. package/build/{BaseEntity.js → src/BaseEntity.js} +13 -9
  3. package/build/src/BaseEntity.js.map +1 -0
  4. package/build/{EntityManager.d.ts → src/EntityManager.d.ts} +139 -110
  5. package/build/{EntityManager.js → src/EntityManager.js} +281 -262
  6. package/build/src/EntityManager.js.map +1 -0
  7. package/build/{QueryBuilder.d.ts → src/QueryBuilder.d.ts} +53 -3
  8. package/build/src/QueryBuilder.js +341 -0
  9. package/build/src/QueryBuilder.js.map +1 -0
  10. package/build/src/Todo.d.ts +25 -0
  11. package/build/src/Todo.js +52 -0
  12. package/build/src/Todo.js.map +1 -0
  13. package/build/src/changes.d.ts +34 -0
  14. package/build/src/changes.js +37 -0
  15. package/build/src/changes.js.map +1 -0
  16. package/build/src/config.d.ts +43 -0
  17. package/build/src/config.js +114 -0
  18. package/build/src/config.js.map +1 -0
  19. package/build/{createOrUpdatePartial.d.ts → src/createOrUpdatePartial.d.ts} +2 -1
  20. package/build/{createOrUpdatePartial.js → src/createOrUpdatePartial.js} +42 -10
  21. package/build/src/createOrUpdatePartial.js.map +1 -0
  22. package/build/src/dataloaders/findDataLoader.d.ts +5 -0
  23. package/build/src/dataloaders/findDataLoader.js +28 -0
  24. package/build/src/dataloaders/findDataLoader.js.map +1 -0
  25. package/build/src/dataloaders/loadDataLoader.d.ts +3 -0
  26. package/build/src/dataloaders/loadDataLoader.js +37 -0
  27. package/build/src/dataloaders/loadDataLoader.js.map +1 -0
  28. package/build/src/dataloaders/manyToManyDataLoader.d.ts +5 -0
  29. package/build/src/dataloaders/manyToManyDataLoader.js +78 -0
  30. package/build/src/dataloaders/manyToManyDataLoader.js.map +1 -0
  31. package/build/src/dataloaders/manyToManyFindDataLoader.d.ts +5 -0
  32. package/build/src/dataloaders/manyToManyFindDataLoader.js +33 -0
  33. package/build/src/dataloaders/manyToManyFindDataLoader.js.map +1 -0
  34. package/build/src/dataloaders/oneToManyDataLoader.d.ts +4 -0
  35. package/build/src/dataloaders/oneToManyDataLoader.js +40 -0
  36. package/build/src/dataloaders/oneToManyDataLoader.js.map +1 -0
  37. package/build/src/dataloaders/oneToManyFindDataLoader.d.ts +5 -0
  38. package/build/src/dataloaders/oneToManyFindDataLoader.js +32 -0
  39. package/build/src/dataloaders/oneToManyFindDataLoader.js.map +1 -0
  40. package/build/src/dataloaders/oneToOneDataLoader.d.ts +4 -0
  41. package/build/src/dataloaders/oneToOneDataLoader.js +40 -0
  42. package/build/src/dataloaders/oneToOneDataLoader.js.map +1 -0
  43. package/build/src/drivers/IdAssigner.d.ts +33 -0
  44. package/build/src/drivers/IdAssigner.js +106 -0
  45. package/build/src/drivers/IdAssigner.js.map +1 -0
  46. package/build/src/drivers/InMemoryDriver.d.ts +29 -0
  47. package/build/src/drivers/InMemoryDriver.js +306 -0
  48. package/build/src/drivers/InMemoryDriver.js.map +1 -0
  49. package/build/src/drivers/PostgresDriver.d.ts +40 -0
  50. package/build/src/drivers/PostgresDriver.js +376 -0
  51. package/build/src/drivers/PostgresDriver.js.map +1 -0
  52. package/build/src/drivers/driver.d.ts +23 -0
  53. package/build/src/drivers/driver.js +3 -0
  54. package/build/src/drivers/driver.js.map +1 -0
  55. package/build/src/drivers/index.d.ts +4 -0
  56. package/build/src/drivers/index.js +17 -0
  57. package/build/src/drivers/index.js.map +1 -0
  58. package/build/{getProperties.d.ts → src/getProperties.d.ts} +0 -0
  59. package/build/{getProperties.js → src/getProperties.js} +1 -1
  60. package/build/src/getProperties.js.map +1 -0
  61. package/build/src/index.d.ts +62 -0
  62. package/build/src/index.js +263 -0
  63. package/build/src/index.js.map +1 -0
  64. package/build/src/keys.d.ts +30 -0
  65. package/build/{keys.js → src/keys.js} +48 -16
  66. package/build/src/keys.js.map +1 -0
  67. package/build/{loadLens.d.ts → src/loadLens.d.ts} +2 -2
  68. package/build/{loadLens.js → src/loadLens.js} +1 -1
  69. package/build/src/loadLens.js.map +1 -0
  70. package/build/src/loaded.d.ts +49 -0
  71. package/build/src/loaded.js +9 -0
  72. package/build/src/loaded.js.map +1 -0
  73. package/build/{newTestInstance.d.ts → src/newTestInstance.d.ts} +37 -3
  74. package/build/src/newTestInstance.js +342 -0
  75. package/build/src/newTestInstance.js.map +1 -0
  76. package/build/{collections → src/relations}/AbstractRelationImpl.d.ts +6 -5
  77. package/build/{collections → src/relations}/AbstractRelationImpl.js +0 -0
  78. package/build/src/relations/AbstractRelationImpl.js.map +1 -0
  79. package/build/src/relations/Collection.d.ts +26 -0
  80. package/build/src/relations/Collection.js +19 -0
  81. package/build/src/relations/Collection.js.map +1 -0
  82. package/build/{collections → src/relations}/CustomCollection.d.ts +6 -2
  83. package/build/{collections → src/relations}/CustomCollection.js +17 -9
  84. package/build/src/relations/CustomCollection.js.map +1 -0
  85. package/build/{collections → src/relations}/CustomReference.d.ts +7 -2
  86. package/build/{collections → src/relations}/CustomReference.js +16 -9
  87. package/build/src/relations/CustomReference.js.map +1 -0
  88. package/build/src/relations/LargeCollection.d.ts +17 -0
  89. package/build/src/relations/LargeCollection.js +3 -0
  90. package/build/src/relations/LargeCollection.js.map +1 -0
  91. package/build/{collections → src/relations}/ManyToManyCollection.d.ts +9 -2
  92. package/build/src/relations/ManyToManyCollection.js +249 -0
  93. package/build/src/relations/ManyToManyCollection.js.map +1 -0
  94. package/build/src/relations/ManyToManyLargeCollection.d.ts +25 -0
  95. package/build/src/relations/ManyToManyLargeCollection.js +97 -0
  96. package/build/src/relations/ManyToManyLargeCollection.js.map +1 -0
  97. package/build/src/relations/ManyToOneReference.d.ts +77 -0
  98. package/build/{collections → src/relations}/ManyToOneReference.js +101 -48
  99. package/build/src/relations/ManyToOneReference.js.map +1 -0
  100. package/build/{collections → src/relations}/OneToManyCollection.d.ts +10 -2
  101. package/build/{collections → src/relations}/OneToManyCollection.js +54 -59
  102. package/build/src/relations/OneToManyCollection.js.map +1 -0
  103. package/build/src/relations/OneToManyLargeCollection.d.ts +25 -0
  104. package/build/src/relations/OneToManyLargeCollection.js +83 -0
  105. package/build/src/relations/OneToManyLargeCollection.js.map +1 -0
  106. package/build/src/relations/OneToOneReference.d.ts +82 -0
  107. package/build/src/relations/OneToOneReference.js +168 -0
  108. package/build/src/relations/OneToOneReference.js.map +1 -0
  109. package/build/src/relations/PolymorphicReference.d.ts +69 -0
  110. package/build/src/relations/PolymorphicReference.js +210 -0
  111. package/build/src/relations/PolymorphicReference.js.map +1 -0
  112. package/build/src/relations/Reference.d.ts +29 -0
  113. package/build/src/relations/Reference.js +23 -0
  114. package/build/src/relations/Reference.js.map +1 -0
  115. package/build/src/relations/Relation.d.ts +10 -0
  116. package/build/src/relations/Relation.js +13 -0
  117. package/build/src/relations/Relation.js.map +1 -0
  118. package/build/src/relations/hasAsyncProperty.d.ts +36 -0
  119. package/build/src/relations/hasAsyncProperty.js +55 -0
  120. package/build/src/relations/hasAsyncProperty.js.map +1 -0
  121. package/build/{collections → src/relations}/hasManyDerived.d.ts +2 -1
  122. package/build/{collections → src/relations}/hasManyDerived.js +1 -1
  123. package/build/src/relations/hasManyDerived.js.map +1 -0
  124. package/build/{collections → src/relations}/hasManyThrough.d.ts +0 -0
  125. package/build/{collections → src/relations}/hasManyThrough.js +2 -2
  126. package/build/src/relations/hasManyThrough.js.map +1 -0
  127. package/build/{collections → src/relations}/hasOneDerived.d.ts +3 -2
  128. package/build/{collections → src/relations}/hasOneDerived.js +1 -1
  129. package/build/src/relations/hasOneDerived.js.map +1 -0
  130. package/build/{collections → src/relations}/hasOneThrough.d.ts +0 -0
  131. package/build/{collections → src/relations}/hasOneThrough.js +2 -2
  132. package/build/src/relations/hasOneThrough.js.map +1 -0
  133. package/build/src/relations/index.d.ts +18 -0
  134. package/build/src/relations/index.js +53 -0
  135. package/build/src/relations/index.js.map +1 -0
  136. package/build/{reverseHint.d.ts → src/reverseHint.d.ts} +2 -1
  137. package/build/{reverseHint.js → src/reverseHint.js} +13 -9
  138. package/build/src/reverseHint.js.map +1 -0
  139. package/build/src/rules.d.ts +23 -0
  140. package/build/src/rules.js +23 -0
  141. package/build/src/rules.js.map +1 -0
  142. package/build/src/serde.d.ts +121 -0
  143. package/build/src/serde.js +190 -0
  144. package/build/src/serde.js.map +1 -0
  145. package/build/{utils.d.ts → src/utils.d.ts} +2 -0
  146. package/build/{utils.js → src/utils.js} +10 -1
  147. package/build/src/utils.js.map +1 -0
  148. package/build/tsconfig.tsbuildinfo +1 -0
  149. package/package.json +30 -15
  150. package/build/BaseEntity.js.map +0 -1
  151. package/build/EntityManager.js.map +0 -1
  152. package/build/EntityPersister.d.ts +0 -30
  153. package/build/EntityPersister.js +0 -197
  154. package/build/EntityPersister.js.map +0 -1
  155. package/build/QueryBuilder.js +0 -195
  156. package/build/QueryBuilder.js.map +0 -1
  157. package/build/changes.d.ts +0 -23
  158. package/build/changes.js +0 -14
  159. package/build/changes.js.map +0 -1
  160. package/build/collections/AbstractRelationImpl.js.map +0 -1
  161. package/build/collections/CustomCollection.js.map +0 -1
  162. package/build/collections/CustomReference.js.map +0 -1
  163. package/build/collections/ManyToManyCollection.js +0 -288
  164. package/build/collections/ManyToManyCollection.js.map +0 -1
  165. package/build/collections/ManyToOneReference.d.ts +0 -50
  166. package/build/collections/ManyToOneReference.js.map +0 -1
  167. package/build/collections/OneToManyCollection.js.map +0 -1
  168. package/build/collections/OneToOneReference.d.ts +0 -51
  169. package/build/collections/OneToOneReference.js +0 -132
  170. package/build/collections/OneToOneReference.js.map +0 -1
  171. package/build/collections/hasManyDerived.js.map +0 -1
  172. package/build/collections/hasManyThrough.js.map +0 -1
  173. package/build/collections/hasOneDerived.js.map +0 -1
  174. package/build/collections/hasOneThrough.js.map +0 -1
  175. package/build/collections/index.d.ts +0 -19
  176. package/build/collections/index.js +0 -49
  177. package/build/collections/index.js.map +0 -1
  178. package/build/createOrUpdatePartial.js.map +0 -1
  179. package/build/getProperties.js.map +0 -1
  180. package/build/index.d.ts +0 -140
  181. package/build/index.js +0 -278
  182. package/build/index.js.map +0 -1
  183. package/build/keys.d.ts +0 -21
  184. package/build/keys.js.map +0 -1
  185. package/build/loadLens.js.map +0 -1
  186. package/build/newTestInstance.js +0 -153
  187. package/build/newTestInstance.js.map +0 -1
  188. package/build/reverseHint.js.map +0 -1
  189. package/build/serde.d.ts +0 -47
  190. package/build/serde.js +0 -93
  191. package/build/serde.js.map +0 -1
  192. package/build/utils.js.map +0 -1
  193. package/package.json.bak +0 -27
  194. package/src/BaseEntity.ts +0 -104
  195. package/src/EntityManager.ts +0 -1263
  196. package/src/EntityPersister.ts +0 -240
  197. package/src/QueryBuilder.ts +0 -289
  198. package/src/changes.ts +0 -40
  199. package/src/collections/AbstractRelationImpl.ts +0 -28
  200. package/src/collections/CustomCollection.ts +0 -152
  201. package/src/collections/CustomReference.ts +0 -138
  202. package/src/collections/ManyToManyCollection.ts +0 -346
  203. package/src/collections/ManyToOneReference.ts +0 -215
  204. package/src/collections/OneToManyCollection.ts +0 -254
  205. package/src/collections/OneToOneReference.ts +0 -153
  206. package/src/collections/hasManyDerived.ts +0 -29
  207. package/src/collections/hasManyThrough.ts +0 -20
  208. package/src/collections/hasOneDerived.ts +0 -26
  209. package/src/collections/hasOneThrough.ts +0 -20
  210. package/src/collections/index.ts +0 -74
  211. package/src/createOrUpdatePartial.ts +0 -144
  212. package/src/getProperties.ts +0 -27
  213. package/src/index.ts +0 -400
  214. package/src/keys.ts +0 -75
  215. package/src/loadLens.ts +0 -126
  216. package/src/newTestInstance.ts +0 -205
  217. package/src/reverseHint.ts +0 -43
  218. package/src/serde.ts +0 -97
  219. package/src/utils.ts +0 -63
  220. package/tsconfig.json +0 -21
  221. package/tsconfig.tsbuildinfo +0 -2646
@@ -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
- }