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