pure-orm 4.0.2 → 4.1.4

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 (209) hide show
  1. package/.benchmarks/bench-core-baseline.json +303 -0
  2. package/.eslintrc.json +20 -10
  3. package/README.md +0 -7
  4. package/coverage/clover.xml +1493 -1232
  5. package/coverage/coverage-final.json +103 -103
  6. package/coverage/lcov-report/dist/src/core.js.html +590 -383
  7. package/coverage/lcov-report/dist/src/driver-integrations/index.html +20 -20
  8. package/coverage/lcov-report/dist/src/driver-integrations/pgp.js.html +52 -52
  9. package/coverage/lcov-report/dist/src/index.html +28 -28
  10. package/coverage/lcov-report/dist/src/index.js.html +2 -2
  11. package/coverage/lcov-report/dist/src/orm.js.html +392 -209
  12. package/coverage/lcov-report/dist/test-utils/blog/entities.js.html +1 -1
  13. package/coverage/lcov-report/dist/test-utils/blog/index.html +1 -1
  14. package/coverage/lcov-report/dist/test-utils/blog/models/article.js.html +15 -15
  15. package/coverage/lcov-report/dist/test-utils/blog/models/article_tag.js.html +2 -2
  16. package/coverage/lcov-report/dist/test-utils/blog/models/index.html +1 -1
  17. package/coverage/lcov-report/dist/test-utils/blog/models/person.js.html +13 -13
  18. package/coverage/lcov-report/dist/test-utils/blog/models/tag.js.html +1 -1
  19. package/coverage/lcov-report/dist/test-utils/five/entities.js.html +1 -1
  20. package/coverage/lcov-report/dist/test-utils/five/index.html +1 -1
  21. package/coverage/lcov-report/dist/test-utils/five/models/index.html +1 -1
  22. package/coverage/lcov-report/dist/test-utils/five/models/line-item.js.html +4 -4
  23. package/coverage/lcov-report/dist/test-utils/five/models/order.js.html +3 -3
  24. package/coverage/lcov-report/dist/test-utils/five/models/parcel-event.js.html +6 -6
  25. package/coverage/lcov-report/dist/test-utils/five/models/parcel-line-item.js.html +7 -7
  26. package/coverage/lcov-report/dist/test-utils/five/models/parcel.js.html +2 -2
  27. package/coverage/lcov-report/dist/test-utils/fourteen/entities.js.html +1 -1
  28. package/coverage/lcov-report/dist/test-utils/fourteen/index.html +1 -1
  29. package/coverage/lcov-report/dist/test-utils/fourteen/models/customer.js.html +1 -1
  30. package/coverage/lcov-report/dist/test-utils/fourteen/models/index.html +1 -1
  31. package/coverage/lcov-report/dist/test-utils/fourteen/models/person.js.html +1 -1
  32. package/coverage/lcov-report/dist/test-utils/nine/entities.js.html +1 -1
  33. package/coverage/lcov-report/dist/test-utils/nine/index.html +1 -1
  34. package/coverage/lcov-report/dist/test-utils/nine/models/feature-switch.js.html +6 -6
  35. package/coverage/lcov-report/dist/test-utils/nine/models/index.html +1 -1
  36. package/coverage/lcov-report/dist/test-utils/order/entities.js.html +9 -9
  37. package/coverage/lcov-report/dist/test-utils/order/index.html +1 -1
  38. package/coverage/lcov-report/dist/test-utils/order/models/index.html +14 -14
  39. package/coverage/lcov-report/dist/test-utils/order/models/line-item.js.html +11 -11
  40. package/coverage/lcov-report/dist/test-utils/order/models/order.js.html +41 -41
  41. package/coverage/lcov-report/dist/test-utils/order/models/product-variant.js.html +18 -18
  42. package/coverage/lcov-report/dist/test-utils/order/models/product.js.html +17 -17
  43. package/coverage/lcov-report/dist/test-utils/order/models/utm-source.js.html +12 -12
  44. package/coverage/lcov-report/dist/test-utils/order-more/entities.js.html +1 -1
  45. package/coverage/lcov-report/dist/test-utils/order-more/index.html +1 -1
  46. package/coverage/lcov-report/dist/test-utils/order-more/models/actual-product-variant.js.html +3 -3
  47. package/coverage/lcov-report/dist/test-utils/order-more/models/color.js.html +6 -6
  48. package/coverage/lcov-report/dist/test-utils/order-more/models/customer.js.html +3 -3
  49. package/coverage/lcov-report/dist/test-utils/order-more/models/gender.js.html +4 -4
  50. package/coverage/lcov-report/dist/test-utils/order-more/models/index.html +23 -23
  51. package/coverage/lcov-report/dist/test-utils/order-more/models/inventory-level.js.html +11 -11
  52. package/coverage/lcov-report/dist/test-utils/order-more/models/line-item.js.html +15 -15
  53. package/coverage/lcov-report/dist/test-utils/order-more/models/order.js.html +39 -39
  54. package/coverage/lcov-report/dist/test-utils/order-more/models/parcel-event.js.html +6 -6
  55. package/coverage/lcov-report/dist/test-utils/order-more/models/parcel-line-item.js.html +7 -7
  56. package/coverage/lcov-report/dist/test-utils/order-more/models/parcel.js.html +2 -2
  57. package/coverage/lcov-report/dist/test-utils/order-more/models/physical-address.js.html +12 -12
  58. package/coverage/lcov-report/dist/test-utils/order-more/models/product-variant-image.js.html +7 -7
  59. package/coverage/lcov-report/dist/test-utils/order-more/models/product-variant.js.html +22 -22
  60. package/coverage/lcov-report/dist/test-utils/order-more/models/product.js.html +11 -11
  61. package/coverage/lcov-report/dist/test-utils/order-more/models/refund.js.html +11 -11
  62. package/coverage/lcov-report/dist/test-utils/order-more/models/shipment-actual-product-variant.js.html +9 -9
  63. package/coverage/lcov-report/dist/test-utils/order-more/models/shipment.js.html +4 -4
  64. package/coverage/lcov-report/dist/test-utils/order-more/models/size.js.html +4 -4
  65. package/coverage/lcov-report/dist/test-utils/order-more/models/utm-medium.js.html +15 -15
  66. package/coverage/lcov-report/dist/test-utils/order-more/models/utm-source.js.html +17 -17
  67. package/coverage/lcov-report/dist/test-utils/six/entities.js.html +1 -1
  68. package/coverage/lcov-report/dist/test-utils/six/index.html +1 -1
  69. package/coverage/lcov-report/dist/test-utils/six/models/customer.js.html +3 -3
  70. package/coverage/lcov-report/dist/test-utils/six/models/index.html +21 -21
  71. package/coverage/lcov-report/dist/test-utils/six/models/line-item.js.html +10 -10
  72. package/coverage/lcov-report/dist/test-utils/six/models/order.js.html +13 -13
  73. package/coverage/lcov-report/dist/test-utils/six/models/parcel-line-item.js.html +2 -2
  74. package/coverage/lcov-report/dist/test-utils/six/models/parcel.js.html +2 -2
  75. package/coverage/lcov-report/dist/test-utils/thirteen/entities.js.html +1 -1
  76. package/coverage/lcov-report/dist/test-utils/thirteen/index.html +1 -1
  77. package/coverage/lcov-report/dist/test-utils/thirteen/models/audience.js.html +2 -2
  78. package/coverage/lcov-report/dist/test-utils/thirteen/models/brand.js.html +2 -2
  79. package/coverage/lcov-report/dist/test-utils/thirteen/models/category.js.html +1 -1
  80. package/coverage/lcov-report/dist/test-utils/thirteen/models/index.html +14 -14
  81. package/coverage/lcov-report/dist/test-utils/thirteen/models/member.js.html +2 -2
  82. package/coverage/lcov-report/dist/test-utils/thirteen/models/passion.js.html +2 -2
  83. package/coverage/lcov-report/dist/test-utils/thirteen/models/product.js.html +10 -10
  84. package/coverage/lcov-report/dist/test-utils/thirteen/models/recommendation-audience.js.html +2 -2
  85. package/coverage/lcov-report/dist/test-utils/thirteen/models/recommendation.js.html +2 -2
  86. package/coverage/lcov-report/dist/test-utils/three/index.html +1 -1
  87. package/coverage/lcov-report/dist/test-utils/three/results.js.html +1 -1
  88. package/coverage/lcov-report/dist/test-utils/twelve/entities.js.html +1 -1
  89. package/coverage/lcov-report/dist/test-utils/twelve/index.html +1 -1
  90. package/coverage/lcov-report/dist/test-utils/twelve/models/index.html +1 -1
  91. package/coverage/lcov-report/dist/test-utils/twelve/models/member.js.html +1 -1
  92. package/coverage/lcov-report/dist/test-utils/twelve/models/prompt.js.html +2 -2
  93. package/coverage/lcov-report/dist/test-utils/two/index.html +1 -1
  94. package/coverage/lcov-report/dist/test-utils/two/results.js.html +1 -1
  95. package/coverage/lcov-report/index.html +103 -103
  96. package/coverage/lcov-report/src/core.ts.html +841 -415
  97. package/coverage/lcov-report/src/driver-integrations/index.html +20 -20
  98. package/coverage/lcov-report/src/driver-integrations/pgp.ts.html +63 -63
  99. package/coverage/lcov-report/src/index.html +28 -28
  100. package/coverage/lcov-report/src/index.ts.html +1 -1
  101. package/coverage/lcov-report/src/orm.ts.html +388 -298
  102. package/coverage/lcov-report/test-utils/blog/entities.ts.html +1 -1
  103. package/coverage/lcov-report/test-utils/blog/index.html +1 -1
  104. package/coverage/lcov-report/test-utils/blog/models/article.ts.html +15 -15
  105. package/coverage/lcov-report/test-utils/blog/models/article_tag.ts.html +2 -2
  106. package/coverage/lcov-report/test-utils/blog/models/index.html +1 -1
  107. package/coverage/lcov-report/test-utils/blog/models/person.ts.html +13 -13
  108. package/coverage/lcov-report/test-utils/blog/models/tag.ts.html +1 -1
  109. package/coverage/lcov-report/test-utils/five/entities.ts.html +1 -1
  110. package/coverage/lcov-report/test-utils/five/index.html +1 -1
  111. package/coverage/lcov-report/test-utils/five/models/index.html +1 -1
  112. package/coverage/lcov-report/test-utils/five/models/line-item.ts.html +4 -4
  113. package/coverage/lcov-report/test-utils/five/models/order.ts.html +3 -3
  114. package/coverage/lcov-report/test-utils/five/models/parcel-event.ts.html +6 -6
  115. package/coverage/lcov-report/test-utils/five/models/parcel-line-item.ts.html +7 -7
  116. package/coverage/lcov-report/test-utils/five/models/parcel.ts.html +2 -2
  117. package/coverage/lcov-report/test-utils/fourteen/entities.ts.html +1 -1
  118. package/coverage/lcov-report/test-utils/fourteen/index.html +1 -1
  119. package/coverage/lcov-report/test-utils/fourteen/models/customer.ts.html +1 -1
  120. package/coverage/lcov-report/test-utils/fourteen/models/index.html +1 -1
  121. package/coverage/lcov-report/test-utils/fourteen/models/person.ts.html +1 -1
  122. package/coverage/lcov-report/test-utils/nine/entities.ts.html +1 -1
  123. package/coverage/lcov-report/test-utils/nine/index.html +1 -1
  124. package/coverage/lcov-report/test-utils/nine/models/feature-switch.ts.html +8 -8
  125. package/coverage/lcov-report/test-utils/nine/models/index.html +1 -1
  126. package/coverage/lcov-report/test-utils/order/entities.ts.html +2 -2
  127. package/coverage/lcov-report/test-utils/order/index.html +1 -1
  128. package/coverage/lcov-report/test-utils/order/models/index.html +14 -14
  129. package/coverage/lcov-report/test-utils/order/models/line-item.ts.html +5 -5
  130. package/coverage/lcov-report/test-utils/order/models/order.ts.html +36 -36
  131. package/coverage/lcov-report/test-utils/order/models/product-variant.ts.html +13 -13
  132. package/coverage/lcov-report/test-utils/order/models/product.ts.html +13 -13
  133. package/coverage/lcov-report/test-utils/order/models/utm-source.ts.html +8 -8
  134. package/coverage/lcov-report/test-utils/order-more/entities.ts.html +1 -1
  135. package/coverage/lcov-report/test-utils/order-more/index.html +1 -1
  136. package/coverage/lcov-report/test-utils/order-more/models/actual-product-variant.ts.html +3 -3
  137. package/coverage/lcov-report/test-utils/order-more/models/color.ts.html +6 -6
  138. package/coverage/lcov-report/test-utils/order-more/models/customer.ts.html +3 -3
  139. package/coverage/lcov-report/test-utils/order-more/models/gender.ts.html +4 -4
  140. package/coverage/lcov-report/test-utils/order-more/models/index.html +23 -23
  141. package/coverage/lcov-report/test-utils/order-more/models/inventory-level.ts.html +11 -11
  142. package/coverage/lcov-report/test-utils/order-more/models/line-item.ts.html +15 -15
  143. package/coverage/lcov-report/test-utils/order-more/models/order.ts.html +45 -45
  144. package/coverage/lcov-report/test-utils/order-more/models/parcel-event.ts.html +6 -6
  145. package/coverage/lcov-report/test-utils/order-more/models/parcel-line-item.ts.html +7 -7
  146. package/coverage/lcov-report/test-utils/order-more/models/parcel.ts.html +2 -2
  147. package/coverage/lcov-report/test-utils/order-more/models/physical-address.ts.html +12 -12
  148. package/coverage/lcov-report/test-utils/order-more/models/product-variant-image.ts.html +7 -7
  149. package/coverage/lcov-report/test-utils/order-more/models/product-variant.ts.html +22 -22
  150. package/coverage/lcov-report/test-utils/order-more/models/product.ts.html +11 -11
  151. package/coverage/lcov-report/test-utils/order-more/models/refund.ts.html +11 -11
  152. package/coverage/lcov-report/test-utils/order-more/models/shipment-actual-product-variant.ts.html +9 -9
  153. package/coverage/lcov-report/test-utils/order-more/models/shipment.ts.html +4 -4
  154. package/coverage/lcov-report/test-utils/order-more/models/size.ts.html +4 -4
  155. package/coverage/lcov-report/test-utils/order-more/models/utm-medium.ts.html +15 -15
  156. package/coverage/lcov-report/test-utils/order-more/models/utm-source.ts.html +17 -17
  157. package/coverage/lcov-report/test-utils/six/entities.ts.html +1 -1
  158. package/coverage/lcov-report/test-utils/six/index.html +1 -1
  159. package/coverage/lcov-report/test-utils/six/models/customer.ts.html +3 -3
  160. package/coverage/lcov-report/test-utils/six/models/index.html +21 -21
  161. package/coverage/lcov-report/test-utils/six/models/line-item.ts.html +10 -10
  162. package/coverage/lcov-report/test-utils/six/models/order.ts.html +13 -13
  163. package/coverage/lcov-report/test-utils/six/models/parcel-line-item.ts.html +2 -2
  164. package/coverage/lcov-report/test-utils/six/models/parcel.ts.html +2 -2
  165. package/coverage/lcov-report/test-utils/thirteen/entities.ts.html +1 -1
  166. package/coverage/lcov-report/test-utils/thirteen/index.html +1 -1
  167. package/coverage/lcov-report/test-utils/thirteen/models/audience.ts.html +2 -2
  168. package/coverage/lcov-report/test-utils/thirteen/models/brand.ts.html +2 -2
  169. package/coverage/lcov-report/test-utils/thirteen/models/category.ts.html +1 -1
  170. package/coverage/lcov-report/test-utils/thirteen/models/index.html +14 -14
  171. package/coverage/lcov-report/test-utils/thirteen/models/member.ts.html +2 -2
  172. package/coverage/lcov-report/test-utils/thirteen/models/passion.ts.html +2 -2
  173. package/coverage/lcov-report/test-utils/thirteen/models/product.ts.html +10 -10
  174. package/coverage/lcov-report/test-utils/thirteen/models/recommendation-audience.ts.html +2 -2
  175. package/coverage/lcov-report/test-utils/thirteen/models/recommendation.ts.html +2 -2
  176. package/coverage/lcov-report/test-utils/three/index.html +1 -1
  177. package/coverage/lcov-report/test-utils/three/results.js.html +1 -1
  178. package/coverage/lcov-report/test-utils/twelve/entities.ts.html +1 -1
  179. package/coverage/lcov-report/test-utils/twelve/index.html +1 -1
  180. package/coverage/lcov-report/test-utils/twelve/models/index.html +1 -1
  181. package/coverage/lcov-report/test-utils/twelve/models/member.ts.html +1 -1
  182. package/coverage/lcov-report/test-utils/twelve/models/prompt.ts.html +2 -2
  183. package/coverage/lcov-report/test-utils/two/index.html +1 -1
  184. package/coverage/lcov-report/test-utils/two/results.js.html +1 -1
  185. package/coverage/lcov.info +2136 -1991
  186. package/dist/example/data-access/person.d.ts +1 -1
  187. package/dist/src/core.d.ts +13 -7
  188. package/dist/src/core.js +258 -189
  189. package/dist/src/core.spec.js +413 -0
  190. package/dist/src/driver-integrations/index.d.ts +5 -5
  191. package/dist/src/driver-integrations/pgp.spec.d.ts +1 -0
  192. package/dist/src/driver-integrations/pgp.spec.js +376 -0
  193. package/dist/src/orm.d.ts +9 -9
  194. package/dist/src/orm.js +137 -76
  195. package/dist/src/orm.spec.js +535 -85
  196. package/dist/test-utils/nine/models/feature-switch.d.ts +2 -2
  197. package/dist/test-utils/nine/models/feature-switch.ts +2 -2
  198. package/example/data-access/person.ts +1 -1
  199. package/package.json +9 -6
  200. package/scripts/bench-core.js +636 -0
  201. package/scripts/check-bench-scenarios.js +47 -0
  202. package/src/core.spec.ts +485 -2
  203. package/src/core.ts +369 -227
  204. package/src/driver-integrations/index.ts +5 -5
  205. package/src/driver-integrations/pgp.spec.ts +444 -0
  206. package/src/driver-integrations/pgp.ts +5 -5
  207. package/src/orm.spec.ts +592 -88
  208. package/src/orm.ts +173 -143
  209. package/test-utils/nine/models/feature-switch.ts +2 -2
package/src/core.ts CHANGED
@@ -51,6 +51,9 @@ export interface IEntityInternal<T extends IModel> {
51
51
  references: object;
52
52
  selectColumnsClause: string;
53
53
  getPkId: (model: IModel) => string;
54
+ columnToPropertyMap: Map<string, string>;
55
+ propertyToColumnMap: Map<string, string>;
56
+ referencesEntries: Array<{ property: string; ModelClass: IModelClass }>;
54
57
  }
55
58
  export type IEntitiesInternal<T extends IModel> = Array<IEntityInternal<T>>;
56
59
 
@@ -137,22 +140,38 @@ export const createCore = ({
137
140
  const _primaryKeys = pkColumnsData.map((x: IColumnInternal) => x.column);
138
141
  const primaryKeys = _primaryKeys.length > 0 ? _primaryKeys : ['id'];
139
142
 
140
- // Returns unique identifier of model (the values of the primary keys)
141
143
  const getPkId = (model: IModel): string => {
142
- return primaryKeys
143
- .map((key: string) => model[key as keyof typeof model])
144
- .join('');
144
+ let id = '';
145
+ for (let i = 0; i < primaryKeys.length; i++) {
146
+ const part = model[primaryKeys[i] as keyof typeof model];
147
+ if (part !== void 0 && part !== null) {
148
+ id += String(part);
149
+ }
150
+ }
151
+ return id;
145
152
  };
146
153
 
147
- const references = columns
148
- .filter((x: IColumnInternal) => x.references)
149
- .reduce(
150
- (accum: any, item: IColumnInternal) =>
151
- Object.assign({}, accum, {
152
- [item.property]: item.references
153
- }),
154
- {}
155
- );
154
+ const references: any = {};
155
+ const referencesEntries: Array<{
156
+ property: string;
157
+ ModelClass: IModelClass;
158
+ }> = [];
159
+ for (const col of columns) {
160
+ if (col.references) {
161
+ references[col.property] = col.references;
162
+ referencesEntries.push({
163
+ property: col.property,
164
+ ModelClass: col.references
165
+ });
166
+ }
167
+ }
168
+
169
+ const columnToPropertyMap = new Map<string, string>();
170
+ const propertyToColumnMap = new Map<string, string>();
171
+ for (let i = 0; i < columnNames.length; i++) {
172
+ columnToPropertyMap.set(columnNames[i], propertyNames[i]);
173
+ propertyToColumnMap.set(propertyNames[i], columnNames[i]);
174
+ }
156
175
 
157
176
  const selectColumnsClause = prefixedColumnNames
158
177
  .map(
@@ -174,7 +193,10 @@ export const createCore = ({
174
193
  primaryKeys,
175
194
  references,
176
195
  selectColumnsClause,
177
- getPkId
196
+ getPkId,
197
+ columnToPropertyMap,
198
+ propertyToColumnMap,
199
+ referencesEntries
178
200
  };
179
201
  }
180
202
  );
@@ -223,237 +245,357 @@ export const createCore = ({
223
245
  return getEntityByModelClass(model.constructor as IModelClass);
224
246
  };
225
247
 
226
- /*
227
- * In:
228
- * [
229
- * [Article {id: 32}, ArticleTag {id: 54}]
230
- * [Article {id: 32}, ArticleTag {id: 55}]
231
- * ]
232
- * Out:
233
- * Article {id: 32, ArticleTags articleTags: [ArticleTag {id: 54}, ArticleTag {id: 55}]
234
- */
235
- const nestClump = (clump: Array<Array<IModel>>): object => {
236
- clump = clump.map((x: Array<IModel>) => Object.values(x));
237
- const root = clump[0][0];
238
- clump = clump.map((row: Array<IModel>) =>
239
- row.filter((item: IModel, index: number) => index !== 0)
240
- );
241
- const built = { [getEntityByModel(root).displayName]: root };
242
-
243
- let nodes = [root];
244
-
245
- // Wowzer is this both CPU and Memory inefficient
246
- clump.forEach((array: Array<IModel>) => {
247
- array.forEach((_model: IModel) => {
248
- const nodeAlreadySeen = nodes.find(
249
- (x: IModel) =>
250
- x.constructor.name === _model.constructor.name &&
251
- getEntityByModel(x).getPkId(x) ===
252
- getEntityByModel(_model).getPkId(_model)
248
+ const entityReferencePlans = new Map<
249
+ IEntityInternal<IModel>,
250
+ Array<{ property: string; targetEntity: IEntityInternal<IModel> }>
251
+ >();
252
+ for (let i = 0; i < entities.length; i++) {
253
+ const entity = entities[i];
254
+ const plans = new Array<{
255
+ property: string;
256
+ targetEntity: IEntityInternal<IModel>;
257
+ }>(entity.referencesEntries.length);
258
+ for (let j = 0; j < entity.referencesEntries.length; j++) {
259
+ const ref = entity.referencesEntries[j];
260
+ plans[j] = {
261
+ property: ref.property,
262
+ targetEntity: getEntityByModelClass(ref.ModelClass)
263
+ };
264
+ }
265
+ entityReferencePlans.set(entity, plans);
266
+ }
267
+
268
+ interface IRowColumnPlan {
269
+ rowKey: string;
270
+ propertyName: string;
271
+ }
272
+ interface IEntityRowPlan {
273
+ entity: IEntityInternal<IModel>;
274
+ columnPlans: Array<IRowColumnPlan>;
275
+ primaryKeyRowKeys: Array<string>;
276
+ }
277
+
278
+ const getPkIdFromRow = (
279
+ row: any,
280
+ primaryKeyRowKeys: Array<string>
281
+ ): string => {
282
+ let id = '';
283
+ for (let i = 0; i < primaryKeyRowKeys.length; i++) {
284
+ const part = row[primaryKeyRowKeys[i]];
285
+ if (part !== void 0 && part !== null) {
286
+ id += String(part);
287
+ }
288
+ }
289
+ return id;
290
+ };
291
+
292
+ const buildEntityRowPlans = (sampleRow: any): Array<IEntityRowPlan> => {
293
+ const plansByTable = new Map<string, IEntityRowPlan>();
294
+ const tableOrder: Array<string> = [];
295
+ for (const text in sampleRow) {
296
+ if (!Object.prototype.hasOwnProperty.call(sampleRow, text)) {
297
+ continue;
298
+ }
299
+ const hashIndex = text.indexOf('#');
300
+ if (hashIndex === -1) {
301
+ throw new Error('Column names must be namespaced to table');
302
+ }
303
+ const tableName = text.substring(0, hashIndex);
304
+ const column = text.substring(hashIndex + 1);
305
+
306
+ let plan = plansByTable.get(tableName);
307
+ if (!plan) {
308
+ const entity = getEntityByTableName(tableName);
309
+ const primaryKeyRowKeys = entity.primaryKeys.map(
310
+ (pk: string) => `${tableName}#${pk}`
253
311
  );
254
- const model = nodeAlreadySeen || _model;
255
- const isNodeAlreadySeen = !!nodeAlreadySeen;
256
- const nodePointingToIt = nodes.find((node) => {
257
- const indexes = Object.values(getEntityByModel(node).references)
258
- .map((x: IModelClass, i: number) =>
259
- x === model.constructor ? i : null
260
- )
261
- .filter((x: number | null, i) => x != null) as Array<number>;
262
- if (!indexes.length) {
263
- return false;
264
- }
265
- for (const index of indexes) {
266
- const property = Object.keys(getEntityByModel(node).references)[
267
- index
268
- ];
269
- if (node[property] === model.id) {
270
- return true;
271
- }
272
- }
273
- return false;
274
- });
275
- // For first obj type which is has an instance in nodes array,
276
- // get its index in nodes array
277
- const indexOfOldestParent =
278
- array.reduce((answer: number | null, obj: IModel) => {
279
- if (answer != null) {
280
- return answer;
281
- }
282
- const index = nodes.findIndex(
283
- (n) => n.constructor === obj.constructor
284
- );
285
- if (index !== -1) {
286
- return index;
287
- }
288
- return null;
289
- }, null) || 0;
290
- const parentHeirarchy = [
291
- root,
292
- ...nodes.slice(0, indexOfOldestParent + 1).reverse()
293
- ];
294
- const nodeItPointsTo = parentHeirarchy.find((parent) => {
295
- const indexes = Object.values(getEntityByModel(model).references)
296
- .map((x: IModelClass, i: number) =>
297
- x === parent.constructor ? i : null
298
- )
299
- .filter((x: number | null, i) => x != null) as Array<number>;
300
- if (!indexes.length) {
301
- return false;
302
- }
303
- for (const index of indexes) {
304
- const property = Object.keys(getEntityByModel(model).references)[
305
- index
306
- ];
307
- if (model[property] === parent.id) {
308
- return true;
309
- }
310
- }
311
- return false;
312
- });
313
- if (isNodeAlreadySeen) {
314
- if (nodeItPointsTo && !nodePointingToIt) {
315
- nodes = [model, ...nodes];
316
- return;
317
- }
318
- // If the nodePointingToIt (eg, parcel_event) is part of an
319
- // existing collection on this node (eg, parcel) which is a
320
- // nodeAlreadySeen, early return so we don't create it (parcel) on
321
- // the nodePointingToIt (parcel_event), since it (parcel) has been
322
- // shown to be the parent (of parcel_events).
323
- if (nodePointingToIt) {
324
- const ec =
325
- model[
326
- getEntityByModel(nodePointingToIt)
327
- .collectionDisplayName as keyof typeof model
328
- ];
329
- if (ec && ec.models.find((m: IModel) => m === nodePointingToIt)) {
330
- nodes = [model, ...nodes];
331
- return;
332
- }
333
- }
334
- }
335
- if (nodePointingToIt) {
336
- nodePointingToIt[getEntityByModel(model).displayName] = model;
337
- } else if (nodeItPointsTo) {
338
- let collection =
339
- nodeItPointsTo[getEntityByModel(model).collectionDisplayName];
340
- if (collection) {
341
- collection.models.push(model);
342
- } else {
343
- const Collection = getEntityByModel(model).Collection;
344
- nodeItPointsTo[getEntityByModel(model).collectionDisplayName] =
345
- new Collection({
346
- models: [model]
347
- });
348
- }
312
+ plan = {
313
+ entity,
314
+ columnPlans: [],
315
+ primaryKeyRowKeys
316
+ };
317
+ plansByTable.set(tableName, plan);
318
+ tableOrder.push(tableName);
319
+ }
320
+
321
+ let propertyName = plan.entity.columnToPropertyMap.get(column);
322
+ if (!propertyName) {
323
+ if (column.startsWith('meta_')) {
324
+ propertyName = camelCase(column);
349
325
  } else {
350
- if (!getEntityByModel(model).getPkId(model)) {
351
- // If the join is fruitless; todo: add a test for this path
352
- return;
353
- }
354
326
  throw Error(
355
- `Could not find how this BO fits: ${JSON.stringify(model)} ${
356
- getEntityByModel(model).tableName
357
- }`
327
+ `No property name for "${column}" in business object "${plan.entity.displayName}". Non-spec'd columns must begin with "meta_".`
358
328
  );
359
329
  }
360
- nodes = [model, ...nodes];
330
+ }
331
+ plan.columnPlans.push({
332
+ rowKey: text,
333
+ propertyName
361
334
  });
362
- });
335
+ }
363
336
 
364
- return built;
337
+ const orderedPlans = new Array<IEntityRowPlan>(tableOrder.length);
338
+ for (let i = 0; i < tableOrder.length; i++) {
339
+ orderedPlans[i] = plansByTable.get(tableOrder[i]) as IEntityRowPlan;
340
+ }
341
+ return orderedPlans;
365
342
  };
366
343
 
367
- /*
368
- * Clump array of flat objects into groups based on id of root
369
- * In:
370
- * [
371
- * [Article {id: 32}, ArticleTag {id: 54}]
372
- * [Article {id: 32}, ArticleTag {id: 55}]
373
- * [Article {id: 33}, ArticleTag {id: 56}]
374
- * ]
375
- * Out:
376
- * [
377
- * [
378
- * [Article {id: 32}, ArticleTag {id: 54}]
379
- * [Article {id: 32}, ArticleTag {id: 55}]
380
- * ]
381
- * [
382
- * [Article {id: 33}, ArticleTag {id: 56}]
383
- * ]
384
- * ]
385
- */
386
- const clumpIntoGroups = (
387
- processed: Array<Array<IModel>>
388
- ): Array<Array<Array<IModel>>> => {
389
- const root = processed[0][0];
390
- const rootBo = root.constructor;
391
- const clumps = processed.reduce((accum: any, item: Array<IModel>) => {
392
- const id = getEntityByModel(root)
393
- .primaryKeys.map(
394
- (key: string) =>
395
- item.find((x: IModel) => x.constructor === rootBo)?.[key]
396
- )
397
- .join('@');
398
- if (accum.has(id)) {
399
- accum.set(id, [...accum.get(id), item]);
400
- } else {
401
- accum.set(id, [item]);
344
+ const materializeModelsFromRow = (
345
+ row: any,
346
+ entityRowPlans: Array<IEntityRowPlan>,
347
+ rootScopedModelsByEntity: IModelsByEntity,
348
+ rowModels: Array<IModel | void>,
349
+ rowModelPkIds: Array<string>,
350
+ rowCreatedWithPkIndexes: Array<number>
351
+ ): IModel => {
352
+ rowCreatedWithPkIndexes.length = 0;
353
+ for (let i = 0; i < entityRowPlans.length; i++) {
354
+ const plan = entityRowPlans[i];
355
+ const pkId = getPkIdFromRow(row, plan.primaryKeyRowKeys);
356
+ rowModelPkIds[i] = pkId;
357
+ if (pkId) {
358
+ let modelsForEntity = rootScopedModelsByEntity.get(plan.entity);
359
+ if (!modelsForEntity) {
360
+ modelsForEntity = new Map<string, IModel>();
361
+ rootScopedModelsByEntity.set(plan.entity, modelsForEntity);
362
+ } else {
363
+ const existing = modelsForEntity.get(pkId);
364
+ if (existing) {
365
+ rowModels[i] = existing;
366
+ continue;
367
+ }
368
+ }
369
+ } else if (i !== 0) {
370
+ // No primary key means this is typically an outer-joined null row.
371
+ // Skip model construction for non-root entities since it cannot link.
372
+ rowModels[i] = void 0;
373
+ continue;
402
374
  }
403
- return accum;
404
- }, new Map());
405
- return [...clumps.values()];
375
+ const props: any = {};
376
+ for (let j = 0; j < plan.columnPlans.length; j++) {
377
+ const columnPlan = plan.columnPlans[j];
378
+ props[columnPlan.propertyName] = row[columnPlan.rowKey];
379
+ }
380
+ const model = new plan.entity.Model(props);
381
+ if (pkId) {
382
+ // modelsForEntity is guaranteed to be initialized above for pk rows.
383
+ (rootScopedModelsByEntity.get(plan.entity) as Map<string, IModel>).set(
384
+ pkId,
385
+ model
386
+ );
387
+ rowCreatedWithPkIndexes.push(i);
388
+ }
389
+ rowModels[i] = model;
390
+ }
391
+ return rowModels[0] as IModel;
406
392
  };
407
393
 
408
- const mapToBos = (objectified: any) => {
409
- return Object.keys(objectified).map((tableName) => {
410
- const entity = getEntityByTableName(tableName);
411
- const propified = Object.keys(objectified[tableName]).reduce(
412
- (obj: any, column) => {
413
- let propertyName =
414
- entity.propertyNames[entity.columnNames.indexOf(column)];
415
- if (!propertyName) {
416
- if (column.startsWith('meta_')) {
417
- propertyName = camelCase(column);
418
- } else {
419
- throw Error(
420
- `No property name for "${column}" in business object "${entity.displayName}". Non-spec'd columns must begin with "meta_".`
421
- );
422
- }
423
- }
424
- obj[propertyName] = objectified[tableName][column];
425
- return obj;
426
- },
427
- {}
428
- );
429
- return new entity.Model(propified);
430
- });
394
+ type IModelsByEntity = Map<IEntityInternal<IModel>, Map<string, IModel>>;
395
+ interface IRootScopeState {
396
+ modelsByEntity: IModelsByEntity;
397
+ collectionMembership?: WeakMap<
398
+ IModel,
399
+ Map<IEntityInternal<IModel>, Set<string>>
400
+ >;
401
+ }
402
+ type IRootScopeStateByKey = Map<string, IRootScopeState>;
403
+
404
+ const getRootScopeKey = (
405
+ row: any,
406
+ rootEntity: IEntityInternal<IModel>,
407
+ rootPrimaryKeys: Array<string>
408
+ ): string => {
409
+ let rootScopeKey = '';
410
+ for (let i = 0; i < rootPrimaryKeys.length; i++) {
411
+ if (i > 0) {
412
+ rootScopeKey += '@';
413
+ }
414
+ const value = row[`${rootEntity.tableName}#${rootPrimaryKeys[i]}`];
415
+ rootScopeKey += value === void 0 || value === null ? '' : String(value);
416
+ }
417
+ return rootScopeKey;
431
418
  };
432
419
 
433
- /*
434
- * Make objects (based on special table#column names) from flat database
435
- * return value.
436
- */
437
- const objectifyDatabaseResult = (result: object) => {
438
- return Object.keys(result).reduce((obj: any, text: string) => {
439
- const tableName = text.split('#')[0];
440
- const column = text.split('#')[1];
441
- if (!tableName || !column) {
442
- throw new Error('Column names must be namespaced to table');
443
- }
444
- obj[tableName] = obj[tableName] || {};
445
- obj[tableName][column] = result[text as keyof typeof result];
446
- return obj;
447
- }, {});
420
+ const ensureRootScopeState = (
421
+ rootScopeKey: string,
422
+ rootScopeStateByKey: IRootScopeStateByKey
423
+ ): IRootScopeState => {
424
+ let state = rootScopeStateByKey.get(rootScopeKey);
425
+ if (!state) {
426
+ state = {
427
+ modelsByEntity: new Map<IEntityInternal<IModel>, Map<string, IModel>>()
428
+ };
429
+ rootScopeStateByKey.set(rootScopeKey, state);
430
+ }
431
+ return state;
432
+ };
433
+
434
+ const ensureCollectionMembership = (
435
+ rootScopeState: IRootScopeState
436
+ ): WeakMap<IModel, Map<IEntityInternal<IModel>, Set<string>>> => {
437
+ if (!rootScopeState.collectionMembership) {
438
+ rootScopeState.collectionMembership = new WeakMap<
439
+ IModel,
440
+ Map<IEntityInternal<IModel>, Set<string>>
441
+ >();
442
+ }
443
+ return rootScopeState.collectionMembership;
444
+ };
445
+
446
+ const linkSourceToTarget = ({
447
+ sourceEntity,
448
+ sourceModel,
449
+ sourceModelPkId,
450
+ targetEntity,
451
+ targetModel,
452
+ collectionMembership
453
+ }: {
454
+ sourceEntity: IEntityInternal<IModel>;
455
+ sourceModel: IModel;
456
+ sourceModelPkId: string;
457
+ targetEntity: IEntityInternal<IModel>;
458
+ targetModel: IModel;
459
+ collectionMembership: WeakMap<
460
+ IModel,
461
+ Map<IEntityInternal<IModel>, Set<string>>
462
+ >;
463
+ }) => {
464
+ sourceModel[targetEntity.displayName as keyof typeof sourceModel] =
465
+ targetModel;
466
+
467
+ let collection =
468
+ targetModel[
469
+ sourceEntity.collectionDisplayName as keyof typeof targetModel
470
+ ];
471
+ if (!collection) {
472
+ const Collection = sourceEntity.Collection;
473
+ collection = new Collection({ models: [] });
474
+ targetModel[
475
+ sourceEntity.collectionDisplayName as keyof typeof targetModel
476
+ ] = collection;
477
+ }
478
+
479
+ let byCollection = collectionMembership.get(targetModel);
480
+ if (!byCollection) {
481
+ byCollection = new Map<IEntityInternal<IModel>, Set<string>>();
482
+ collectionMembership.set(targetModel, byCollection);
483
+ }
484
+ let memberIds = byCollection.get(sourceEntity);
485
+ if (!memberIds) {
486
+ memberIds = new Set<string>();
487
+ byCollection.set(sourceEntity, memberIds);
488
+ }
489
+ if (!memberIds.has(sourceModelPkId)) {
490
+ collection.models.push(sourceModel);
491
+ memberIds.add(sourceModelPkId);
492
+ }
448
493
  };
449
494
 
495
+ /*
496
+ * createFromDatabase architecture:
497
+ * 1) Compile row plans once (column -> property mapping per entity/table).
498
+ * 2) Materialize models per row with scoped de-duplication by root scope key.
499
+ * 3) Index models by root scope + entity + entity primary key.
500
+ * 4) Link refs incrementally as new models appear.
501
+ * 5) Return root models in first-seen root scope order.
502
+ */
450
503
  const createFromDatabase = <T extends ICollection<IModel>>(rows: any): T => {
451
504
  const result = Array.isArray(rows) ? rows : [rows];
452
- const objectified = result.map(objectifyDatabaseResult);
453
- const boified = objectified.map(mapToBos);
454
- const clumps = clumpIntoGroups(boified);
455
- const nested = clumps.map(nestClump);
456
- const models = nested.map((n) => Object.values(n)[0]);
505
+ const len = result.length;
506
+ const entityRowPlans = buildEntityRowPlans(result[0]);
507
+ const selectedEntities = new Set<IEntityInternal<IModel>>();
508
+ for (let i = 0; i < entityRowPlans.length; i++) {
509
+ selectedEntities.add(entityRowPlans[i].entity);
510
+ }
511
+ const applicableRefPlans = new Map<
512
+ IEntityInternal<IModel>,
513
+ Array<{ property: string; targetEntity: IEntityInternal<IModel> }>
514
+ >();
515
+ for (let i = 0; i < entityRowPlans.length; i++) {
516
+ const entity = entityRowPlans[i].entity;
517
+ const refs = entityReferencePlans.get(entity) || [];
518
+ const filteredRefs = refs.filter((ref) =>
519
+ selectedEntities.has(ref.targetEntity)
520
+ );
521
+ applicableRefPlans.set(entity, filteredRefs);
522
+ }
523
+ const rootEntity = entityRowPlans[0].entity;
524
+ const rootPrimaryKeys = rootEntity.primaryKeys;
525
+ const rootScopeOrder: Array<string> = [];
526
+ const rootModelsByScopeKey = new Map<string, IModel>();
527
+ const rootScopeStateByKey: IRootScopeStateByKey = new Map();
528
+ let currentRootScopeKey: string | void = void 0;
529
+ let currentRootScopeState: IRootScopeState | void = void 0;
530
+ const rowModels = new Array<IModel | void>(entityRowPlans.length);
531
+ const rowModelPkIds = new Array<string>(entityRowPlans.length);
532
+ const rowCreatedWithPkIndexes: Array<number> = [];
533
+
534
+ // Phase 1: materialize and index model instances by root scope + entity.
535
+ for (let i = 0; i < len; i++) {
536
+ const row = result[i];
537
+ const rootScopeKey = getRootScopeKey(row, rootEntity, rootPrimaryKeys);
538
+ let rootScopeState: IRootScopeState | void = currentRootScopeState;
539
+ if (!rootScopeState || rootScopeKey !== currentRootScopeKey) {
540
+ rootScopeState = ensureRootScopeState(
541
+ rootScopeKey,
542
+ rootScopeStateByKey
543
+ );
544
+ currentRootScopeKey = rootScopeKey;
545
+ currentRootScopeState = rootScopeState;
546
+ }
547
+ const rootModel = materializeModelsFromRow(
548
+ row,
549
+ entityRowPlans,
550
+ rootScopeState.modelsByEntity,
551
+ rowModels,
552
+ rowModelPkIds,
553
+ rowCreatedWithPkIndexes
554
+ );
555
+ if (!rootModelsByScopeKey.has(rootScopeKey)) {
556
+ rootScopeOrder.push(rootScopeKey);
557
+ rootModelsByScopeKey.set(rootScopeKey, rootModel);
558
+ }
559
+
560
+ for (let c = 0; c < rowCreatedWithPkIndexes.length; c++) {
561
+ const j = rowCreatedWithPkIndexes[c];
562
+ const sourceModel = rowModels[j] as IModel;
563
+ const sourceModelPkId = rowModelPkIds[j];
564
+ const sourceEntity = entityRowPlans[j].entity;
565
+
566
+ const refs = applicableRefPlans.get(sourceEntity);
567
+ if (!refs || refs.length === 0) {
568
+ continue;
569
+ }
570
+
571
+ for (let r = 0; r < refs.length; r++) {
572
+ const ref = refs[r];
573
+ const refId = sourceModel[ref.property as keyof typeof sourceModel];
574
+ if (refId == null) {
575
+ continue;
576
+ }
577
+ const targetPkId = String(refId);
578
+ const target = rootScopeState.modelsByEntity
579
+ .get(ref.targetEntity)
580
+ ?.get(targetPkId);
581
+ if (target) {
582
+ linkSourceToTarget({
583
+ sourceEntity,
584
+ sourceModel,
585
+ sourceModelPkId,
586
+ targetEntity: ref.targetEntity,
587
+ targetModel: target,
588
+ collectionMembership: ensureCollectionMembership(rootScopeState)
589
+ });
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ const models = new Array<IModel>(rootScopeOrder.length);
596
+ for (let i = 0; i < rootScopeOrder.length; i++) {
597
+ models[i] = rootModelsByScopeKey.get(rootScopeOrder[i]) as IModel;
598
+ }
457
599
  const Collection = getEntityByModel(models[0]).Collection;
458
600
  return <T>new Collection({ models });
459
601
  };