velocious 1.0.643 → 1.0.645

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 (279) hide show
  1. package/README.md +21 -10
  2. package/build/authorization/ability.js +36 -13
  3. package/build/background-jobs/adapter.js +9 -0
  4. package/build/background-jobs/main.js +35 -10
  5. package/build/background-jobs/store.js +217 -38
  6. package/build/background-jobs/types.js +15 -0
  7. package/build/configuration-types.js +1 -1
  8. package/build/configuration.js +45 -9
  9. package/build/database/drivers/base-column.js +8 -0
  10. package/build/database/drivers/mssql/sql/update.js +1 -3
  11. package/build/database/drivers/mysql/sql/update.js +1 -3
  12. package/build/database/drivers/pgsql/column.js +22 -0
  13. package/build/database/drivers/pgsql/sql/update.js +1 -3
  14. package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
  15. package/build/database/drivers/sqlite/sql/update.js +1 -3
  16. package/build/database/query/index.js +2 -2
  17. package/build/database/query/model-class-query.js +26 -10
  18. package/build/database/query/preloader/has-many.js +4 -1
  19. package/build/database/query/query-data.js +2 -1
  20. package/build/database/query/update-base.js +14 -0
  21. package/build/database/query/with-count.js +3 -1
  22. package/build/database/record/acts-as-list.js +37 -13
  23. package/build/database/record/attachments/store.js +336 -26
  24. package/build/database/record/auditing.js +6 -3
  25. package/build/database/record/index.js +197 -42
  26. package/build/database/record/instance-relationships/belongs-to.js +2 -1
  27. package/build/database/record/validators/uniqueness.js +8 -5
  28. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  29. package/build/frontend-model-controller.js +136 -53
  30. package/build/frontend-model-resource/base-resource.js +87 -37
  31. package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
  32. package/build/frontend-models/base.js +307 -59
  33. package/build/frontend-models/preloader.js +6 -3
  34. package/build/frontend-models/query.js +15 -6
  35. package/build/frontend-models/resource-definition.js +21 -2
  36. package/build/frontend-models/use-destroyed-event.js +1 -1
  37. package/build/frontend-models/use-model-class-event.js +2 -2
  38. package/build/frontend-models/use-updated-event.js +1 -1
  39. package/build/frontend-models/websocket-channel.js +256 -93
  40. package/build/frontend-models/websocket-publishers.js +218 -18
  41. package/build/http-server/client/websocket-session.js +13 -1
  42. package/build/http-server/websocket-channel.js +15 -2
  43. package/build/src/authorization/ability.d.ts +9 -3
  44. package/build/src/authorization/ability.d.ts.map +1 -1
  45. package/build/src/authorization/ability.js +33 -14
  46. package/build/src/background-jobs/adapter.d.ts +6 -0
  47. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  48. package/build/src/background-jobs/adapter.js +9 -1
  49. package/build/src/background-jobs/main.d.ts +6 -0
  50. package/build/src/background-jobs/main.d.ts.map +1 -1
  51. package/build/src/background-jobs/main.js +34 -11
  52. package/build/src/background-jobs/store.d.ts +66 -12
  53. package/build/src/background-jobs/store.d.ts.map +1 -1
  54. package/build/src/background-jobs/store.js +191 -35
  55. package/build/src/background-jobs/types.d.ts +55 -0
  56. package/build/src/background-jobs/types.d.ts.map +1 -1
  57. package/build/src/background-jobs/types.js +16 -1
  58. package/build/src/configuration-types.d.ts +3 -3
  59. package/build/src/configuration-types.d.ts.map +1 -1
  60. package/build/src/configuration-types.js +2 -2
  61. package/build/src/configuration.d.ts +18 -8
  62. package/build/src/configuration.d.ts.map +1 -1
  63. package/build/src/configuration.js +38 -7
  64. package/build/src/database/drivers/base-column.d.ts +5 -0
  65. package/build/src/database/drivers/base-column.d.ts.map +1 -1
  66. package/build/src/database/drivers/base-column.js +8 -1
  67. package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
  68. package/build/src/database/drivers/mssql/sql/update.js +2 -4
  69. package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
  70. package/build/src/database/drivers/mysql/sql/update.js +2 -4
  71. package/build/src/database/drivers/pgsql/column.d.ts +5 -0
  72. package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
  73. package/build/src/database/drivers/pgsql/column.js +19 -1
  74. package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
  75. package/build/src/database/drivers/pgsql/sql/update.js +2 -4
  76. package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
  77. package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
  78. package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
  79. package/build/src/database/drivers/sqlite/sql/update.js +2 -4
  80. package/build/src/database/query/index.d.ts +4 -4
  81. package/build/src/database/query/index.d.ts.map +1 -1
  82. package/build/src/database/query/index.js +3 -3
  83. package/build/src/database/query/model-class-query.d.ts +8 -2
  84. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  85. package/build/src/database/query/model-class-query.js +25 -11
  86. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  87. package/build/src/database/query/preloader/has-many.js +4 -2
  88. package/build/src/database/query/query-data.d.ts.map +1 -1
  89. package/build/src/database/query/query-data.js +3 -2
  90. package/build/src/database/query/update-base.d.ts +7 -0
  91. package/build/src/database/query/update-base.d.ts.map +1 -1
  92. package/build/src/database/query/update-base.js +13 -1
  93. package/build/src/database/query/with-count.d.ts.map +1 -1
  94. package/build/src/database/query/with-count.js +3 -2
  95. package/build/src/database/record/acts-as-list.d.ts.map +1 -1
  96. package/build/src/database/record/acts-as-list.js +35 -14
  97. package/build/src/database/record/attachments/store.d.ts +67 -0
  98. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  99. package/build/src/database/record/attachments/store.js +301 -25
  100. package/build/src/database/record/auditing.d.ts.map +1 -1
  101. package/build/src/database/record/auditing.js +7 -4
  102. package/build/src/database/record/index.d.ts +78 -26
  103. package/build/src/database/record/index.d.ts.map +1 -1
  104. package/build/src/database/record/index.js +171 -42
  105. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  106. package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
  107. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  108. package/build/src/database/record/validators/uniqueness.js +8 -6
  109. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
  110. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  111. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -12
  112. package/build/src/frontend-model-controller.d.ts +29 -8
  113. package/build/src/frontend-model-controller.d.ts.map +1 -1
  114. package/build/src/frontend-model-controller.js +122 -51
  115. package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
  116. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  117. package/build/src/frontend-model-resource/base-resource.js +83 -37
  118. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
  119. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
  120. package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
  121. package/build/src/frontend-models/base.d.ts +53 -20
  122. package/build/src/frontend-models/base.d.ts.map +1 -1
  123. package/build/src/frontend-models/base.js +275 -59
  124. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  125. package/build/src/frontend-models/preloader.js +6 -4
  126. package/build/src/frontend-models/query.d.ts +2 -2
  127. package/build/src/frontend-models/query.d.ts.map +1 -1
  128. package/build/src/frontend-models/query.js +14 -7
  129. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  130. package/build/src/frontend-models/resource-definition.js +20 -3
  131. package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
  132. package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
  133. package/build/src/frontend-models/use-destroyed-event.js +2 -2
  134. package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
  135. package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
  136. package/build/src/frontend-models/use-model-class-event.js +3 -3
  137. package/build/src/frontend-models/use-updated-event.d.ts +1 -1
  138. package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
  139. package/build/src/frontend-models/use-updated-event.js +2 -2
  140. package/build/src/frontend-models/websocket-channel.d.ts +60 -41
  141. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  142. package/build/src/frontend-models/websocket-channel.js +225 -85
  143. package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
  144. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  145. package/build/src/frontend-models/websocket-publishers.js +190 -19
  146. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  147. package/build/src/http-server/client/websocket-session.js +11 -2
  148. package/build/src/http-server/websocket-channel.d.ts +20 -8
  149. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  150. package/build/src/http-server/websocket-channel.js +15 -3
  151. package/build/src/sync/assets/cache.d.ts +288 -0
  152. package/build/src/sync/assets/cache.d.ts.map +1 -0
  153. package/build/src/sync/assets/cache.js +883 -0
  154. package/build/src/sync/assets/types.d.ts +199 -0
  155. package/build/src/sync/assets/types.d.ts.map +1 -0
  156. package/build/src/sync/assets/types.js +56 -0
  157. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  158. package/build/src/sync/sync-api-client.js +4 -3
  159. package/build/src/sync/sync-client.d.ts.map +1 -1
  160. package/build/src/sync/sync-client.js +3 -2
  161. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  162. package/build/src/sync/sync-envelope-replay-service.js +6 -5
  163. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  164. package/build/src/sync/sync-publisher.js +4 -3
  165. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
  166. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
  167. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
  168. package/build/src/testing/test-profiler.d.ts +4 -4
  169. package/build/src/testing/test-profiler.d.ts.map +1 -1
  170. package/build/src/testing/test-profiler.js +3 -3
  171. package/build/src/testing/test-runner.d.ts +279 -117
  172. package/build/src/testing/test-runner.d.ts.map +1 -1
  173. package/build/src/testing/test-runner.js +503 -933
  174. package/build/src/testing/test.d.ts +11 -72
  175. package/build/src/testing/test.d.ts.map +1 -1
  176. package/build/src/testing/test.js +131 -284
  177. package/build/src/testing/velocious-attempt-executor.d.ts +54 -0
  178. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
  179. package/build/src/testing/velocious-attempt-executor.js +408 -0
  180. package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
  181. package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
  182. package/build/src/testing/velocious-runner-reporter.js +240 -0
  183. package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
  184. package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
  185. package/build/src/testing/velocious-suite-hook-executor.js +41 -0
  186. package/build/src/testing/velocious-test-arguments.d.ts +39 -0
  187. package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
  188. package/build/src/testing/velocious-test-arguments.js +55 -0
  189. package/build/src/utils/model-primary-key.d.ts +73 -0
  190. package/build/src/utils/model-primary-key.d.ts.map +1 -0
  191. package/build/src/utils/model-primary-key.js +144 -0
  192. package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
  193. package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
  194. package/build/src/utils/sha256-bytes-hex.js +117 -0
  195. package/build/src/utils/sha256-hex.d.ts +2 -4
  196. package/build/src/utils/sha256-hex.d.ts.map +1 -1
  197. package/build/src/utils/sha256-hex.js +6 -120
  198. package/build/sync/assets/cache.js +998 -0
  199. package/build/sync/assets/types.js +62 -0
  200. package/build/sync/sync-api-client.js +3 -2
  201. package/build/sync/sync-client.js +2 -1
  202. package/build/sync/sync-envelope-replay-service.js +5 -4
  203. package/build/sync/sync-publisher.js +3 -2
  204. package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  205. package/build/testing/test-profiler.js +2 -2
  206. package/build/testing/test-runner.js +496 -990
  207. package/build/testing/test.js +164 -306
  208. package/build/testing/velocious-attempt-executor.js +441 -0
  209. package/build/testing/velocious-runner-reporter.js +264 -0
  210. package/build/testing/velocious-suite-hook-executor.js +43 -0
  211. package/build/testing/velocious-test-arguments.js +64 -0
  212. package/build/tsconfig.tsbuildinfo +1 -1
  213. package/build/utils/model-primary-key.js +171 -0
  214. package/build/utils/sha256-bytes-hex.js +132 -0
  215. package/build/utils/sha256-hex.js +7 -135
  216. package/package.json +5 -4
  217. package/src/authorization/ability.js +36 -13
  218. package/src/background-jobs/adapter.js +9 -0
  219. package/src/background-jobs/main.js +35 -10
  220. package/src/background-jobs/store.js +217 -38
  221. package/src/background-jobs/types.js +15 -0
  222. package/src/configuration-types.js +1 -1
  223. package/src/configuration.js +45 -9
  224. package/src/database/drivers/base-column.js +8 -0
  225. package/src/database/drivers/mssql/sql/update.js +1 -3
  226. package/src/database/drivers/mysql/sql/update.js +1 -3
  227. package/src/database/drivers/pgsql/column.js +22 -0
  228. package/src/database/drivers/pgsql/sql/update.js +1 -3
  229. package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
  230. package/src/database/drivers/sqlite/sql/update.js +1 -3
  231. package/src/database/query/index.js +2 -2
  232. package/src/database/query/model-class-query.js +26 -10
  233. package/src/database/query/preloader/has-many.js +4 -1
  234. package/src/database/query/query-data.js +2 -1
  235. package/src/database/query/update-base.js +14 -0
  236. package/src/database/query/with-count.js +3 -1
  237. package/src/database/record/acts-as-list.js +37 -13
  238. package/src/database/record/attachments/store.js +336 -26
  239. package/src/database/record/auditing.js +6 -3
  240. package/src/database/record/index.js +197 -42
  241. package/src/database/record/instance-relationships/belongs-to.js +2 -1
  242. package/src/database/record/validators/uniqueness.js +8 -5
  243. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  244. package/src/frontend-model-controller.js +136 -53
  245. package/src/frontend-model-resource/base-resource.js +87 -37
  246. package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
  247. package/src/frontend-models/base.js +307 -59
  248. package/src/frontend-models/preloader.js +6 -3
  249. package/src/frontend-models/query.js +15 -6
  250. package/src/frontend-models/resource-definition.js +21 -2
  251. package/src/frontend-models/use-destroyed-event.js +1 -1
  252. package/src/frontend-models/use-model-class-event.js +2 -2
  253. package/src/frontend-models/use-updated-event.js +1 -1
  254. package/src/frontend-models/websocket-channel.js +256 -93
  255. package/src/frontend-models/websocket-publishers.js +218 -18
  256. package/src/http-server/client/websocket-session.js +13 -1
  257. package/src/http-server/websocket-channel.js +15 -2
  258. package/src/sync/assets/cache.js +998 -0
  259. package/src/sync/assets/types.js +62 -0
  260. package/src/sync/sync-api-client.js +3 -2
  261. package/src/sync/sync-client.js +2 -1
  262. package/src/sync/sync-envelope-replay-service.js +5 -4
  263. package/src/sync/sync-publisher.js +3 -2
  264. package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  265. package/src/testing/test-profiler.js +2 -2
  266. package/src/testing/test-runner.js +496 -990
  267. package/src/testing/test.js +164 -306
  268. package/src/testing/velocious-attempt-executor.js +441 -0
  269. package/src/testing/velocious-runner-reporter.js +264 -0
  270. package/src/testing/velocious-suite-hook-executor.js +43 -0
  271. package/src/testing/velocious-test-arguments.js +64 -0
  272. package/src/utils/model-primary-key.js +171 -0
  273. package/src/utils/sha256-bytes-hex.js +132 -0
  274. package/src/utils/sha256-hex.js +7 -135
  275. package/build/src/testing/testing-package-adapter.d.ts +0 -9
  276. package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
  277. package/build/src/testing/testing-package-adapter.js +0 -69
  278. package/build/testing/testing-package-adapter.js +0 -76
  279. package/src/testing/testing-package-adapter.js +0 -76
@@ -41,6 +41,7 @@ import HasManyRelationship from "./relationships/has-many.js"
41
41
  import HasOneInstanceRelationship from "./instance-relationships/has-one.js"
42
42
  import HasOneRelationship from "./relationships/has-one.js"
43
43
  import RecordAttachmentHandle from "./attachments/handle.js"
44
+ import {recordAttachmentsStoreForModel, recordAttachmentsStoreForModelClass} from "./attachments/store.js"
44
45
  import * as inflection from "inflection"
45
46
  import deburrColumnName from "../../utils/deburr-column-name.js"
46
47
  import ModelClassQuery from "../query/model-class-query.js"
@@ -52,6 +53,7 @@ import singularizeModelName from "../../utils/singularize-model-name.js"
52
53
  import {defineModelScope} from "../../utils/model-scope.js"
53
54
  import { normalizeDateStringForWrite, normalizeDateValueForRead, normalizeDateValueForWrite } from "../datetime-storage.js"
54
55
  import {formatValue} from "../../utils/format-value.js"
56
+ import {modelPrimaryKeyCacheKey, modelPrimaryKeyConditions, readModelPrimaryKeyValue, scalarModelPrimaryKey, scalarModelPrimaryKeyValue} from "../../utils/model-primary-key.js"
55
57
  import {captureCreateAuditChanges, captureUpdateAuditChanges, createAudit, createCreateAudit, createDestroyAudit, createUpdateAudit, initializeAuditing, registerAuditCallback, registerAuditing, withoutAudit} from "./auditing.js"
56
58
  import {registerMagnitudeCounterCache} from "./counter-cache-magnitude.js"
57
59
  import {stateMachine} from "./state-machine.js"
@@ -519,6 +521,11 @@ class VelociousDatabaseRecord {
519
521
  * @type {Record<string, ReturnType<typeof JSON.parse>>} */
520
522
  _changes = {}
521
523
 
524
+ /**
525
+ * Whether primary-key reads are pinned to the stored attributes.
526
+ * @type {boolean} */
527
+ _readsPersistedPrimaryKey = false
528
+
522
529
  /**
523
530
  * Changes captured before a create audit is written.
524
531
  * @type {import("./auditing.js").AuditChanges | undefined} */
@@ -2551,6 +2558,9 @@ class VelociousDatabaseRecord {
2551
2558
  const primaryKey = this.primaryKey()
2552
2559
  const tableName = this.tableName()
2553
2560
  const connection = this.connection()
2561
+
2562
+ if (Array.isArray(primaryKey)) throw new Error(`${this.name}.nextPrimaryKey() does not support composite primary keys.`)
2563
+
2554
2564
  const newestRecord = await this.order(`${connection.quoteTable(tableName)}.${connection.quoteColumn(primaryKey)}`).last()
2555
2565
 
2556
2566
  if (newestRecord) {
@@ -2568,10 +2578,25 @@ class VelociousDatabaseRecord {
2568
2578
 
2569
2579
  /**
2570
2580
  * Runs set primary key.
2571
- * @param {string} primaryKey - Primary key.
2581
+ * @param {string | string[] | null} primaryKey - Primary key.
2572
2582
  * @returns {void} - No return value.
2573
2583
  */
2574
2584
  static setPrimaryKey(primaryKey) {
2585
+ if (Array.isArray(primaryKey)) {
2586
+ if (primaryKey.length === 0) throw new TypeError("Composite primary keys require at least one column.")
2587
+
2588
+ const seenColumns = new Set()
2589
+
2590
+ for (const columnName of primaryKey) {
2591
+ if (seenColumns.has(columnName)) throw new TypeError(`Composite primary key has duplicate column: ${columnName}.`)
2592
+
2593
+ seenColumns.add(columnName)
2594
+ }
2595
+
2596
+ this._primaryKey = [...primaryKey]
2597
+ return
2598
+ }
2599
+
2575
2600
  this._primaryKey = primaryKey
2576
2601
  }
2577
2602
 
@@ -2616,7 +2641,7 @@ class VelociousDatabaseRecord {
2616
2641
 
2617
2642
  /**
2618
2643
  * Runs primary key.
2619
- * @returns {string} - The primary key.
2644
+ * @returns {string | string[]} - The primary key.
2620
2645
  */
2621
2646
  static primaryKey() {
2622
2647
  if (this._primaryKey) return this._primaryKey
@@ -2646,33 +2671,60 @@ class VelociousDatabaseRecord {
2646
2671
  await this._runLifecycleCallbacks("beforeValidation")
2647
2672
  await this._runValidations()
2648
2673
 
2674
+ if (this._databaseOperation && this.isPersisted() && Object.keys(this.getModelClass().getAttachments()).length > 0) {
2675
+ const connection = this._connection()
2676
+
2677
+ if (!connection.insideTransaction()) {
2678
+ await this.getModelClass()._prepareAttachmentStoreSchema(connection)
2679
+ }
2680
+ }
2681
+
2649
2682
  const saveInTransaction = async () => {
2650
- await this._runLifecycleCallbacks("beforeSave")
2683
+ /** @type {Record<string, ReturnType<typeof JSON.parse>> | undefined} */
2684
+ let persistedAttributesBeforeUpdate
2685
+ /** @type {Record<string, ReturnType<typeof JSON.parse>> | undefined} */
2686
+ let changesBeforeUpdate
2687
+ let updateReloaded = false
2688
+
2689
+ try {
2690
+ await this._runLifecycleCallbacks("beforeSave")
2651
2691
 
2652
- // If any belongs-to-relationships was saved, then updated-at should still be set on this record.
2653
- const {savedCount} = await this._autoSaveBelongsToRelationships()
2692
+ // If any belongs-to-relationships was saved, then updated-at should still be set on this record.
2693
+ const {savedCount} = await this._autoSaveBelongsToRelationships()
2654
2694
 
2655
- if (this.isPersisted()) {
2656
- await this._runLifecycleCallbacks("beforeUpdate")
2695
+ if (this.isPersisted()) {
2696
+ persistedAttributesBeforeUpdate = {...this._attributes}
2697
+ await this._runLifecycleCallbacks("beforeUpdate")
2698
+ changesBeforeUpdate = {...this._changes}
2657
2699
 
2658
- // If any has-many-relationships will be saved, then updated-at should still be set on this record.
2659
- const autoSaveHasManyrelationships = this._autoSaveHasManyAndHasOneRelationshipsToSave()
2700
+ // If any has-many-relationships will be saved, then updated-at should still be set on this record.
2701
+ const autoSaveHasManyrelationships = this._autoSaveHasManyAndHasOneRelationshipsToSave()
2702
+
2703
+ if (this._hasChanges() || savedCount > 0 || autoSaveHasManyrelationships.length > 0) {
2704
+ result = await this._updateRecordWithChanges()
2705
+ updateReloaded = true
2706
+ }
2660
2707
 
2661
- if (this._hasChanges() || savedCount > 0 || autoSaveHasManyrelationships.length > 0) {
2662
- result = await this._updateRecordWithChanges()
2708
+ await this._runLifecycleCallbacks("afterUpdate")
2709
+ } else {
2710
+ await this._runLifecycleCallbacks("beforeCreate")
2711
+ result = await this._createNewRecord()
2712
+ await this._runLifecycleCallbacks("afterCreate")
2663
2713
  }
2664
2714
 
2665
- await this._runLifecycleCallbacks("afterUpdate")
2666
- } else {
2667
- await this._runLifecycleCallbacks("beforeCreate")
2668
- result = await this._createNewRecord()
2669
- await this._runLifecycleCallbacks("afterCreate")
2670
- }
2715
+ await this._autoSaveHasManyAndHasOneRelationships({isNewRecord})
2716
+ await this._autoSaveAttachments()
2717
+ await this._runLifecycleCallbacks("afterSave")
2718
+ await this._emitRecordChangeAfterCommit(isNewRecord ? "create" : "update")
2719
+ } catch (error) {
2720
+ if (updateReloaded && persistedAttributesBeforeUpdate && changesBeforeUpdate) {
2721
+ this._attributes = persistedAttributesBeforeUpdate
2722
+ this._changes = changesBeforeUpdate
2723
+ this._assignedAttributeNames = undefined
2724
+ }
2671
2725
 
2672
- await this._autoSaveHasManyAndHasOneRelationships({isNewRecord})
2673
- await this._autoSaveAttachments()
2674
- await this._runLifecycleCallbacks("afterSave")
2675
- await this._emitRecordChangeAfterCommit(isNewRecord ? "create" : "update")
2726
+ throw error
2727
+ }
2676
2728
  }
2677
2729
 
2678
2730
  if (this._databaseOperation) {
@@ -2774,7 +2826,7 @@ class VelociousDatabaseRecord {
2774
2826
  this.bindRelatedRecord(model)
2775
2827
  const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2776
2828
 
2777
- model.setAttribute(foreignKey, this.id())
2829
+ model.setAttribute(foreignKey, scalarModelPrimaryKeyValue(this.id(), `Has-many autosave for ${this.getModelClass().name}`))
2778
2830
 
2779
2831
  if (model.isChanged()) {
2780
2832
  useRelationship = true
@@ -2828,7 +2880,7 @@ class VelociousDatabaseRecord {
2828
2880
  this.bindRelatedRecord(model)
2829
2881
  const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2830
2882
 
2831
- model.setAttribute(foreignKey, this.id())
2883
+ model.setAttribute(foreignKey, scalarModelPrimaryKeyValue(this.id(), `Has-many autosave for ${this.getModelClass().name}`))
2832
2884
 
2833
2885
  if (model.isChanged()) {
2834
2886
  await model.save()
@@ -2882,15 +2934,30 @@ class VelociousDatabaseRecord {
2882
2934
  static async transaction(callback) {
2883
2935
  await this.ensureInitialized()
2884
2936
 
2885
- const useTransactions = this.connection().getArgs().record?.transactions
2937
+ const connection = this.connection()
2938
+
2939
+ if (!connection.insideTransaction()) {
2940
+ await this._prepareAttachmentStoreSchema(connection)
2941
+ }
2942
+
2943
+ const useTransactions = connection.getArgs().record?.transactions
2886
2944
 
2887
2945
  if (useTransactions !== false) {
2888
- return await this.connection().transaction(callback)
2946
+ return await connection.transaction(callback)
2889
2947
  } else {
2890
2948
  return await callback()
2891
2949
  }
2892
2950
  }
2893
2951
 
2952
+ /**
2953
+ * Prepares this model's attachment store before opening a model transaction.
2954
+ * @param {import("../drivers/base.js").default} connection - Model connection.
2955
+ * @returns {Promise<void>} - Resolves when the attachment schema is current.
2956
+ */
2957
+ static async _prepareAttachmentStoreSchema(connection) {
2958
+ await recordAttachmentsStoreForModelClass(this, connection).prepareRecordIdentityMigration({connection})
2959
+ }
2960
+
2894
2961
  /**
2895
2962
  * Runs the callback while holding a named advisory lock. Calls without
2896
2963
  * By default calls use the caller connection. Calls with `dedicatedConnection`
@@ -3027,7 +3094,7 @@ class VelociousDatabaseRecord {
3027
3094
  const targetTableSql = driver.quoteTable(query.getTableReferenceForJoin())
3028
3095
  const scopeTableSql = driver.quoteTable(scopeTableReference)
3029
3096
  const scopeTableFromSql = `${driver.quoteTable(tableName)} AS ${scopeTableSql}`
3030
- const primaryKeyColumn = translationClass.primaryKey()
3097
+ const primaryKeyColumn = scalarModelPrimaryKey(translationClass.primaryKey(), `Current translation scope for ${translationClass.name}`)
3031
3098
  const foreignKeyColumn = relationship.getForeignKey()
3032
3099
  const targetPrimaryKeySql = `${targetTableSql}.${driver.quoteColumn(primaryKeyColumn)}`
3033
3100
  const targetForeignKeySql = `${targetTableSql}.${driver.quoteColumn(foreignKeyColumn)}`
@@ -3284,7 +3351,11 @@ class VelociousDatabaseRecord {
3284
3351
  static orderableColumn() {
3285
3352
  // FIXME: Allow to change to 'created_at' if using UUID?
3286
3353
 
3287
- return this.primaryKey()
3354
+ const primaryKey = this.primaryKey()
3355
+
3356
+ if (Array.isArray(primaryKey)) return primaryKey[0]
3357
+
3358
+ return primaryKey
3288
3359
  }
3289
3360
 
3290
3361
  /**
@@ -3390,7 +3461,7 @@ class VelociousDatabaseRecord {
3390
3461
  * Runs find.
3391
3462
  * @template {typeof VelociousDatabaseRecord} MC
3392
3463
  * @this {MC}
3393
- * @param {number|string} recordId - Record id.
3464
+ * @param {import("../../utils/model-primary-key.js").ModelPrimaryKeyValue} recordId - Record id.
3394
3465
  * @returns {Promise<InstanceType<MC>>} - Resolves with the find.
3395
3466
  */
3396
3467
  static async find(recordId) {
@@ -3969,11 +4040,38 @@ class VelociousDatabaseRecord {
3969
4040
  return tenants
3970
4041
  }
3971
4042
 
4043
+ /**
4044
+ * Runs a callback while primary-key reads resolve to the persisted identity.
4045
+ * @param {() => Promise<void>} callback - Callback that requires the stored identity.
4046
+ * @returns {Promise<void>} - Resolves when the callback completes.
4047
+ */
4048
+ async _withPersistedPrimaryKey(callback) {
4049
+ const previousReadsPersistedPrimaryKey = this._readsPersistedPrimaryKey
4050
+
4051
+ this._readsPersistedPrimaryKey = true
4052
+
4053
+ try {
4054
+ await callback()
4055
+ } finally {
4056
+ this._readsPersistedPrimaryKey = previousReadsPersistedPrimaryKey
4057
+ }
4058
+ }
4059
+
3972
4060
  /**
3973
4061
  * Destroys the record in the database and all of its dependent records.
3974
4062
  * @returns {Promise<void>} - Resolves when complete.
3975
4063
  */
3976
4064
  async destroy() {
4065
+ await this._withPersistedPrimaryKey(async () => {
4066
+ await this._destroyPersistedRecord()
4067
+ })
4068
+ }
4069
+
4070
+ /**
4071
+ * Runs the destroy lifecycle after the persisted identity has been restored.
4072
+ * @returns {Promise<void>} - Resolves when complete.
4073
+ */
4074
+ async _destroyPersistedRecord() {
3977
4075
  await this._runLifecycleCallbacks("beforeDestroy")
3978
4076
 
3979
4077
  for (const relationship of this.getModelClass().getRelationships()) {
@@ -4053,7 +4151,7 @@ class VelociousDatabaseRecord {
4053
4151
  * @type {Record<string, ReturnType<typeof JSON.parse>>} */
4054
4152
  const conditions = {}
4055
4153
 
4056
- conditions[this.getModelClass().primaryKey()] = this.id()
4154
+ Object.assign(conditions, modelPrimaryKeyConditions(this.getModelClass().primaryKey(), this._persistedPrimaryKeyValue()))
4057
4155
 
4058
4156
  const sql = this._connection().deleteSql({
4059
4157
  conditions,
@@ -4416,9 +4514,15 @@ class VelociousDatabaseRecord {
4416
4514
  readColumn(attributeName) {
4417
4515
  this.getModelClass()._assertHasBeenInitialized()
4418
4516
  const belongsToChanges = this._belongsToChanges()
4517
+ const primaryKey = this.getModelClass().primaryKey()
4518
+ const readsPersistedPrimaryKey = this._readsPersistedPrimaryKey && (
4519
+ Array.isArray(primaryKey) ? primaryKey.includes(attributeName) : primaryKey == attributeName
4520
+ )
4419
4521
  let result
4420
4522
 
4421
- if (attributeName in belongsToChanges) {
4523
+ if (readsPersistedPrimaryKey) {
4524
+ result = this._attributes[attributeName]
4525
+ } else if (attributeName in belongsToChanges) {
4422
4526
  result = belongsToChanges[attributeName]
4423
4527
  } else if (attributeName in this._changes) {
4424
4528
  result = this._changes[attributeName]
@@ -4551,7 +4655,10 @@ class VelociousDatabaseRecord {
4551
4655
 
4552
4656
  const data = Object.assign({}, this._belongsToChanges(), this.rawAttributes())
4553
4657
  const primaryKey = this.getModelClass().primaryKey()
4554
- const primaryKeyColumn = this.getModelClass().getColumns().find((column) => column.getName() == primaryKey)
4658
+ const primaryKeyColumns = this.getModelClass().getColumns().filter((column) => {
4659
+ return Array.isArray(primaryKey) ? primaryKey.includes(column.getName()) : column.getName() == primaryKey
4660
+ })
4661
+ const primaryKeyColumn = Array.isArray(primaryKey) ? undefined : primaryKeyColumns[0]
4555
4662
  const primaryKeyType = primaryKeyColumn?.getType()?.toLowerCase()
4556
4663
  const driverSupportsDefaultUUID = typeof connection.supportsDefaultPrimaryKeyUUID == "function" && connection.supportsDefaultPrimaryKeyUUID()
4557
4664
  const isUUIDPrimaryKey = primaryKeyType?.includes("uuid")
@@ -4559,9 +4666,18 @@ class VelociousDatabaseRecord {
4559
4666
  this._setDefaultTimestampValues(data)
4560
4667
 
4561
4668
  const columnNames = this.getModelClass().getColumnNames()
4562
- const hasUserProvidedPrimaryKey = data[primaryKey] !== undefined && data[primaryKey] !== null && data[primaryKey] !== ""
4669
+ const hasUserProvidedPrimaryKey = Array.isArray(primaryKey)
4670
+ ? primaryKey.every((columnName) => data[columnName] !== undefined && data[columnName] !== null && data[columnName] !== "")
4671
+ : data[primaryKey] !== undefined && data[primaryKey] !== null && data[primaryKey] !== ""
4672
+ const hasUserProvidedAutoIncrementPrimaryKey = primaryKeyColumns.some((column) => {
4673
+ const value = data[column.getName()]
4674
+
4675
+ return column.getAutoIncrement() === true && value !== undefined && value !== null && value !== ""
4676
+ })
4563
4677
 
4564
4678
  if (shouldAssignUUIDPrimaryKey && !hasUserProvidedPrimaryKey) {
4679
+ if (Array.isArray(primaryKey)) throw new Error("Composite UUID primary keys must be provided explicitly.")
4680
+
4565
4681
  data[primaryKey] = new UUID(4).format()
4566
4682
  }
4567
4683
 
@@ -4576,7 +4692,7 @@ class VelociousDatabaseRecord {
4576
4692
  // Explicit primary-key inserts into auto-increment columns go through the
4577
4693
  // driver's explicit-primary-key insert (MSSQL wraps it in IDENTITY_INSERT);
4578
4694
  // everything else uses the plain query path.
4579
- const insertResult = hasUserProvidedPrimaryKey && primaryKeyColumn?.getAutoIncrement() === true
4695
+ const insertResult = hasUserProvidedAutoIncrementPrimaryKey
4580
4696
  ? await connection.insertWithExplicitPrimaryKey({options: insertOptions, sql, tableName: this._tableName()})
4581
4697
  : await connection.query(sql, insertOptions)
4582
4698
 
@@ -4606,10 +4722,17 @@ class VelociousDatabaseRecord {
4606
4722
 
4607
4723
  /**
4608
4724
  * Applies the database insert response to this record.
4609
- * @param {{connection: import("../drivers/base.js").default, data: Record<string, string | number | boolean | Date | null | undefined>, insertResult: Array<Record<string, string | number | boolean | Date | null | undefined>> | null | undefined, primaryKey: string}} options - Pinned insert connection, inserted data, connection result, and primary key column name.
4725
+ * @param {{connection: import("../drivers/base.js").default, data: Record<string, string | number | boolean | Date | null | undefined>, insertResult: Array<Record<string, string | number | boolean | Date | null | undefined>> | null | undefined, primaryKey: string | string[]}} options - Pinned insert connection, inserted data, connection result, and primary key column name.
4610
4726
  * @returns {Promise<void>} - Resolves when complete.
4611
4727
  */
4612
4728
  async _applyInsertResult({connection, data, insertResult, primaryKey}) {
4729
+ if (Array.isArray(primaryKey)) {
4730
+ const insertedRow = Array.isArray(insertResult) ? insertResult[0] : undefined
4731
+
4732
+ await this._reloadWithId(readModelPrimaryKeyValue(primaryKey, (columnName) => insertedRow?.[columnName] ?? data[columnName]))
4733
+ return
4734
+ }
4735
+
4613
4736
  if (Array.isArray(insertResult) && insertResult[0] && insertResult[0][primaryKey]) {
4614
4737
  this._attributes = insertResult[0]
4615
4738
  this._changes = {}
@@ -4671,12 +4794,16 @@ class VelociousDatabaseRecord {
4671
4794
  * @returns {Promise<void>} - Resolves when complete.
4672
4795
  */
4673
4796
  async _updateRecordWithChanges() {
4797
+ const connection = this._connection()
4798
+ const primaryKey = this.getModelClass().primaryKey()
4799
+ const persistedPrimaryKeyValue = this._persistedPrimaryKeyValue()
4800
+ const nextPrimaryKeyValue = this.id()
4674
4801
  /**
4675
4802
  * Conditions.
4676
4803
  * @type {Record<string, ReturnType<typeof JSON.parse>>} */
4677
4804
  const conditions = {}
4678
4805
 
4679
- conditions[this.getModelClass().primaryKey()] = this.id()
4806
+ Object.assign(conditions, modelPrimaryKeyConditions(primaryKey, persistedPrimaryKeyValue))
4680
4807
 
4681
4808
  const changes = Object.assign({}, this._belongsToChanges(), this._changes)
4682
4809
  const updatedAtColumn = this.getModelClass().getColumns().find((column) => column.getName() == "updated_at")
@@ -4688,19 +4815,32 @@ class VelociousDatabaseRecord {
4688
4815
 
4689
4816
  if (Object.keys(changes).length > 0) {
4690
4817
  this._normalizeDateValuesForWrite(changes)
4691
- const sql = this._connection().updateSql({
4818
+ const sql = connection.updateSql({
4692
4819
  tableName: this._tableName(),
4693
4820
  data: changes,
4694
4821
  conditions
4695
4822
  })
4696
- await this._connection().query(sql, {logName: `${this.getModelClass().name} Update`})
4697
- await this._reloadWithId(this.id())
4823
+ await connection.query(sql, {logName: `${this.getModelClass().name} Update`})
4824
+ await this._reloadWithId(nextPrimaryKeyValue)
4825
+ const reloadedPrimaryKeyValue = this.id()
4826
+
4827
+ if (
4828
+ Object.keys(this.getModelClass().getAttachments()).length > 0
4829
+ && modelPrimaryKeyCacheKey(primaryKey, persistedPrimaryKeyValue) !== modelPrimaryKeyCacheKey(primaryKey, reloadedPrimaryKeyValue)
4830
+ ) {
4831
+ await recordAttachmentsStoreForModel(this).migrateRecordIdentity({
4832
+ connection,
4833
+ model: this,
4834
+ nextIdentity: reloadedPrimaryKeyValue,
4835
+ previousIdentity: persistedPrimaryKeyValue
4836
+ })
4837
+ }
4698
4838
  }
4699
4839
  }
4700
4840
 
4701
4841
  /**
4702
4842
  * Runs id.
4703
- * @returns {number|string} - The id.
4843
+ * @returns {import("../../utils/model-primary-key.js").ModelPrimaryKeyValue} - The id.
4704
4844
  */
4705
4845
  id() {
4706
4846
  if (!this.getModelClass()._columnNameToAttributeName) {
@@ -4708,6 +4848,11 @@ class VelociousDatabaseRecord {
4708
4848
  }
4709
4849
 
4710
4850
  const primaryKey = this.getModelClass().primaryKey()
4851
+
4852
+ if (Array.isArray(primaryKey)) {
4853
+ return readModelPrimaryKeyValue(primaryKey, (columnName) => this.readColumn(columnName))
4854
+ }
4855
+
4711
4856
  const attributeName = this.getModelClass().getColumnNameToAttributeNameMap()[primaryKey]
4712
4857
 
4713
4858
  if (attributeName === undefined) {
@@ -4717,6 +4862,16 @@ class VelociousDatabaseRecord {
4717
4862
  return /** @type {number | string} */ (this.readAttribute(attributeName))
4718
4863
  }
4719
4864
 
4865
+ /**
4866
+ * Returns the identity represented by the last persisted database attributes.
4867
+ * @returns {import("../../utils/model-primary-key.js").ModelPrimaryKeyValue} - Persisted identity.
4868
+ */
4869
+ _persistedPrimaryKeyValue() {
4870
+ const primaryKey = this.getModelClass().primaryKey()
4871
+
4872
+ return readModelPrimaryKeyValue(primaryKey, (columnName) => this._attributes[columnName])
4873
+ }
4874
+
4720
4875
  /**
4721
4876
  * Runs is persisted.
4722
4877
  * @returns {boolean} - Whether persisted.
@@ -4741,7 +4896,7 @@ class VelociousDatabaseRecord {
4741
4896
  /**
4742
4897
  * Runs reload with id.
4743
4898
  * @template {typeof VelociousDatabaseRecord} MC
4744
- * @param {string | number} id - Record identifier.
4899
+ * @param {import("../../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Record identifier.
4745
4900
  * @returns {Promise<void>} - Resolves when complete.
4746
4901
  */
4747
4902
  async _reloadWithId(id) {
@@ -4752,7 +4907,7 @@ class VelociousDatabaseRecord {
4752
4907
  * @type {Record<string, ReturnType<typeof JSON.parse>>} */
4753
4908
  const whereObject = {}
4754
4909
 
4755
- whereObject[primaryKey] = id
4910
+ Object.assign(whereObject, modelPrimaryKeyConditions(primaryKey, id))
4756
4911
 
4757
4912
  const query = /** @type {import("../query/model-class-query.js").default<MC>} */ (
4758
4913
  this
@@ -4773,7 +4928,7 @@ class VelociousDatabaseRecord {
4773
4928
  * @returns {Promise<void>} - Resolves when complete.
4774
4929
  */
4775
4930
  async reload() {
4776
- const recordId = /** @type {string | number} */ (this.readAttribute("id"))
4931
+ const recordId = this._persistedPrimaryKeyValue()
4777
4932
  await this._reloadWithId(recordId)
4778
4933
  }
4779
4934
 
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseInstanceRelationship from "./base.js"
4
+ import {scalarModelPrimaryKey} from "../../../utils/model-primary-key.js"
4
5
 
5
6
  /**
6
7
  * A generic query over some model type.
@@ -79,7 +80,7 @@ export default class VelociousDatabaseRecordBelongsToInstanceRelationship extend
79
80
  * @returns {Promise<InstanceType<TMC> | undefined>} - Loaded foreign model.
80
81
  */
81
82
  async _loadForeignModel({foreignModelID, TargetModelClass}) {
82
- const primaryKey = TargetModelClass.primaryKey()
83
+ const primaryKey = scalarModelPrimaryKey(TargetModelClass.primaryKey(), `Belongs-to relationship load for ${TargetModelClass.name}`)
83
84
  /**
84
85
  * Where args.
85
86
  * @type {Record<string, string | number | null | undefined>} */
@@ -2,6 +2,7 @@
2
2
 
3
3
  import Base from "./base.js"
4
4
  import * as inflection from "inflection"
5
+ import {modelPrimaryKeyConditions, scalarModelPrimaryKeyValue} from "../../../utils/model-primary-key.js"
5
6
  import validationMessage from "../validation-messages.js"
6
7
 
7
8
  export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
@@ -15,8 +16,6 @@ export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
15
16
  async validate({model, attributeName}) {
16
17
  const modelClass = /** @type {typeof import("../index.js").default} */ (model.constructor)
17
18
 
18
- const connection = model.connection()
19
- const tableName = modelClass._getTable().getName()
20
19
  const attributeValue = /** @type {string | number} */ (model.readAttribute(attributeName))
21
20
  const attributeNameUnderscore = inflection.underscore(attributeName)
22
21
 
@@ -55,7 +54,7 @@ export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
55
54
  .where(whereArgs)
56
55
 
57
56
  if (model.isPersisted()) {
58
- existingRecordQuery.where(`${connection.quoteTable(tableName)}.${connection.quoteColumn(modelClass.primaryKey())} != ${connection.quote(model.id())}`)
57
+ existingRecordQuery.whereNot(modelPrimaryKeyConditions(modelClass.primaryKey(), model._persistedPrimaryKeyValue()))
59
58
  }
60
59
 
61
60
  const existingRecord = await existingRecordQuery.first()
@@ -95,8 +94,12 @@ export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
95
94
  const instanceRelationship = model.getRelationshipByName(relationshipName)
96
95
  const loaded = instanceRelationship.loaded()
97
96
 
98
- if (loaded && !Array.isArray(loaded) && typeof loaded.id === "function") {
99
- return loaded.id()
97
+ if (loaded && !Array.isArray(loaded)) {
98
+ const loadedId = loaded.id()
99
+
100
+ if (loadedId == null) return null
101
+
102
+ return scalarModelPrimaryKeyValue(loadedId, `Uniqueness scope relationship for ${modelClass.name}`)
100
103
  }
101
104
  }
102
105
 
@@ -543,7 +543,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
543
543
  fileContent += ` return /** @type {${signature.returnType}} */ (await ${className}.executeCustomCommand({\n`
544
544
  fileContent += ` commandName: ${JSON.stringify(memberCommands[methodName])},\n`
545
545
  fileContent += ` commandType: ${JSON.stringify(memberCommands[methodName])},\n`
546
- fileContent += " memberId: this.primaryKeyValue(),\n"
546
+ fileContent += ` memberId: this.scalarPrimaryKeyValue(${JSON.stringify(`Custom member command ${className}#${methodName}`)}),\n`
547
547
  fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
548
548
  fileContent += ` resourcePath: ${className}.resourcePath()\n`
549
549
  fileContent += " }))\n"
@@ -1179,10 +1179,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1179
1179
 
1180
1180
  const primaryKey = modelClass.primaryKey()
1181
1181
 
1182
- if (typeof primaryKey != "string" || primaryKey.length < 1) return false
1183
- if (attributeName === primaryKey) return true
1182
+ for (const columnName of Array.isArray(primaryKey) ? primaryKey : [primaryKey]) {
1183
+ if (attributeName === columnName) return true
1184
+ if (modelClass.resolveAttributeName(columnName) === attributeName) return true
1185
+ }
1184
1186
 
1185
- return modelClass.resolveAttributeName(primaryKey) === attributeName
1187
+ return false
1186
1188
  }
1187
1189
 
1188
1190
  /**
@@ -1202,13 +1204,27 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1202
1204
 
1203
1205
  /**
1204
1206
  * Validates an explicitly configured frontend-model primary key.
1205
- * @param {{attributeNames: Array<string>, primaryKey: string}} args - Configured primary key args.
1206
- * @returns {string} - Configured primary key.
1207
+ * @param {{attributeNames: Array<string>, primaryKey: string | string[]}} args - Configured primary key args.
1208
+ * @returns {string | string[]} - Configured primary key.
1207
1209
  */
1208
1210
  validatedConfiguredPrimaryKey({attributeNames, primaryKey}) {
1209
- if (attributeNames.includes(primaryKey)) return primaryKey
1211
+ const primaryKeyAttributes = Array.isArray(primaryKey) ? primaryKey : [primaryKey]
1212
+
1213
+ if (primaryKeyAttributes.length < 1) {
1214
+ throw new Error("Configured frontend model composite primary key must contain at least one attribute.")
1215
+ }
1216
+
1217
+ if (new Set(primaryKeyAttributes).size !== primaryKeyAttributes.length) {
1218
+ throw new Error("Configured frontend model composite primary key attributes must be unique.")
1219
+ }
1220
+
1221
+ for (const attributeName of primaryKeyAttributes) {
1222
+ if (!attributeNames.includes(attributeName)) {
1223
+ throw new Error(`Configured frontend model primary key "${attributeName}" is not a generated frontend model attribute.`)
1224
+ }
1225
+ }
1210
1226
 
1211
- throw new Error(`Configured frontend model primary key "${primaryKey}" is not a generated frontend model attribute.`)
1227
+ return primaryKey
1212
1228
  }
1213
1229
 
1214
1230
  /**