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
@@ -0,0 +1,47 @@
1
+ /* eslint-disable no-console */
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const benchPath = path.resolve(__dirname, 'bench-core.js');
6
+ const source = fs.readFileSync(benchPath, 'utf8');
7
+
8
+ const failures = [];
9
+
10
+ const requirePattern = (pattern, message) => {
11
+ if (!pattern.test(source)) {
12
+ failures.push(message);
13
+ }
14
+ };
15
+
16
+ // Guardrail: keep broad fixture-mixing scenarios.
17
+ requirePattern(
18
+ /label:\s*'stress\/mixed-fixtures x80'/,
19
+ "Missing stress scenario label 'stress/mixed-fixtures x80'."
20
+ );
21
+ requirePattern(
22
+ /label:\s*'stress\/mixed-sparse x80'/,
23
+ "Missing stress scenario label 'stress/mixed-sparse x80'."
24
+ );
25
+
26
+ // Guardrail: keep heterogeneous input families and adversarial ordering.
27
+ requirePattern(
28
+ /baseRowsSet:\s*\[[^\]]*seven[^\]]*eight[^\]]*ten[^\]]*eleven[^\]]*\]/s,
29
+ 'Missing heterogeneous baseRowsSet coverage over multiple fixtures.'
30
+ );
31
+ requirePattern(
32
+ /shuffleRows:\s*true/,
33
+ 'Missing shuffled-row stress coverage (shuffleRows: true).'
34
+ );
35
+
36
+ if (failures.length > 0) {
37
+ console.error('\nBenchmark scenario guard failed:');
38
+ for (const failure of failures) {
39
+ console.error(`- ${failure}`);
40
+ }
41
+ console.error(
42
+ '\nThis guard prevents benchmark drift back to narrow, overfit scenarios.'
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log('Benchmark scenario guard passed.');
package/src/core.spec.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable max-len */
2
- import { createCore } from './core';
2
+ import { createCore, IModel, ICollection, IColumns } from './core';
3
3
  import { entities as orderEntities } from '../test-utils/order/entities';
4
4
  import { entities as blogEntities } from '../test-utils/blog/entities';
5
5
  import { entities as orderMoreEntities } from '../test-utils/order-more/entities';
@@ -9,7 +9,10 @@ import { entities as sixEntities } from '../test-utils/six/entities';
9
9
  import { entities as twelveEntities } from '../test-utils/twelve/entities';
10
10
  import { entities as thirteenEntities } from '../test-utils/thirteen/entities';
11
11
  import { entities as fourteenEntities } from '../test-utils/fourteen/entities';
12
- import { Articles } from '../test-utils/blog/models/article';
12
+ import { Articles, Article } from '../test-utils/blog/models/article';
13
+ import { Order, Orders } from '../test-utils/order/models/order';
14
+ import { UtmSource } from '../test-utils/order/models/utm-source';
15
+ import { FeatureSwitch } from '../test-utils/nine/models/feature-switch';
13
16
  const two = require('../test-utils/two/results');
14
17
  const three = require('../test-utils/three/results');
15
18
  const one = require('../test-utils/one/results.json');
@@ -1186,3 +1189,483 @@ test('tables', () => {
1186
1189
  '"product".id as "product#id", "product".vendor_id as "product#vendor_id", "product".value as "product#value", "product".label as "product#label", "product".product_type as "product#product_type", "product".created_date as "product#created_date", "product".updated_date as "product#updated_date", "product".published_date as "product#published_date", "product".category as "product#category"'
1187
1190
  );
1188
1191
  });
1192
+
1193
+ /* -------------------------------------------------------------------------*/
1194
+ /* getEntityByModel --------------------------------------------------------*/
1195
+ /* -------------------------------------------------------------------------*/
1196
+
1197
+ describe('getEntityByModel', () => {
1198
+ test('returns entity for a model instance', () => {
1199
+ const core = createCore({ entities: orderEntities });
1200
+ const order = new Order({ id: 1, email: 'x@x.com' });
1201
+ const entity = core.getEntityByModel(order);
1202
+ expect(entity.tableName).toEqual('order');
1203
+ expect(entity.displayName).toEqual('order');
1204
+ expect(entity.Model).toBe(Order);
1205
+ expect(entity.Collection).toBe(Orders);
1206
+ });
1207
+
1208
+ test('returns correct entity among multiple registered entities', () => {
1209
+ const core = createCore({ entities: orderEntities });
1210
+ const utm = new UtmSource({
1211
+ id: 1,
1212
+ value: 'google',
1213
+ label: 'Google',
1214
+ internal: 'false'
1215
+ });
1216
+ const entity = core.getEntityByModel(utm);
1217
+ expect(entity.tableName).toEqual('utm_source');
1218
+ expect(entity.displayName).toEqual('utmSource');
1219
+ });
1220
+
1221
+ test('throws for an unregistered model class', () => {
1222
+ const core = createCore({ entities: nineEntities });
1223
+ const order = new Order({ id: 1 });
1224
+ expect(() => core.getEntityByModel(order)).toThrow(
1225
+ 'Could not find entity for class'
1226
+ );
1227
+ });
1228
+
1229
+ test('entity has correct propertyNames derived from columns', () => {
1230
+ const core = createCore({ entities: orderEntities });
1231
+ const order = new Order({ id: 1 });
1232
+ const entity = core.getEntityByModel(order);
1233
+ expect(entity.propertyNames).toContain('id');
1234
+ expect(entity.propertyNames).toContain('email');
1235
+ expect(entity.propertyNames).toContain('browserIP');
1236
+ expect(entity.propertyNames).toContain('utmSourceId');
1237
+ });
1238
+
1239
+ test('entity has correct columnNames derived from columns', () => {
1240
+ const core = createCore({ entities: orderEntities });
1241
+ const order = new Order({ id: 1 });
1242
+ const entity = core.getEntityByModel(order);
1243
+ expect(entity.columnNames).toContain('id');
1244
+ expect(entity.columnNames).toContain('email');
1245
+ expect(entity.columnNames).toContain('browser_ip');
1246
+ expect(entity.columnNames).toContain('utm_source_id');
1247
+ });
1248
+
1249
+ test('entity has correct prefixedColumnNames', () => {
1250
+ const core = createCore({ entities: orderEntities });
1251
+ const order = new Order({ id: 1 });
1252
+ const entity = core.getEntityByModel(order);
1253
+ expect(entity.prefixedColumnNames).toContain('order#id');
1254
+ expect(entity.prefixedColumnNames).toContain('order#email');
1255
+ expect(entity.prefixedColumnNames).toContain('order#browser_ip');
1256
+ });
1257
+
1258
+ test('entity has correct references', () => {
1259
+ const core = createCore({ entities: orderEntities });
1260
+ const order = new Order({ id: 1 });
1261
+ const entity = core.getEntityByModel(order);
1262
+ expect(entity.references).toEqual({ utmSourceId: UtmSource });
1263
+ });
1264
+
1265
+ test('entity defaults primaryKeys to [id] when none specified', () => {
1266
+ const core = createCore({ entities: orderEntities });
1267
+ const order = new Order({ id: 1 });
1268
+ const entity = core.getEntityByModel(order);
1269
+ expect(entity.primaryKeys).toEqual(['id']);
1270
+ });
1271
+
1272
+ test('entity selectColumnsClause is correctly formed', () => {
1273
+ const core = createCore({ entities: nineEntities });
1274
+ const fs = new FeatureSwitch({ id: 'x', label: 'x', on: true });
1275
+ const entity = core.getEntityByModel(fs);
1276
+ expect(entity.selectColumnsClause).toEqual(
1277
+ '"feature_switch".id as "feature_switch#id", "feature_switch".label as "feature_switch#label", "feature_switch".on as "feature_switch#on"'
1278
+ );
1279
+ });
1280
+ });
1281
+
1282
+ /* -------------------------------------------------------------------------*/
1283
+ /* getEntityByTableName ----------------------------------------------------*/
1284
+ /* -------------------------------------------------------------------------*/
1285
+
1286
+ describe('getEntityByTableName', () => {
1287
+ test('returns entity for a valid table name', () => {
1288
+ const core = createCore({ entities: orderEntities });
1289
+ const entity = core.getEntityByTableName('order');
1290
+ expect(entity.tableName).toEqual('order');
1291
+ expect(entity.Model).toBe(Order);
1292
+ });
1293
+
1294
+ test('returns correct entity among multiple registered entities', () => {
1295
+ const core = createCore({ entities: orderEntities });
1296
+ const entity = core.getEntityByTableName('utm_source');
1297
+ expect(entity.tableName).toEqual('utm_source');
1298
+ expect(entity.displayName).toEqual('utmSource');
1299
+ expect(entity.Model).toBe(UtmSource);
1300
+ });
1301
+
1302
+ test('throws for a non-existent table name', () => {
1303
+ const core = createCore({ entities: orderEntities });
1304
+ expect(() => core.getEntityByTableName('nonexistent')).toThrow(
1305
+ 'Could not find entity for table nonexistent'
1306
+ );
1307
+ });
1308
+ });
1309
+
1310
+ /* -------------------------------------------------------------------------*/
1311
+ /* Entity configuration edge cases -----------------------------------------*/
1312
+ /* -------------------------------------------------------------------------*/
1313
+
1314
+ describe('entity configuration', () => {
1315
+ test('custom displayName is used instead of camelCase tableName', () => {
1316
+ class Widget implements IModel {
1317
+ id: number;
1318
+ constructor(props: any) {
1319
+ this.id = props.id;
1320
+ }
1321
+ }
1322
+ class Widgets implements ICollection<Widget> {
1323
+ models: Array<Widget>;
1324
+ constructor({ models }: any) {
1325
+ this.models = models;
1326
+ }
1327
+ }
1328
+ const core = createCore({
1329
+ entities: [
1330
+ {
1331
+ tableName: 'widget_thing',
1332
+ displayName: 'myWidget',
1333
+ columns: ['id'],
1334
+ Model: Widget,
1335
+ Collection: Widgets
1336
+ }
1337
+ ]
1338
+ });
1339
+ expect(core.tables.myWidget).toBeDefined();
1340
+ expect(core.tables.myWidget.columns).toEqual(
1341
+ '"widget_thing".id as "widget_thing#id"'
1342
+ );
1343
+ });
1344
+
1345
+ test('custom collectionDisplayName is used', () => {
1346
+ class Goose implements IModel {
1347
+ id: number;
1348
+ constructor(props: any) {
1349
+ this.id = props.id;
1350
+ }
1351
+ }
1352
+ class Geese implements ICollection<Goose> {
1353
+ models: Array<Goose>;
1354
+ constructor({ models }: any) {
1355
+ this.models = models;
1356
+ }
1357
+ }
1358
+ const core = createCore({
1359
+ entities: [
1360
+ {
1361
+ tableName: 'goose',
1362
+ collectionDisplayName: 'geese',
1363
+ columns: ['id'],
1364
+ Model: Goose,
1365
+ Collection: Geese
1366
+ }
1367
+ ]
1368
+ });
1369
+ const entity = core.getEntityByTableName('goose');
1370
+ expect(entity.collectionDisplayName).toEqual('geese');
1371
+ });
1372
+
1373
+ test('default collectionDisplayName appends "s" to displayName', () => {
1374
+ const core = createCore({ entities: orderEntities });
1375
+ const entity = core.getEntityByTableName('order');
1376
+ expect(entity.collectionDisplayName).toEqual('orders');
1377
+ });
1378
+
1379
+ test('function-based columns are resolved', () => {
1380
+ class Item implements IModel {
1381
+ id: number;
1382
+ name: string;
1383
+ constructor(props: any) {
1384
+ this.id = props.id;
1385
+ this.name = props.name;
1386
+ }
1387
+ }
1388
+ class Items implements ICollection<Item> {
1389
+ models: Array<Item>;
1390
+ constructor({ models }: any) {
1391
+ this.models = models;
1392
+ }
1393
+ }
1394
+ const columnsFn: IColumns = () => ['id', 'name'];
1395
+ const core = createCore({
1396
+ entities: [
1397
+ {
1398
+ tableName: 'item',
1399
+ columns: columnsFn,
1400
+ Model: Item,
1401
+ Collection: Items
1402
+ }
1403
+ ]
1404
+ });
1405
+ const entity = core.getEntityByTableName('item');
1406
+ expect(entity.columnNames).toEqual(['id', 'name']);
1407
+ expect(entity.propertyNames).toEqual(['id', 'name']);
1408
+ });
1409
+
1410
+ test('custom primary key is used', () => {
1411
+ class Tenant implements IModel {
1412
+ slug: string;
1413
+ name: string;
1414
+ constructor(props: any) {
1415
+ this.slug = props.slug;
1416
+ this.name = props.name;
1417
+ }
1418
+ }
1419
+ class Tenants implements ICollection<Tenant> {
1420
+ models: Array<Tenant>;
1421
+ constructor({ models }: any) {
1422
+ this.models = models;
1423
+ }
1424
+ }
1425
+ const core = createCore({
1426
+ entities: [
1427
+ {
1428
+ tableName: 'tenant',
1429
+ columns: [{ column: 'slug', primaryKey: true }, 'name'],
1430
+ Model: Tenant,
1431
+ Collection: Tenants
1432
+ }
1433
+ ]
1434
+ });
1435
+ const entity = core.getEntityByTableName('tenant');
1436
+ expect(entity.primaryKeys).toEqual(['slug']);
1437
+ });
1438
+
1439
+ test('composite primary keys', () => {
1440
+ class Mapping implements IModel {
1441
+ aId: number;
1442
+ bId: number;
1443
+ constructor(props: any) {
1444
+ this.aId = props.aId;
1445
+ this.bId = props.bId;
1446
+ }
1447
+ }
1448
+ class Mappings implements ICollection<Mapping> {
1449
+ models: Array<Mapping>;
1450
+ constructor({ models }: any) {
1451
+ this.models = models;
1452
+ }
1453
+ }
1454
+ const core = createCore({
1455
+ entities: [
1456
+ {
1457
+ tableName: 'mapping',
1458
+ columns: [
1459
+ { column: 'a_id', primaryKey: true },
1460
+ { column: 'b_id', primaryKey: true }
1461
+ ],
1462
+ Model: Mapping,
1463
+ Collection: Mappings
1464
+ }
1465
+ ]
1466
+ });
1467
+ const entity = core.getEntityByTableName('mapping');
1468
+ expect(entity.primaryKeys).toEqual(['a_id', 'b_id']);
1469
+ });
1470
+
1471
+ test('column with explicit property name overrides camelCase', () => {
1472
+ class Thing implements IModel {
1473
+ myIP: string;
1474
+ constructor(props: any) {
1475
+ this.myIP = props.myIP;
1476
+ }
1477
+ }
1478
+ class Things implements ICollection<Thing> {
1479
+ models: Array<Thing>;
1480
+ constructor({ models }: any) {
1481
+ this.models = models;
1482
+ }
1483
+ }
1484
+ const core = createCore({
1485
+ entities: [
1486
+ {
1487
+ tableName: 'thing',
1488
+ columns: [{ column: 'ip_address', property: 'myIP' }],
1489
+ Model: Thing,
1490
+ Collection: Things
1491
+ }
1492
+ ]
1493
+ });
1494
+ const entity = core.getEntityByTableName('thing');
1495
+ expect(entity.propertyNames).toEqual(['myIP']);
1496
+ expect(entity.columnNames).toEqual(['ip_address']);
1497
+ });
1498
+
1499
+ test('getPkId returns concatenated primary key values', () => {
1500
+ class Mapping implements IModel {
1501
+ a_id: number;
1502
+ b_id: number;
1503
+ constructor(props: any) {
1504
+ this.a_id = props.aId;
1505
+ this.b_id = props.bId;
1506
+ }
1507
+ }
1508
+ class Mappings implements ICollection<Mapping> {
1509
+ models: Array<Mapping>;
1510
+ constructor({ models }: any) {
1511
+ this.models = models;
1512
+ }
1513
+ }
1514
+ const core = createCore({
1515
+ entities: [
1516
+ {
1517
+ tableName: 'mapping',
1518
+ columns: [
1519
+ { column: 'a_id', primaryKey: true },
1520
+ { column: 'b_id', primaryKey: true }
1521
+ ],
1522
+ Model: Mapping,
1523
+ Collection: Mappings
1524
+ }
1525
+ ]
1526
+ });
1527
+ const entity = core.getEntityByTableName('mapping');
1528
+ const m = new Mapping({ aId: 5, bId: 10 });
1529
+ expect(entity.getPkId(m)).toEqual('510');
1530
+ });
1531
+
1532
+ test('entity with no columns other than id', () => {
1533
+ class Simple implements IModel {
1534
+ id: number;
1535
+ constructor(props: any) {
1536
+ this.id = props.id;
1537
+ }
1538
+ }
1539
+ class Simples implements ICollection<Simple> {
1540
+ models: Array<Simple>;
1541
+ constructor({ models }: any) {
1542
+ this.models = models;
1543
+ }
1544
+ }
1545
+ const core = createCore({
1546
+ entities: [
1547
+ {
1548
+ tableName: 'simple',
1549
+ columns: ['id'],
1550
+ Model: Simple,
1551
+ Collection: Simples
1552
+ }
1553
+ ]
1554
+ });
1555
+ const entity = core.getEntityByTableName('simple');
1556
+ expect(entity.columnNames).toEqual(['id']);
1557
+ expect(entity.propertyNames).toEqual(['id']);
1558
+ expect(entity.primaryKeys).toEqual(['id']);
1559
+ expect(entity.references).toEqual({});
1560
+ });
1561
+ });
1562
+
1563
+ /* -------------------------------------------------------------------------*/
1564
+ /* createFromDatabase error/edge cases -------------------------------------*/
1565
+ /* -------------------------------------------------------------------------*/
1566
+
1567
+ describe('createFromDatabase edge cases', () => {
1568
+ test('handles a single row (not an array)', () => {
1569
+ const core = createCore({ entities: nineEntities });
1570
+ const row = {
1571
+ 'feature_switch#id': 'test_switch',
1572
+ 'feature_switch#label': 'Test',
1573
+ 'feature_switch#on': true
1574
+ };
1575
+ const result = core.createFromDatabase(row);
1576
+ expect(result.models.length).toEqual(1);
1577
+ expect(result.models[0].id).toEqual('test_switch');
1578
+ });
1579
+
1580
+ test('throws when column names are not namespaced', () => {
1581
+ const core = createCore({ entities: nineEntities });
1582
+ expect(() =>
1583
+ core.createFromDatabase([{ id: 1, label: 'test', on: true }])
1584
+ ).toThrow('Column names must be namespaced to table');
1585
+ });
1586
+
1587
+ test('handles meta_ prefixed columns', () => {
1588
+ class Widget implements IModel {
1589
+ [key: string]: any;
1590
+ id: number;
1591
+ constructor(props: any) {
1592
+ this.id = props.id;
1593
+ Object.assign(this, props);
1594
+ }
1595
+ }
1596
+ class Widgets implements ICollection<Widget> {
1597
+ models: Array<Widget>;
1598
+ constructor({ models }: any) {
1599
+ this.models = models;
1600
+ }
1601
+ }
1602
+ const core = createCore({
1603
+ entities: [
1604
+ {
1605
+ tableName: 'widget',
1606
+ columns: ['id'],
1607
+ Model: Widget,
1608
+ Collection: Widgets
1609
+ }
1610
+ ]
1611
+ });
1612
+ const result = core.createFromDatabase([
1613
+ { 'widget#id': 1, 'widget#meta_count': 42 }
1614
+ ]);
1615
+ expect(result.models[0].id).toEqual(1);
1616
+ expect(result.models[0].metaCount).toEqual(42);
1617
+ });
1618
+
1619
+ test('throws for non-meta unrecognized columns', () => {
1620
+ class Widget implements IModel {
1621
+ id: number;
1622
+ constructor(props: any) {
1623
+ this.id = props.id;
1624
+ }
1625
+ }
1626
+ class Widgets implements ICollection<Widget> {
1627
+ models: Array<Widget>;
1628
+ constructor({ models }: any) {
1629
+ this.models = models;
1630
+ }
1631
+ }
1632
+ const core = createCore({
1633
+ entities: [
1634
+ {
1635
+ tableName: 'widget',
1636
+ columns: ['id'],
1637
+ Model: Widget,
1638
+ Collection: Widgets
1639
+ }
1640
+ ]
1641
+ });
1642
+ expect(() =>
1643
+ core.createFromDatabase([{ 'widget#id': 1, 'widget#unknown_col': 'x' }])
1644
+ ).toThrow('No property name for "unknown_col"');
1645
+ });
1646
+ });
1647
+
1648
+ /* -------------------------------------------------------------------------*/
1649
+ /* tables property ---------------------------------------------------------*/
1650
+ /* -------------------------------------------------------------------------*/
1651
+
1652
+ describe('tables property', () => {
1653
+ test('uses displayName as key', () => {
1654
+ const core = createCore({ entities: blogEntities });
1655
+ expect(core.tables.article).toBeDefined();
1656
+ expect(core.tables.person).toBeDefined();
1657
+ expect(core.tables.articleTag).toBeDefined();
1658
+ expect(core.tables.tag).toBeDefined();
1659
+ });
1660
+
1661
+ test('each table entry has a columns string', () => {
1662
+ const core = createCore({ entities: nineEntities });
1663
+ expect(typeof core.tables.featureSwitch.columns).toBe('string');
1664
+ expect(core.tables.featureSwitch.columns).toContain('feature_switch');
1665
+ });
1666
+
1667
+ test('single entity produces single table entry', () => {
1668
+ const core = createCore({ entities: nineEntities });
1669
+ expect(Object.keys(core.tables).length).toEqual(1);
1670
+ });
1671
+ });