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
@@ -17,6 +17,8 @@ import {captureFrontendModelRemoteRequestContext, mergeFrontendModelRemoteReques
17
17
  import {bufferOutgoingEvent, clearBufferedOutgoingEvents, drainBufferedOutgoingEvents} from "./outgoing-event-buffer.js"
18
18
  import {defineModelScope} from "../utils/model-scope.js"
19
19
  import isPlainObject from "../utils/plain-object.js"
20
+ import {forcedNonBlankString} from "typanic"
21
+ import {modelPrimaryKeyCacheKey, modelPrimaryKeyConditions, readModelPrimaryKeyValue, scalarModelPrimaryKey, scalarModelPrimaryKeyValue} from "../utils/model-primary-key.js"
20
22
  import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQueryData, setPayloadAssociationCount, setPayloadComputedAbility, setPayloadQueryData} from "../record-payload-values.js"
21
23
 
22
24
  /**
@@ -30,11 +32,11 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
30
32
  */
31
33
  /**
32
34
  * Defines this typedef.
33
- * @typedef {{callback: (payload: {id: string, model: FrontendModelBase}) => void, eventFilterKey: string | null, eventFilterPayload: import("./query.js").FrontendModelEventFilterPayload | null, projectionPayload: import("./query.js").FrontendModelProjectionPayload}} FrontendModelModelEventCallbackEntry
35
+ * @typedef {{callback: (payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue, model: FrontendModelBase}) => void, eventFilterKey: string | null, eventFilterPayload: import("./query.js").FrontendModelEventFilterPayload | null, projectionPayload: import("./query.js").FrontendModelProjectionPayload}} FrontendModelModelEventCallbackEntry
34
36
  */
35
37
  /**
36
38
  * Defines this typedef.
37
- * @typedef {{callback: (payload: {id: string}) => void}} FrontendModelDestroyEventCallbackEntry
39
+ * @typedef {{callback: (payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue}) => void}} FrontendModelDestroyEventCallbackEntry
38
40
  */
39
41
  /**
40
42
  * FrontendModelCommandType type.
@@ -90,7 +92,7 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
90
92
  */
91
93
  /**
92
94
  * Defines this typedef.
93
- * @typedef {{attributes?: Array<string | FrontendModelAttributeDefinition> | Record<string, FrontendModelAttributeDefinition>, builtInCollectionCommands?: string[], builtInMemberCommands?: string[], collectionCommands?: string[], commands?: string[], memberCommands?: string[], attachments?: Record<string, FrontendModelAttachmentDefinition>, modelName?: string, nestedAttributes?: Record<string, {allowDestroy?: boolean, limit?: number}>, primaryKey?: string, relationships?: string[], sync?: FrontendModelSyncConfig}} FrontendModelResourceConfig
95
+ * @typedef {{attributes?: Array<string | FrontendModelAttributeDefinition> | Record<string, FrontendModelAttributeDefinition>, builtInCollectionCommands?: string[], builtInMemberCommands?: string[], collectionCommands?: string[], commands?: string[], memberCommands?: string[], attachments?: Record<string, FrontendModelAttachmentDefinition>, modelName?: string, nestedAttributes?: Record<string, {allowDestroy?: boolean, limit?: number}>, primaryKey?: string | string[], relationships?: string[], sync?: FrontendModelSyncConfig}} FrontendModelResourceConfig
94
96
  */
95
97
  /**
96
98
  * Frontend model constructor type.
@@ -158,6 +160,7 @@ const SELECTED_ATTRIBUTES_KEY = "__selectedAttributes"
158
160
  const ASSOCIATION_COUNTS_KEY = "__associationCounts"
159
161
  const QUERY_DATA_KEY = "__queryData"
160
162
  const ABILITIES_KEY = "__abilities"
163
+ const ATTACHMENT_OWNER_KEY = "__attachmentOwner"
161
164
  /**
162
165
  * Pending shared frontend model requests.
163
166
  * @type {Array<{commandName?: string, commandType: FrontendModelRequestCommandType, customPath?: string, modelClass: FrontendModelClass, payload: Record<string, ReturnType<typeof JSON.parse>>, requestContext: import("../remote-request-context.js").RemoteRequestContext, requestId: string, resolve: (response: Record<string, ReturnType<typeof JSON.parse>>) => void, reject: (error: ReturnType<typeof JSON.parse>) => void, resourcePath?: string | null}>} */
@@ -774,6 +777,19 @@ function frontendModelAttachmentCommandPayload(attachment, attachmentId) {
774
777
  return payload
775
778
  }
776
779
 
780
+ /**
781
+ * Returns the canonical backing owner used by attachment metadata storage.
782
+ * @param {FrontendModelBase} model - Frontend attachment owner.
783
+ * @returns {{recordId: string, recordType: string, resourceName: string}} - Canonical attachment owner and originating resource.
784
+ */
785
+ function frontendModelAttachmentOwner(model) {
786
+ if (!model._attachmentOwner) {
787
+ throw new Error(`Missing attachment owner metadata on ${frontendModelClassFor(model).name}`)
788
+ }
789
+
790
+ return model._attachmentOwner
791
+ }
792
+
777
793
  /**
778
794
  * Runs frontend attachment value is bytes.
779
795
  * @param {ReturnType<typeof JSON.parse>} value - Candidate value.
@@ -1179,13 +1195,14 @@ export class FrontendModelAttachmentHandle {
1179
1195
  * @returns {import("./query.js").default<typeof VelociousAttachment>} - Attachment metadata query.
1180
1196
  */
1181
1197
  query() {
1182
- const ModelClass = frontendModelClassFor(this.model)
1198
+ const attachmentOwner = frontendModelAttachmentOwner(this.model)
1183
1199
 
1184
1200
  return VelociousAttachment
1185
1201
  .where({
1186
1202
  name: this.attachmentName,
1187
- recordId: String(this.model.primaryKeyValue()),
1188
- recordType: ModelClass.getModelName()
1203
+ recordId: attachmentOwner.recordId,
1204
+ recordType: attachmentOwner.recordType,
1205
+ resourceName: attachmentOwner.resourceName
1189
1206
  })
1190
1207
  .order([["position", "asc"]])
1191
1208
  }
@@ -1241,7 +1258,7 @@ export class FrontendModelAttachmentHandle {
1241
1258
  const commandUrl = frontendModelCommandUrl(resourcePath, commandName)
1242
1259
  const params = new URLSearchParams({
1243
1260
  attachmentName: this.attachmentName,
1244
- id: String(this.model.primaryKeyValue())
1261
+ id: modelPrimaryKeyCacheKey(ModelClass.primaryKey(), this.model.primaryKeyValue())
1245
1262
  })
1246
1263
 
1247
1264
  return `${commandUrl}?${params.toString()}`
@@ -1644,7 +1661,20 @@ class FrontendModelEventSubscription {
1644
1661
  if (action !== "create" && action !== "update" && action !== "destroy") return
1645
1662
  if (rawId === undefined || rawId === null) return
1646
1663
 
1647
- const id = String(rawId)
1664
+ const primaryKey = this.ModelClass.primaryKey()
1665
+ const identity = Array.isArray(primaryKey)
1666
+ ? modelPrimaryKeyConditions(primaryKey, rawId)
1667
+ : String(rawId)
1668
+ const id = modelPrimaryKeyCacheKey(primaryKey, identity)
1669
+ const rawPreviousId = body.previousId
1670
+ const previousIdentity = rawPreviousId === undefined || rawPreviousId === null
1671
+ ? null
1672
+ : Array.isArray(primaryKey)
1673
+ ? modelPrimaryKeyConditions(primaryKey, rawPreviousId)
1674
+ : String(rawPreviousId)
1675
+ const previousId = previousIdentity === null
1676
+ ? null
1677
+ : modelPrimaryKeyCacheKey(primaryKey, previousIdentity)
1648
1678
  const matchedEventFilterKeys = frontendModelMatchedEventFilterKeys(body)
1649
1679
 
1650
1680
  if (action === "destroy") {
@@ -1652,23 +1682,33 @@ class FrontendModelEventSubscription {
1652
1682
 
1653
1683
  if (listener) {
1654
1684
  for (const entry of listener.destroyCallbacks) {
1655
- try { entry.callback({id}) } catch (error) { console.error(error) }
1685
+ try { entry.callback({id: identity}) } catch (error) { console.error(error) }
1656
1686
  }
1657
- this.instanceListeners.delete(id)
1687
+ deleteFrontendModelInstanceListener(this, listener)
1658
1688
  }
1659
1689
  for (const entry of this.classDestroyCallbacks) {
1660
- try { entry.callback({id}) } catch (error) { console.error(error) }
1690
+ try { entry.callback({id: identity}) } catch (error) { console.error(error) }
1661
1691
  }
1662
1692
  return
1663
1693
  }
1664
1694
 
1695
+ const listener = this.instanceListeners.get(id) || (previousId === null ? undefined : this.instanceListeners.get(previousId))
1696
+
1697
+ if (action === "update" && listener && previousIdentity !== null) {
1698
+ applyFrontendModelPersistedIdentity(this.ModelClass, listener.instance, identity)
1699
+ rekeyFrontendModelInstanceListeners(this.ModelClass, listener.instance, previousIdentity, identity)
1700
+ }
1701
+
1665
1702
  if (!body.record || typeof body.record !== "object") return
1666
1703
 
1667
1704
  const deserializedRecord = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (deserializeFrontendModelTransportValue(body.record))
1668
1705
  const freshModel = /** @type {ReturnType<typeof JSON.parse>} */ (this.ModelClass).instantiateFromResponse(deserializedRecord)
1669
- const listener = this.instanceListeners.get(id)
1670
1706
 
1671
1707
  if (action === "update" && listener) {
1708
+ const instanceAny = /** @type {ReturnType<typeof JSON.parse>} */ (listener.instance)
1709
+
1710
+ instanceAny._attachmentOwner = freshModel._attachmentOwner
1711
+
1672
1712
  const matchingUpdateCallbacks = Array.from(listener.updateCallbacks).filter((entry) =>
1673
1713
  frontendModelEventEntryMatches(entry, matchedEventFilterKeys)
1674
1714
  )
@@ -1676,13 +1716,11 @@ class FrontendModelEventSubscription {
1676
1716
  if (matchingUpdateCallbacks.length > 0) {
1677
1717
  // Auto-merge into the registered instance so callers reading
1678
1718
  // through the same handle see fresh attributes.
1679
- const instanceAny = /** @type {ReturnType<typeof JSON.parse>} */ (listener.instance)
1680
-
1681
1719
  instanceAny.assignAttributes(freshModel.attributes())
1682
1720
  instanceAny._persistedAttributes = cloneFrontendModelAttributes(listener.instance.attributes())
1683
1721
 
1684
1722
  for (const entry of matchingUpdateCallbacks) {
1685
- try { entry.callback({id, model: listener.instance}) } catch (error) { console.error(error) }
1723
+ try { entry.callback({id: identity, model: listener.instance}) } catch (error) { console.error(error) }
1686
1724
  }
1687
1725
  }
1688
1726
  }
@@ -1692,7 +1730,7 @@ class FrontendModelEventSubscription {
1692
1730
  for (const entry of classCallbacks) {
1693
1731
  if (!frontendModelEventEntryMatches(entry, matchedEventFilterKeys)) continue
1694
1732
 
1695
- try { entry.callback({id, model: freshModel}) } catch (error) { console.error(error) }
1733
+ try { entry.callback({id: identity, model: freshModel}) } catch (error) { console.error(error) }
1696
1734
  }
1697
1735
  }
1698
1736
 
@@ -1810,25 +1848,129 @@ function ensureFrontendModelInstanceListener(sub, id, instance) {
1810
1848
  return listener
1811
1849
  }
1812
1850
 
1851
+ /**
1852
+ * Removes every identity key pointing at an instance listener.
1853
+ * @param {FrontendModelEventSubscription} sub - Event subscription bucket.
1854
+ * @param {ReturnType<typeof ensureFrontendModelInstanceListener>} listener - Instance listener bucket.
1855
+ * @returns {void}
1856
+ */
1857
+ function deleteFrontendModelInstanceListener(sub, listener) {
1858
+ for (const [id, current] of sub.instanceListeners) {
1859
+ if (current === listener) sub.instanceListeners.delete(id)
1860
+ }
1861
+ }
1862
+
1813
1863
  /**
1814
1864
  * Removes one instance callback entry and tears down an empty listener/subscription bucket.
1815
1865
  * @param {FrontendModelEventSubscription} sub - Event subscription bucket.
1816
- * @param {string} id - Model id.
1817
- * @param {(listener: {instance: FrontendModelBase, updateCallbacks: Set<FrontendModelModelEventCallbackEntry>, destroyCallbacks: Set<FrontendModelDestroyEventCallbackEntry>}) => void} removeEntry - Callback entry removal.
1866
+ * @param {(listener: ReturnType<typeof ensureFrontendModelInstanceListener>) => boolean} removeEntry - Callback entry removal.
1818
1867
  * @returns {void}
1819
1868
  */
1820
- function removeFrontendModelInstanceListenerEntry(sub, id, removeEntry) {
1821
- const current = sub.instanceListeners.get(id)
1869
+ function removeFrontendModelInstanceListenerEntry(sub, removeEntry) {
1870
+ for (const current of sub.instanceListeners.values()) {
1871
+ if (!removeEntry(current)) continue
1872
+
1873
+ if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
1874
+ deleteFrontendModelInstanceListener(sub, current)
1875
+ }
1876
+ break
1877
+ }
1878
+
1879
+ sub.maybeTeardown()
1880
+ }
1881
+
1882
+ /**
1883
+ * Temporarily registers an instance listener under its pending identity while retaining its persisted identity.
1884
+ * @param {FrontendModelClass} ModelClass - Frontend model class.
1885
+ * @param {FrontendModelBase} instance - Instance being re-keyed.
1886
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} previousIdentity - Persisted identity.
1887
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} nextIdentity - Pending identity sent to the server.
1888
+ * @returns {() => void} - Callback that removes the temporary aliases.
1889
+ */
1890
+ function aliasFrontendModelInstanceListeners(ModelClass, instance, previousIdentity, nextIdentity) {
1891
+ const primaryKey = ModelClass.primaryKey()
1892
+ const previousId = modelPrimaryKeyCacheKey(primaryKey, previousIdentity)
1893
+ const nextId = modelPrimaryKeyCacheKey(primaryKey, nextIdentity)
1894
+ /** @type {Array<{listener: ReturnType<typeof ensureFrontendModelInstanceListener>, sub: FrontendModelEventSubscription}>} */
1895
+ const aliases = []
1896
+
1897
+ if (previousId === nextId) return () => {}
1898
+
1899
+ const subscriptions = frontendModelEventSubscriptions.get(ModelClass)
1900
+
1901
+ if (!subscriptions) return () => {}
1822
1902
 
1823
- if (!current) return
1903
+ for (const sub of subscriptions.values()) {
1904
+ const listener = sub.instanceListeners.get(previousId)
1824
1905
 
1825
- removeEntry(current)
1906
+ if (!listener || listener.instance !== instance || sub.instanceListeners.has(nextId)) continue
1826
1907
 
1827
- if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
1828
- sub.instanceListeners.delete(id)
1908
+ sub.instanceListeners.set(nextId, listener)
1909
+ aliases.push({listener, sub})
1829
1910
  }
1830
1911
 
1831
- sub.maybeTeardown()
1912
+ return () => {
1913
+ for (const {listener, sub} of aliases) {
1914
+ if (sub.instanceListeners.get(previousId) === listener && sub.instanceListeners.get(nextId) === listener) {
1915
+ sub.instanceListeners.delete(nextId)
1916
+ }
1917
+ }
1918
+ }
1919
+ }
1920
+
1921
+ /**
1922
+ * Applies a remotely persisted identity to a listener instance without merging an unavailable record payload.
1923
+ * @param {FrontendModelClass} ModelClass - Frontend model class.
1924
+ * @param {FrontendModelBase} instance - Listener instance receiving the identity.
1925
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} identity - Persisted identity.
1926
+ * @returns {void}
1927
+ */
1928
+ function applyFrontendModelPersistedIdentity(ModelClass, instance, identity) {
1929
+ const identityAttributes = modelPrimaryKeyConditions(ModelClass.primaryKey(), identity)
1930
+
1931
+ instance.assignAttributes(identityAttributes)
1932
+
1933
+ for (const attributeName of Object.keys(identityAttributes)) {
1934
+ instance.markAttributeUnchanged(attributeName)
1935
+ }
1936
+ }
1937
+
1938
+ /**
1939
+ * Moves callbacks registered on an instance to its newly persisted identity.
1940
+ * @param {FrontendModelClass} ModelClass - Frontend model class.
1941
+ * @param {FrontendModelBase} instance - Re-keyed instance.
1942
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} previousIdentity - Previous persisted identity.
1943
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} nextIdentity - New persisted identity.
1944
+ * @returns {void}
1945
+ */
1946
+ function rekeyFrontendModelInstanceListeners(ModelClass, instance, previousIdentity, nextIdentity) {
1947
+ const primaryKey = ModelClass.primaryKey()
1948
+ const previousId = modelPrimaryKeyCacheKey(primaryKey, previousIdentity)
1949
+ const nextId = modelPrimaryKeyCacheKey(primaryKey, nextIdentity)
1950
+
1951
+ if (previousId === nextId) return
1952
+
1953
+ const subscriptions = frontendModelEventSubscriptions.get(ModelClass)
1954
+
1955
+ if (!subscriptions) return
1956
+
1957
+ for (const sub of subscriptions.values()) {
1958
+ const listener = sub.instanceListeners.get(previousId)
1959
+
1960
+ if (!listener || listener.instance !== instance) continue
1961
+
1962
+ const nextListener = sub.instanceListeners.get(nextId)
1963
+
1964
+ sub.instanceListeners.delete(previousId)
1965
+
1966
+ if (nextListener) {
1967
+ nextListener.instance = instance
1968
+ for (const entry of listener.updateCallbacks) nextListener.updateCallbacks.add(entry)
1969
+ for (const entry of listener.destroyCallbacks) nextListener.destroyCallbacks.add(entry)
1970
+ } else {
1971
+ sub.instanceListeners.set(nextId, listener)
1972
+ }
1973
+ }
1832
1974
  }
1833
1975
 
1834
1976
  /**
@@ -2333,6 +2475,11 @@ export default class FrontendModelBase {
2333
2475
  * @type {Array<FrontendModelBase> | undefined} - Shared reference to sibling records loaded in the same batch. Used by auto-batch-preload.
2334
2476
  */
2335
2477
  _loadCohort
2478
+ /**
2479
+ * Canonical backing-record attachment owner returned by the server.
2480
+ * @type {{recordId: string, recordType: string, resourceName: string} | null}
2481
+ */
2482
+ _attachmentOwner
2336
2483
 
2337
2484
  /**
2338
2485
  * Runs constructor.
@@ -2350,6 +2497,7 @@ export default class FrontendModelBase {
2350
2497
  this._isNewRecord = true
2351
2498
  this._markedForDestruction = false
2352
2499
  this._persistedAttributes = {}
2500
+ this._attachmentOwner = null
2353
2501
  if (attributes) this.assignAttributes(attributes)
2354
2502
  }
2355
2503
 
@@ -2724,6 +2872,9 @@ export default class FrontendModelBase {
2724
2872
  if (batch.length === 0) return false
2725
2873
 
2726
2874
  const primaryKey = ModelClass.primaryKey()
2875
+
2876
+ if (Array.isArray(primaryKey)) return false
2877
+
2727
2878
  const batchIds = batch.map((sibling) => sibling.primaryKeyValue())
2728
2879
  const reloadedBatch = await ModelClass
2729
2880
  .preload([relationshipName])
@@ -2736,11 +2887,11 @@ export default class FrontendModelBase {
2736
2887
  const reloadedById = new Map()
2737
2888
 
2738
2889
  for (const reloaded of reloadedBatch) {
2739
- reloadedById.set(String(reloaded.primaryKeyValue()), reloaded)
2890
+ reloadedById.set(modelPrimaryKeyCacheKey(primaryKey, reloaded.primaryKeyValue()), reloaded)
2740
2891
  }
2741
2892
 
2742
2893
  for (const sibling of batch) {
2743
- const key = String(sibling.primaryKeyValue())
2894
+ const key = modelPrimaryKeyCacheKey(primaryKey, sibling.primaryKeyValue())
2744
2895
  const reloaded = reloadedById.get(key)
2745
2896
 
2746
2897
  if (!reloaded) continue
@@ -2809,7 +2960,7 @@ export default class FrontendModelBase {
2809
2960
  /**
2810
2961
  * Runs primary key.
2811
2962
  * @this {FrontendModelClass}
2812
- * @returns {string} - Primary key name.
2963
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} - Primary key name.
2813
2964
  */
2814
2965
  static primaryKey() {
2815
2966
  return this.resourceConfig().primaryKey || "id"
@@ -2817,17 +2968,49 @@ export default class FrontendModelBase {
2817
2968
 
2818
2969
  /**
2819
2970
  * Runs primary key value.
2820
- * @returns {number | string} - Primary key value.
2971
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} - Primary key value.
2821
2972
  */
2822
2973
  primaryKeyValue() {
2823
2974
  const ModelClass = frontendModelClassFor(this)
2824
- const value = this.readAttribute(ModelClass.primaryKey())
2975
+ const primaryKey = ModelClass.primaryKey()
2825
2976
 
2826
- if (value === undefined || value === null) {
2827
- throw new Error(`Missing primary key '${ModelClass.primaryKey()}' on ${ModelClass.name}`)
2828
- }
2977
+ return readModelPrimaryKeyValue(primaryKey, (attributeName) => {
2978
+ const value = this.readAttribute(attributeName)
2979
+
2980
+ if (value === undefined || value === null) {
2981
+ throw new Error(`Missing primary key '${attributeName}' on ${ModelClass.name}`)
2982
+ }
2829
2983
 
2830
- return value
2984
+ return value
2985
+ })
2986
+ }
2987
+
2988
+ /**
2989
+ * Returns the scalar identity required by scalar-only frontend features.
2990
+ * @param {string} operation - Operation requiring a scalar identity.
2991
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyScalar} - Scalar primary-key value.
2992
+ */
2993
+ scalarPrimaryKeyValue(operation) {
2994
+ return scalarModelPrimaryKeyValue(this.primaryKeyValue(), operation)
2995
+ }
2996
+
2997
+ /**
2998
+ * Returns the identity represented by the last persisted frontend attributes.
2999
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} - Persisted primary-key value.
3000
+ */
3001
+ persistedPrimaryKeyValue() {
3002
+ const ModelClass = frontendModelClassFor(this)
3003
+ const primaryKey = ModelClass.primaryKey()
3004
+
3005
+ return readModelPrimaryKeyValue(primaryKey, (attributeName) => {
3006
+ const value = this._persistedAttributes[attributeName]
3007
+
3008
+ if (value === undefined || value === null) {
3009
+ throw new Error(`Missing persisted primary key '${attributeName}' on ${ModelClass.name}`)
3010
+ }
3011
+
3012
+ return value
3013
+ })
2831
3014
  }
2832
3015
 
2833
3016
  /**
@@ -3435,7 +3618,7 @@ export default class FrontendModelBase {
3435
3618
  * Runs model data from response.
3436
3619
  * @this {FrontendModelClass}
3437
3620
  * @param {object} response - Response payload.
3438
- * @returns {{abilities: Record<string, boolean>, attributes: Record<string, FrontendModelAttributeValue>, associationCounts: Record<string, number>, queryData: Record<string, FrontendModelAttributeValue>, preloadedRelationships: Record<string, FrontendModelAttributeValue>, selectedAttributes: Set<string>}} - Attributes, preloaded relationships, association counts, queryData, abilities, and the selected-attributes set.
3621
+ * @returns {{abilities: Record<string, boolean>, attachmentOwner: {recordId: string, recordType: string, resourceName: string} | null, attributes: Record<string, FrontendModelAttributeValue>, associationCounts: Record<string, number>, queryData: Record<string, FrontendModelAttributeValue>, preloadedRelationships: Record<string, FrontendModelAttributeValue>, selectedAttributes: Set<string>}} - Attributes, attachment owner, preloaded relationships, association counts, queryData, abilities, and selected attributes.
3439
3622
  */
3440
3623
  static modelDataFromResponse(response) {
3441
3624
  if (!response || typeof response !== "object") {
@@ -3476,7 +3659,24 @@ export default class FrontendModelBase {
3476
3659
  const selectedAttributesFromPayload = Array.isArray(attributes[SELECTED_ATTRIBUTES_KEY])
3477
3660
  ? new Set(/** @type {string[]} */ (attributes[SELECTED_ATTRIBUTES_KEY]).filter((attributeName) => typeof attributeName === "string"))
3478
3661
  : null
3662
+ const attachmentOwnerPayload = attributes[ATTACHMENT_OWNER_KEY]
3663
+ let attachmentOwner = null
3664
+
3665
+ if (attachmentOwnerPayload !== undefined) {
3666
+ if (!isPlainObject(attachmentOwnerPayload)) {
3667
+ throw new TypeError(`Expected ${ATTACHMENT_OWNER_KEY} to be an object`)
3668
+ }
3479
3669
 
3670
+ const attachmentOwnerObject = /** @type {{recordId?: unknown, recordType?: unknown, resourceName?: unknown}} */ (attachmentOwnerPayload)
3671
+
3672
+ attachmentOwner = {
3673
+ recordId: forcedNonBlankString(attachmentOwnerObject.recordId, `${ATTACHMENT_OWNER_KEY}.recordId`),
3674
+ recordType: forcedNonBlankString(attachmentOwnerObject.recordType, `${ATTACHMENT_OWNER_KEY}.recordType`),
3675
+ resourceName: forcedNonBlankString(attachmentOwnerObject.resourceName, `${ATTACHMENT_OWNER_KEY}.resourceName`)
3676
+ }
3677
+ }
3678
+
3679
+ delete attributes[ATTACHMENT_OWNER_KEY]
3480
3680
  delete attributes[PRELOADED_RELATIONSHIPS_KEY]
3481
3681
  delete attributes[SELECTED_ATTRIBUTES_KEY]
3482
3682
  delete attributes[ASSOCIATION_COUNTS_KEY]
@@ -3485,7 +3685,7 @@ export default class FrontendModelBase {
3485
3685
 
3486
3686
  const selectedAttributes = selectedAttributesFromPayload || new Set(Object.keys(attributes))
3487
3687
 
3488
- return {abilities, attributes, associationCounts, queryData, preloadedRelationships, selectedAttributes}
3688
+ return {abilities, attachmentOwner, attributes, associationCounts, queryData, preloadedRelationships, selectedAttributes}
3489
3689
  }
3490
3690
 
3491
3691
  /**
@@ -3578,10 +3778,12 @@ export default class FrontendModelBase {
3578
3778
  const associationCounts = modelData.associationCounts
3579
3779
  const queryData = modelData.queryData
3580
3780
  const abilities = modelData.abilities
3781
+ const attachmentOwner = modelData.attachmentOwner
3581
3782
  const selectedAttributes = modelData.selectedAttributes
3582
3783
  const receiver = /** @type {unknown} */ (this)
3583
3784
  const ModelClass = /** @type {new (attributes?: Record<string, FrontendModelAttributeValue>) => InstanceType<T>} */ (receiver)
3584
3785
  const model = new ModelClass(attributes)
3786
+ model._attachmentOwner = attachmentOwner
3585
3787
  model._selectedAttributes = selectedAttributes ? new Set(selectedAttributes) : null
3586
3788
 
3587
3789
  this.applyPreloadedRelationships(model, preloadedRelationships)
@@ -3608,7 +3810,7 @@ export default class FrontendModelBase {
3608
3810
  * Runs find.
3609
3811
  * @template {FrontendModelClass} T
3610
3812
  * @this {T}
3611
- * @param {number | string} id - Record identifier.
3813
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Record identifier.
3612
3814
  * @returns {Promise<InstanceType<T>>} - Resolved model.
3613
3815
  */
3614
3816
  static async find(id) {
@@ -3750,7 +3952,7 @@ export default class FrontendModelBase {
3750
3952
  * without re-checking per-record visibility. Query options can still
3751
3953
  * narrow which events reach this callback.
3752
3954
  * @this {FrontendModelClass}
3753
- * @param {(payload: {id: string, model: FrontendModelBase}) => void} callback - Event callback.
3955
+ * @param {(payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue, model: FrontendModelBase}) => void} callback - Event callback.
3754
3956
  * @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
3755
3957
  * @returns {Promise<() => void>} - Unsubscribe callback.
3756
3958
  */
@@ -3765,7 +3967,7 @@ export default class FrontendModelBase {
3765
3967
  /**
3766
3968
  * Class-level hook fired when any record of this model is updated.
3767
3969
  * @this {FrontendModelClass}
3768
- * @param {(payload: {id: string, model: FrontendModelBase}) => void} callback - Event callback.
3970
+ * @param {(payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue, model: FrontendModelBase}) => void} callback - Event callback.
3769
3971
  * @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
3770
3972
  * @returns {Promise<() => void>} - Unsubscribe callback.
3771
3973
  */
@@ -3780,7 +3982,7 @@ export default class FrontendModelBase {
3780
3982
  /**
3781
3983
  * Class-level hook fired when any record of this model is destroyed.
3782
3984
  * @this {FrontendModelClass}
3783
- * @param {(payload: {id: string}) => void} callback - Event callback.
3985
+ * @param {(payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue}) => void} callback - Event callback.
3784
3986
  * @param {import("./query.js").FrontendModelEventOptions} [options] - Accepted for API symmetry; destroy events carry ids only.
3785
3987
  * @returns {Promise<() => void>} - Unsubscribe callback.
3786
3988
  */
@@ -3799,16 +4001,16 @@ export default class FrontendModelBase {
3799
4001
  * instance's attributes are auto-merged with the broadcast payload
3800
4002
  * before the callback runs, so callers can read fresh values via
3801
4003
  * `this.someAttr()` without re-fetching.
3802
- * @param {(payload: {id: string, model: FrontendModelBase}) => void} callback - Event callback.
4004
+ * @param {(payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue, model: FrontendModelBase}) => void} callback - Event callback.
3803
4005
  * @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
3804
4006
  * @returns {Promise<() => void>} - Unsubscribe callback.
3805
4007
  */
3806
4008
  async onUpdate(callback, options = {}) {
3807
- const self = /** @type {ReturnType<typeof JSON.parse>} */ (this)
3808
4009
  const ModelClass = frontendModelClassFor(this)
3809
4010
  const {requestContext, ...eventOptionsPayload} = frontendModelEventOptionsPayload(ModelClass, options)
3810
4011
  const sub = ensureFrontendModelEventSubscription(ModelClass, frontendModelEventRequestContext(requestContext))
3811
- const id = String(self.id())
4012
+ const identity = this.isNewRecord() ? this.primaryKeyValue() : this.persistedPrimaryKeyValue()
4013
+ const id = modelPrimaryKeyCacheKey(ModelClass.primaryKey(), identity)
3812
4014
  const entry = {callback, ...eventOptionsPayload}
3813
4015
  const listener = ensureFrontendModelInstanceListener(sub, id, this)
3814
4016
 
@@ -3817,30 +4019,30 @@ export default class FrontendModelBase {
3817
4019
  try {
3818
4020
  await sub.ensureSubscribed()
3819
4021
  } catch (error) {
3820
- removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.updateCallbacks.delete(entry))
4022
+ removeFrontendModelInstanceListenerEntry(sub, (current) => current.updateCallbacks.delete(entry))
3821
4023
  throw error
3822
4024
  }
3823
4025
 
3824
4026
  return () => {
3825
- removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.updateCallbacks.delete(entry))
4027
+ removeFrontendModelInstanceListenerEntry(sub, (current) => current.updateCallbacks.delete(entry))
3826
4028
  }
3827
4029
  }
3828
4030
 
3829
4031
  /**
3830
4032
  * Instance-level hook fired when THIS record is destroyed.
3831
- * @param {(payload: {id: string}) => void} callback - Event callback.
4033
+ * @param {(payload: {id: string | import("../utils/model-primary-key.js").CompositeModelPrimaryKeyValue}) => void} callback - Event callback.
3832
4034
  * @param {import("./query.js").FrontendModelEventOptions} [options] - Accepted for API symmetry; destroy events carry ids only.
3833
4035
  * @returns {Promise<() => void>} - Unsubscribe callback.
3834
4036
  */
3835
4037
  async onDestroy(callback, options = {}) {
3836
- const self = /** @type {ReturnType<typeof JSON.parse>} */ (this)
3837
4038
  const ModelClass = frontendModelClassFor(this)
3838
4039
 
3839
4040
  assertNoDestroyEventFilter(ModelClass, options)
3840
4041
 
3841
4042
  const {requestContext} = frontendModelEventOptionsPayload(ModelClass, options)
3842
4043
  const sub = ensureFrontendModelEventSubscription(ModelClass, frontendModelEventRequestContext(requestContext))
3843
- const id = String(self.id())
4044
+ const identity = this.isNewRecord() ? this.primaryKeyValue() : this.persistedPrimaryKeyValue()
4045
+ const id = modelPrimaryKeyCacheKey(ModelClass.primaryKey(), identity)
3844
4046
  const entry = {callback}
3845
4047
  const listener = ensureFrontendModelInstanceListener(sub, id, this)
3846
4048
 
@@ -3849,12 +4051,12 @@ export default class FrontendModelBase {
3849
4051
  try {
3850
4052
  await sub.ensureSubscribed()
3851
4053
  } catch (error) {
3852
- removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.destroyCallbacks.delete(entry))
4054
+ removeFrontendModelInstanceListenerEntry(sub, (current) => current.destroyCallbacks.delete(entry))
3853
4055
  throw error
3854
4056
  }
3855
4057
 
3856
4058
  return () => {
3857
- removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.destroyCallbacks.delete(entry))
4059
+ removeFrontendModelInstanceListenerEntry(sub, (current) => current.destroyCallbacks.delete(entry))
3858
4060
  }
3859
4061
  }
3860
4062
 
@@ -4256,6 +4458,17 @@ export default class FrontendModelBase {
4256
4458
  async save() {
4257
4459
  const ModelClass = frontendModelClassFor(this)
4258
4460
  const isNew = this.isNewRecord()
4461
+ const primaryKey = ModelClass.primaryKey()
4462
+ const previousIdentity = isNew ? null : this.persistedPrimaryKeyValue()
4463
+ const listenerIdentityBeforeSave = isNew
4464
+ && Array.isArray(primaryKey)
4465
+ && primaryKey.every((attributeName) => {
4466
+ const value = this._attributes[attributeName]
4467
+
4468
+ return typeof value === "string" || typeof value === "number"
4469
+ })
4470
+ ? this.primaryKeyValue()
4471
+ : previousIdentity
4259
4472
  const commandType = isNew ? "create" : "update"
4260
4473
  /**
4261
4474
  * Payload.
@@ -4265,7 +4478,7 @@ export default class FrontendModelBase {
4265
4478
  }
4266
4479
 
4267
4480
  if (!isNew) {
4268
- payload.id = this.primaryKeyValue()
4481
+ payload.id = this.persistedPrimaryKeyValue()
4269
4482
  }
4270
4483
 
4271
4484
  const nestedAttributes = await this._buildNestedAttributesPayload()
@@ -4285,7 +4498,7 @@ export default class FrontendModelBase {
4285
4498
  let clientMutationId
4286
4499
 
4287
4500
  if (isNew) {
4288
- const primaryKey = ModelClass.primaryKey()
4501
+ const primaryKey = scalarModelPrimaryKey(ModelClass.primaryKey(), `Offline create for ${ModelClass.name}`)
4289
4502
  const currentPrimaryKey = this.readAttribute(primaryKey)
4290
4503
 
4291
4504
  if (currentPrimaryKey === undefined || currentPrimaryKey === null) {
@@ -4296,7 +4509,9 @@ export default class FrontendModelBase {
4296
4509
  offlineAttributes[primaryKey] = clientMutationId
4297
4510
  }
4298
4511
  } else {
4299
- offlineAttributes[ModelClass.primaryKey()] = payload.id
4512
+ const primaryKey = scalarModelPrimaryKey(ModelClass.primaryKey(), `Offline update for ${ModelClass.name}`)
4513
+
4514
+ offlineAttributes[primaryKey] = payload.id
4300
4515
  }
4301
4516
 
4302
4517
  if (payload.nestedAttributes !== undefined || payload.attachments !== undefined) {
@@ -4317,10 +4532,30 @@ export default class FrontendModelBase {
4317
4532
  return this
4318
4533
  }
4319
4534
 
4320
- const response = await ModelClass.executeCommand(commandType, payload)
4535
+ const removeTemporaryListenerAliases = previousIdentity === null
4536
+ ? () => {}
4537
+ : aliasFrontendModelInstanceListeners(ModelClass, this, previousIdentity, this.primaryKeyValue())
4538
+ let response
4539
+
4540
+ try {
4541
+ response = await ModelClass.executeCommand(commandType, payload)
4542
+ } catch (error) {
4543
+ removeTemporaryListenerAliases()
4544
+ throw error
4545
+ }
4546
+
4547
+ removeTemporaryListenerAliases()
4321
4548
 
4322
- this.assignAttributes(ModelClass.attributesFromResponse(response))
4549
+ const modelData = ModelClass.modelDataFromResponse(response)
4550
+
4551
+ this.assignAttributes(modelData.attributes)
4552
+ this._attachmentOwner = modelData.attachmentOwner
4323
4553
  this.setIsNewRecord(false)
4554
+
4555
+ if (listenerIdentityBeforeSave !== null) {
4556
+ rekeyFrontendModelInstanceListeners(ModelClass, this, listenerIdentityBeforeSave, this.primaryKeyValue())
4557
+ }
4558
+
4324
4559
  this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
4325
4560
  this._pendingNestedAttributes = {}
4326
4561
  this._clearPendingAttachments()
@@ -4369,11 +4604,13 @@ export default class FrontendModelBase {
4369
4604
  */
4370
4605
  async destroy() {
4371
4606
  const ModelClass = frontendModelClassFor(this)
4372
- const id = this.primaryKeyValue()
4607
+ const id = this.isNewRecord() ? this.primaryKeyValue() : this.persistedPrimaryKeyValue()
4373
4608
 
4374
4609
  if (shouldQueueFrontendModelOperationOffline(ModelClass, "destroy")) {
4610
+ const primaryKey = scalarModelPrimaryKey(ModelClass.primaryKey(), `Offline destroy for ${ModelClass.name}`)
4611
+
4375
4612
  await queueFrontendModelMutationOffline({
4376
- attributes: {[ModelClass.primaryKey()]: id},
4613
+ attributes: {[primaryKey]: id},
4377
4614
  ModelClass,
4378
4615
  operation: "destroy"
4379
4616
  })
@@ -4486,7 +4723,7 @@ export default class FrontendModelBase {
4486
4723
  async _nestedAttributesEntryForParentSave() {
4487
4724
  if (this.markedForDestruction()) {
4488
4725
  if (this.isNewRecord()) return null
4489
- return {id: this.primaryKeyValue(), _destroy: true}
4726
+ return {id: this.persistedPrimaryKeyValue(), _destroy: true}
4490
4727
  }
4491
4728
 
4492
4729
  const nestedAttributes = await this._buildNestedAttributesPayload()
@@ -4513,7 +4750,7 @@ export default class FrontendModelBase {
4513
4750
  /**
4514
4751
  * Entry.
4515
4752
  * @type {Record<string, ReturnType<typeof JSON.parse>>} */
4516
- const entry = {id: this.primaryKeyValue()}
4753
+ const entry = {id: this.persistedPrimaryKeyValue()}
4517
4754
 
4518
4755
  if (this.isChanged()) entry.attributes = this._changedAttributesForSave()
4519
4756
  if (hasAttachments) entry.attachments = attachments
@@ -4915,6 +5152,17 @@ export class VelociousAttachment extends FrontendModelBase {
4915
5152
  }
4916
5153
  }
4917
5154
 
5155
+ /**
5156
+ * Finds attachment metadata by its public id through the member authorization path.
5157
+ * @template {FrontendModelClass} T
5158
+ * @this {T}
5159
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Attachment id.
5160
+ * @returns {Promise<InstanceType<T>>} - Resolved attachment metadata.
5161
+ */
5162
+ static async find(id) {
5163
+ return this.instantiateFromResponse(await this.executeCommand("find", {id}))
5164
+ }
5165
+
4918
5166
  /**
4919
5167
  * Returns the attachment id.
4920
5168
  * @returns {string} - Attachment id.