@contember/bindx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +40 -0
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
  3. package/dist/adapter/MockAdapter.d.ts +81 -0
  4. package/dist/adapter/MockAdapter.d.ts.map +1 -0
  5. package/dist/adapter/MockQueryEngine.d.ts +43 -0
  6. package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
  7. package/dist/adapter/index.d.ts +5 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/types.d.ts +184 -0
  10. package/dist/adapter/types.d.ts.map +1 -0
  11. package/dist/brand/ComponentBrand.d.ts +25 -0
  12. package/dist/brand/ComponentBrand.d.ts.map +1 -0
  13. package/dist/contember/index.d.ts +10 -0
  14. package/dist/contember/index.d.ts.map +1 -0
  15. package/dist/core/ActionDispatcher.d.ts +64 -0
  16. package/dist/core/ActionDispatcher.d.ts.map +1 -0
  17. package/dist/core/Disposable.d.ts +57 -0
  18. package/dist/core/Disposable.d.ts.map +1 -0
  19. package/dist/core/EntityLoader.d.ts +80 -0
  20. package/dist/core/EntityLoader.d.ts.map +1 -0
  21. package/dist/core/PersistenceManager.d.ts +126 -0
  22. package/dist/core/PersistenceManager.d.ts.map +1 -0
  23. package/dist/core/SelectionResolver.d.ts +21 -0
  24. package/dist/core/SelectionResolver.d.ts.map +1 -0
  25. package/dist/core/actionClassification.d.ts +48 -0
  26. package/dist/core/actionClassification.d.ts.map +1 -0
  27. package/dist/core/actions.d.ts +301 -0
  28. package/dist/core/actions.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/dataview/filterHandlers.d.ts +18 -0
  32. package/dist/dataview/filterHandlers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +3 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/types.d.ts +164 -0
  36. package/dist/dataview/types.d.ts.map +1 -0
  37. package/dist/errors/UnfetchedFieldError.d.ts +11 -0
  38. package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
  39. package/dist/errors/errorPathResolver.d.ts +52 -0
  40. package/dist/errors/errorPathResolver.d.ts.map +1 -0
  41. package/dist/errors/index.d.ts +5 -0
  42. package/dist/errors/index.d.ts.map +1 -0
  43. package/dist/errors/pathMapper.d.ts +42 -0
  44. package/dist/errors/pathMapper.d.ts.map +1 -0
  45. package/dist/errors/types.d.ts +131 -0
  46. package/dist/errors/types.d.ts.map +1 -0
  47. package/dist/events/EventEmitter.d.ts +97 -0
  48. package/dist/events/EventEmitter.d.ts.map +1 -0
  49. package/dist/events/eventFactory.d.ts +40 -0
  50. package/dist/events/eventFactory.d.ts.map +1 -0
  51. package/dist/events/index.d.ts +15 -0
  52. package/dist/events/index.d.ts.map +1 -0
  53. package/dist/events/types.d.ts +264 -0
  54. package/dist/events/types.d.ts.map +1 -0
  55. package/dist/handles/BaseHandle.d.ts +64 -0
  56. package/dist/handles/BaseHandle.d.ts.map +1 -0
  57. package/dist/handles/EntityHandle.d.ts +208 -0
  58. package/dist/handles/EntityHandle.d.ts.map +1 -0
  59. package/dist/handles/FieldHandle.d.ts +111 -0
  60. package/dist/handles/FieldHandle.d.ts.map +1 -0
  61. package/dist/handles/HasManyListHandle.d.ts +166 -0
  62. package/dist/handles/HasManyListHandle.d.ts.map +1 -0
  63. package/dist/handles/HasOneHandle.d.ts +168 -0
  64. package/dist/handles/HasOneHandle.d.ts.map +1 -0
  65. package/dist/handles/PlaceholderHandle.d.ts +117 -0
  66. package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
  67. package/dist/handles/index.d.ts +8 -0
  68. package/dist/handles/index.d.ts.map +1 -0
  69. package/dist/handles/proxyFactory.d.ts +66 -0
  70. package/dist/handles/proxyFactory.d.ts.map +1 -0
  71. package/dist/handles/types.d.ts +430 -0
  72. package/dist/handles/types.d.ts.map +1 -0
  73. package/dist/index.d.ts +53 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/internal.d.ts +11 -0
  76. package/dist/internal.d.ts.map +1 -0
  77. package/dist/persistence/BatchPersister.d.ts +142 -0
  78. package/dist/persistence/BatchPersister.d.ts.map +1 -0
  79. package/dist/persistence/ChangeRegistry.d.ts +87 -0
  80. package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
  81. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
  82. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
  83. package/dist/persistence/MutationCollector.d.ts +96 -0
  84. package/dist/persistence/MutationCollector.d.ts.map +1 -0
  85. package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
  86. package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
  87. package/dist/persistence/index.d.ts +7 -0
  88. package/dist/persistence/index.d.ts.map +1 -0
  89. package/dist/persistence/types.d.ts +189 -0
  90. package/dist/persistence/types.d.ts.map +1 -0
  91. package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
  92. package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
  93. package/dist/roles/index.d.ts +11 -0
  94. package/dist/roles/index.d.ts.map +1 -0
  95. package/dist/roles/types.d.ts +242 -0
  96. package/dist/roles/types.d.ts.map +1 -0
  97. package/dist/schema/ContemberSchema.d.ts +98 -0
  98. package/dist/schema/ContemberSchema.d.ts.map +1 -0
  99. package/dist/schema/SchemaLoader.d.ts +28 -0
  100. package/dist/schema/SchemaLoader.d.ts.map +1 -0
  101. package/dist/schema/SchemaRegistry.d.ts +95 -0
  102. package/dist/schema/SchemaRegistry.d.ts.map +1 -0
  103. package/dist/schema/index.d.ts +6 -0
  104. package/dist/schema/index.d.ts.map +1 -0
  105. package/dist/schema/roles.d.ts +62 -0
  106. package/dist/schema/roles.d.ts.map +1 -0
  107. package/dist/schema/types.d.ts +179 -0
  108. package/dist/schema/types.d.ts.map +1 -0
  109. package/dist/selection/SelectionMetaCollector.d.ts +32 -0
  110. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
  111. package/dist/selection/SelectionScope.d.ts +77 -0
  112. package/dist/selection/SelectionScope.d.ts.map +1 -0
  113. package/dist/selection/buildQuery.d.ts +45 -0
  114. package/dist/selection/buildQuery.d.ts.map +1 -0
  115. package/dist/selection/createFragment.d.ts +29 -0
  116. package/dist/selection/createFragment.d.ts.map +1 -0
  117. package/dist/selection/createSelectionBuilder.d.ts +18 -0
  118. package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
  119. package/dist/selection/index.d.ts +8 -0
  120. package/dist/selection/index.d.ts.map +1 -0
  121. package/dist/selection/queryTypes.d.ts +84 -0
  122. package/dist/selection/queryTypes.d.ts.map +1 -0
  123. package/dist/selection/types.d.ts +238 -0
  124. package/dist/selection/types.d.ts.map +1 -0
  125. package/dist/store/DirtyTracker.d.ts +26 -0
  126. package/dist/store/DirtyTracker.d.ts.map +1 -0
  127. package/dist/store/EntityMetaStore.d.ts +52 -0
  128. package/dist/store/EntityMetaStore.d.ts.map +1 -0
  129. package/dist/store/EntitySnapshotStore.d.ts +63 -0
  130. package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
  131. package/dist/store/ErrorStore.d.ts +82 -0
  132. package/dist/store/ErrorStore.d.ts.map +1 -0
  133. package/dist/store/RelationStore.d.ts +175 -0
  134. package/dist/store/RelationStore.d.ts.map +1 -0
  135. package/dist/store/SnapshotStore.d.ts +143 -0
  136. package/dist/store/SnapshotStore.d.ts.map +1 -0
  137. package/dist/store/SubscriptionManager.d.ts +90 -0
  138. package/dist/store/SubscriptionManager.d.ts.map +1 -0
  139. package/dist/store/TouchedStore.d.ts +24 -0
  140. package/dist/store/TouchedStore.d.ts.map +1 -0
  141. package/dist/store/entityId.d.ts +29 -0
  142. package/dist/store/entityId.d.ts.map +1 -0
  143. package/dist/store/index.d.ts +1 -0
  144. package/dist/store/index.d.ts.map +1 -0
  145. package/dist/store/snapshots.d.ts +144 -0
  146. package/dist/store/snapshots.d.ts.map +1 -0
  147. package/dist/undo/UndoManager.d.ts +120 -0
  148. package/dist/undo/UndoManager.d.ts.map +1 -0
  149. package/dist/undo/index.d.ts +3 -0
  150. package/dist/undo/index.d.ts.map +1 -0
  151. package/dist/undo/types.d.ts +74 -0
  152. package/dist/undo/types.d.ts.map +1 -0
  153. package/dist/utils/aliasGenerator.d.ts +25 -0
  154. package/dist/utils/aliasGenerator.d.ts.map +1 -0
  155. package/dist/utils/deepEqual.d.ts +6 -0
  156. package/dist/utils/deepEqual.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +3 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/package.json +29 -0
  160. package/src/adapter/ContemberAdapter.ts +239 -0
  161. package/src/adapter/MockAdapter.ts +394 -0
  162. package/src/adapter/MockQueryEngine.ts +236 -0
  163. package/src/adapter/index.ts +21 -0
  164. package/src/adapter/types.ts +199 -0
  165. package/src/brand/ComponentBrand.ts +28 -0
  166. package/src/contember/index.ts +17 -0
  167. package/src/core/ActionDispatcher.ts +450 -0
  168. package/src/core/Disposable.ts +126 -0
  169. package/src/core/EntityLoader.ts +169 -0
  170. package/src/core/SelectionResolver.ts +78 -0
  171. package/src/core/actionClassification.ts +196 -0
  172. package/src/core/actions.ts +491 -0
  173. package/src/core/index.ts +20 -0
  174. package/src/dataview/filterHandlers.ts +443 -0
  175. package/src/dataview/index.ts +36 -0
  176. package/src/dataview/types.ts +220 -0
  177. package/src/errors/UnfetchedFieldError.ts +18 -0
  178. package/src/errors/errorPathResolver.ts +239 -0
  179. package/src/errors/index.ts +40 -0
  180. package/src/errors/pathMapper.ts +40 -0
  181. package/src/errors/types.ts +223 -0
  182. package/src/events/EventEmitter.ts +447 -0
  183. package/src/events/eventFactory.ts +430 -0
  184. package/src/events/index.ts +89 -0
  185. package/src/events/types.ts +374 -0
  186. package/src/handles/BaseHandle.ts +100 -0
  187. package/src/handles/EntityHandle.ts +538 -0
  188. package/src/handles/FieldHandle.ts +268 -0
  189. package/src/handles/HasManyListHandle.ts +500 -0
  190. package/src/handles/HasOneHandle.ts +500 -0
  191. package/src/handles/PlaceholderHandle.ts +321 -0
  192. package/src/handles/index.ts +37 -0
  193. package/src/handles/proxyFactory.ts +218 -0
  194. package/src/handles/types.ts +743 -0
  195. package/src/index.ts +384 -0
  196. package/src/internal.ts +19 -0
  197. package/src/persistence/BatchPersister.ts +911 -0
  198. package/src/persistence/ChangeRegistry.ts +150 -0
  199. package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
  200. package/src/persistence/MutationCollector.ts +567 -0
  201. package/src/persistence/MutationSchemaProvider.ts +33 -0
  202. package/src/persistence/index.ts +23 -0
  203. package/src/persistence/types.ts +220 -0
  204. package/src/schema/ContemberSchema.ts +132 -0
  205. package/src/schema/SchemaLoader.ts +93 -0
  206. package/src/schema/SchemaRegistry.ts +286 -0
  207. package/src/schema/index.ts +51 -0
  208. package/src/schema/roles.ts +83 -0
  209. package/src/schema/types.ts +238 -0
  210. package/src/selection/SelectionMetaCollector.ts +122 -0
  211. package/src/selection/SelectionScope.ts +208 -0
  212. package/src/selection/buildQuery.ts +134 -0
  213. package/src/selection/createFragment.ts +50 -0
  214. package/src/selection/createSelectionBuilder.ts +283 -0
  215. package/src/selection/index.ts +31 -0
  216. package/src/selection/queryTypes.ts +135 -0
  217. package/src/selection/types.ts +320 -0
  218. package/src/store/DirtyTracker.ts +117 -0
  219. package/src/store/EntityMetaStore.ts +143 -0
  220. package/src/store/EntitySnapshotStore.ts +260 -0
  221. package/src/store/ErrorStore.ts +251 -0
  222. package/src/store/RelationStore.ts +696 -0
  223. package/src/store/SnapshotStore.ts +765 -0
  224. package/src/store/SubscriptionManager.ts +241 -0
  225. package/src/store/TouchedStore.ts +42 -0
  226. package/src/store/entityId.ts +43 -0
  227. package/src/store/index.ts +2 -0
  228. package/src/store/snapshots.ts +203 -0
  229. package/src/undo/UndoManager.ts +418 -0
  230. package/src/undo/index.ts +9 -0
  231. package/src/undo/types.ts +84 -0
  232. package/src/utils/aliasGenerator.ts +56 -0
  233. package/src/utils/deepEqual.ts +33 -0
  234. package/src/utils/index.ts +2 -0
@@ -0,0 +1,500 @@
1
+ import { EntityRelatedHandle } from './BaseHandle.js'
2
+ import { EntityHandle } from './EntityHandle.js'
3
+ import type { ActionDispatcher } from '../core/ActionDispatcher.js'
4
+ import type { SnapshotStore } from '../store/SnapshotStore.js'
5
+ import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
6
+ import type { SelectionMeta } from '../selection/types.js'
7
+ import {
8
+ addRelationError,
9
+ clearRelationErrors,
10
+ } from '../core/actions.js'
11
+ import { FIELD_REF_META, type HasManyRef, type FieldRefMeta, type EntityAccessor, type Unsubscribe } from './types.js'
12
+ import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
13
+ import type {
14
+ EventListener,
15
+ Interceptor,
16
+ HasManyConnectedEvent,
17
+ HasManyDisconnectedEvent,
18
+ HasManyConnectingEvent,
19
+ HasManyDisconnectingEvent,
20
+ } from '../events/types.js'
21
+ import { createAliasProxy } from './proxyFactory.js'
22
+
23
+ /**
24
+ * HasManyListHandle provides access to a has-many relation (list of entities).
25
+ * Implements HasManyRef interface for JSX compatibility.
26
+ *
27
+ * @typeParam TEntity - The full entity type of items in the relation
28
+ * @typeParam TSelected - The selected subset of fields (defaults to TEntity for backwards compatibility)
29
+ */
30
+ export class HasManyListHandle<TEntity extends object = object, TSelected = TEntity> extends EntityRelatedHandle implements HasManyRef<TEntity, TSelected> {
31
+ private itemHandleCache = new Map<string, EntityHandle<TEntity, TSelected>>()
32
+
33
+ /** Runtime brand symbols for validation */
34
+ readonly __brands?: Set<symbol>
35
+
36
+ /**
37
+ * Type brand - entity name for type inference.
38
+ * Implements HasManyRef.__entityName.
39
+ */
40
+ get __entityName(): string {
41
+ return this.itemType
42
+ }
43
+
44
+ /**
45
+ * Type brand - schema for type inference.
46
+ * Returns undefined at runtime; schema type is compile-time only.
47
+ */
48
+ get __schema(): undefined {
49
+ return undefined
50
+ }
51
+
52
+ /**
53
+ * Alias for this has-many relation.
54
+ * When the same field is used multiple times with different params,
55
+ * each instance has a unique alias to store data separately.
56
+ */
57
+ private readonly alias: string
58
+
59
+ private constructor(
60
+ parentEntityType: string,
61
+ parentEntityId: string,
62
+ private readonly fieldName: string,
63
+ private readonly itemType: string,
64
+ store: SnapshotStore,
65
+ dispatcher: ActionDispatcher,
66
+ private readonly schema: SchemaRegistry,
67
+ brands?: Set<symbol>,
68
+ alias?: string,
69
+ private readonly selection?: SelectionMeta,
70
+ ) {
71
+ super(parentEntityType, parentEntityId, store, dispatcher)
72
+ this.__brands = brands
73
+ this.alias = alias ?? fieldName
74
+ }
75
+
76
+ static create<TEntity extends object = object, TSelected = TEntity>(
77
+ parentEntityType: string,
78
+ parentEntityId: string,
79
+ fieldName: string,
80
+ itemType: string,
81
+ store: SnapshotStore,
82
+ dispatcher: ActionDispatcher,
83
+ schema: SchemaRegistry,
84
+ brands?: Set<symbol>,
85
+ alias?: string,
86
+ selection?: SelectionMeta,
87
+ ): HasManyListHandle<TEntity, TSelected> {
88
+ return createAliasProxy(new HasManyListHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, itemType, store, dispatcher, schema, brands, alias, selection))
89
+ }
90
+
91
+ /**
92
+ * JSX field reference metadata for collection phase.
93
+ * Implements HasManyRef interface.
94
+ */
95
+ get [FIELD_REF_META](): FieldRefMeta {
96
+ return {
97
+ entityType: this.entityType,
98
+ entityId: this.entityId,
99
+ path: [this.fieldName],
100
+ fieldName: this.fieldName,
101
+ isArray: true,
102
+ isRelation: true,
103
+ targetType: this.itemType,
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Gets the list of items as entity accessors with direct field access.
109
+ * Returns selection-aware EntityAccessors that support `item.fieldName.value`.
110
+ * Includes planned connections and excludes planned removals.
111
+ * Uses ordered IDs to preserve order including after move() operations.
112
+ */
113
+ get items(): EntityAccessor<TEntity, TSelected>[] {
114
+ const data = this.getEntityData()
115
+ if (!data) return []
116
+
117
+ const rawData = data[this.alias] ?? data[this.fieldName]
118
+ const listData = this.extractItems(rawData)
119
+ if (!listData) return []
120
+
121
+ // Ensure snapshots exist for embedded items
122
+ this.ensureItemSnapshots(listData)
123
+
124
+ // Extract server IDs from embedded data
125
+ const serverIds = listData
126
+ .map((item) => item['id'] as string | undefined)
127
+ .filter((id): id is string => id !== undefined)
128
+
129
+ // Ensure has-many state exists with proper server IDs
130
+ // This is needed so that connect/disconnect operations work correctly
131
+ this.store.getOrCreateHasMany(
132
+ this.entityType,
133
+ this.entityId,
134
+ this.fieldName,
135
+ serverIds,
136
+ this.alias,
137
+ )
138
+
139
+ // Use ordered IDs from store (handles removals, connections, and ordering)
140
+ const orderedIds = this.store.getHasManyOrderedIds(
141
+ this.entityType,
142
+ this.entityId,
143
+ this.fieldName,
144
+ this.alias,
145
+ )
146
+
147
+ return orderedIds.map((id) => this.getItemHandle(id))
148
+ }
149
+
150
+ /**
151
+ * Ensures snapshots exist for embedded has-many items.
152
+ */
153
+ private ensureItemSnapshots(listData: Array<Record<string, unknown>>): void {
154
+ for (const itemData of listData) {
155
+ const itemId = itemData['id'] as string
156
+ if (!itemId) continue
157
+
158
+ // Register parent-child relationship for change propagation
159
+ // This needs to happen even if the snapshot already exists
160
+ this.store.registerParentChild(this.entityType, this.entityId, this.itemType, itemId)
161
+
162
+ // Skip if snapshot already exists
163
+ if (this.store.hasEntity(this.itemType, itemId)) {
164
+ continue
165
+ }
166
+
167
+ // Create snapshot from embedded data
168
+ // Skip notification to avoid triggering React state updates during render
169
+ this.store.setEntityData(
170
+ this.itemType,
171
+ itemId,
172
+ itemData,
173
+ true, // isServerData
174
+ true, // skipNotify - called during render, data already exists embedded in parent
175
+ )
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Extracts items from either flat array or Connection format (paginateRelation).
181
+ * Returns null if data is not in a recognized format.
182
+ */
183
+ private extractItems(rawData: unknown): Array<Record<string, unknown>> | null {
184
+ // Flat array format: [{ id, name }, ...]
185
+ if (Array.isArray(rawData)) {
186
+ return rawData
187
+ }
188
+ // Connection format: { pageInfo: { totalCount }, edges: [{ node: { id, name } }] }
189
+ if (rawData && typeof rawData === 'object' && 'edges' in rawData) {
190
+ const connection = rawData as { edges: Array<{ node: Record<string, unknown> }> }
191
+ return connection.edges.map(edge => edge.node)
192
+ }
193
+ return null
194
+ }
195
+
196
+ /**
197
+ * Gets the total count from paginateRelation response.
198
+ * Content client attaches totalCount as a non-enumerable property on the array.
199
+ * Returns undefined if totalCount was not requested or not available.
200
+ */
201
+ get totalCount(): number | undefined {
202
+ const data = this.getEntityData()
203
+ if (!data) return undefined
204
+
205
+ const rawData = data[this.alias] ?? data[this.fieldName]
206
+ if (Array.isArray(rawData) && 'totalCount' in rawData) {
207
+ return (rawData as unknown as { totalCount: number }).totalCount
208
+ }
209
+ return undefined
210
+ }
211
+
212
+ /**
213
+ * Gets the number of items.
214
+ */
215
+ get length(): number {
216
+ return this.items.length
217
+ }
218
+
219
+ /**
220
+ * Gets a handle for a specific item.
221
+ * Returns selection-aware EntityAccessor that supports direct field access.
222
+ */
223
+ getItemHandle(itemId: string): EntityAccessor<TEntity, TSelected> {
224
+ let handle = this.itemHandleCache.get(itemId)
225
+
226
+ if (!handle) {
227
+ handle = EntityHandle.create<TEntity, TSelected>(
228
+ itemId,
229
+ this.itemType,
230
+ this.store,
231
+ this.dispatcher,
232
+ this.schema,
233
+ this.__brands,
234
+ this.selection,
235
+ )
236
+ this.itemHandleCache.set(itemId, handle)
237
+ }
238
+
239
+ // EntityHandle constructor returns a Proxy that implements EntityAccessor
240
+ return handle as unknown as EntityAccessor<TEntity, TSelected>
241
+ }
242
+
243
+ /**
244
+ * Gets an item handle by entity ID.
245
+ * Works for both server entities and newly created entities (via add()).
246
+ * Returns an EntityAccessor with direct field access.
247
+ */
248
+ getById(id: string): EntityAccessor<TEntity, TSelected> {
249
+ return this.getItemHandle(id)
250
+ }
251
+
252
+ /**
253
+ * Checks if the list is dirty (items added/removed/moved/connected/disconnected).
254
+ */
255
+ get isDirty(): boolean {
256
+ const state = this.store.getHasMany(
257
+ this.entityType,
258
+ this.entityId,
259
+ this.fieldName,
260
+ this.alias,
261
+ )
262
+
263
+ if (!state) return false
264
+
265
+ return (
266
+ state.plannedRemovals.size > 0 ||
267
+ state.plannedConnections.size > 0 ||
268
+ state.createdEntities.size > 0 ||
269
+ state.orderedIds !== null
270
+ )
271
+ }
272
+
273
+ /**
274
+ * Maps over items.
275
+ * Implements HasManyRef interface - returns selection-aware entity accessors with direct field access.
276
+ */
277
+ map<R>(fn: (item: EntityAccessor<TEntity, TSelected>, index: number) => R): R[] {
278
+ return this.items.map((handle, index) => fn(handle, index))
279
+ }
280
+
281
+ /**
282
+ * Connects an existing entity to this has-many relation.
283
+ */
284
+ connect(itemId: string): void {
285
+ this.assertNotDisposed()
286
+ this.store.planHasManyConnection(
287
+ this.entityType,
288
+ this.entityId,
289
+ this.fieldName,
290
+ itemId,
291
+ this.alias,
292
+ )
293
+
294
+ // Register parent-child so that changes to the connected entity propagate to the parent
295
+ this.store.registerParentChild(this.entityType, this.entityId, this.itemType, itemId)
296
+ }
297
+
298
+ /**
299
+ * Disconnects an entity from this has-many relation.
300
+ */
301
+ disconnect(itemId: string): void {
302
+ this.assertNotDisposed()
303
+ this.store.planHasManyRemoval(
304
+ this.entityType,
305
+ this.entityId,
306
+ this.fieldName,
307
+ itemId,
308
+ 'disconnect',
309
+ this.alias,
310
+ )
311
+ }
312
+
313
+ /**
314
+ * Adds a new item to the list by creating a new entity.
315
+ * Returns the ID of the newly created entity (temp ID).
316
+ * For connecting existing entities, use connect() instead.
317
+ */
318
+ add(data?: Partial<TEntity>): string {
319
+ this.assertNotDisposed()
320
+
321
+ // Create new entity with temp ID
322
+ const tempId = this.store.createEntity(this.itemType, data as Record<string, unknown>)
323
+
324
+ // Add to has-many relation
325
+ this.store.addToHasMany(
326
+ this.entityType,
327
+ this.entityId,
328
+ this.fieldName,
329
+ tempId,
330
+ this.alias,
331
+ )
332
+
333
+ // Register parent-child so that changes to the new entity propagate to the parent
334
+ this.store.registerParentChild(this.entityType, this.entityId, this.itemType, tempId)
335
+
336
+ return tempId
337
+ }
338
+
339
+ /**
340
+ * Removes an item from the list by ID.
341
+ * For newly created entities (via add()), cancels the add operation.
342
+ * For existing server entities, plans a disconnect.
343
+ */
344
+ remove(itemId: string): void {
345
+ this.assertNotDisposed()
346
+
347
+ this.store.removeFromHasMany(
348
+ this.entityType,
349
+ this.entityId,
350
+ this.fieldName,
351
+ itemId,
352
+ this.alias,
353
+ )
354
+ }
355
+
356
+ /**
357
+ * Moves an item from one position to another within the list.
358
+ * Note: This only affects client-side order. For persistent ordering,
359
+ * use an 'order' field on the entity.
360
+ */
361
+ move(fromIndex: number, toIndex: number): void {
362
+ this.assertNotDisposed()
363
+
364
+ this.store.moveInHasMany(
365
+ this.entityType,
366
+ this.entityId,
367
+ this.fieldName,
368
+ fromIndex,
369
+ toIndex,
370
+ this.alias,
371
+ )
372
+ }
373
+
374
+ /**
375
+ * Resets the has-many relation to server state.
376
+ * Clears all planned connections and removals.
377
+ */
378
+ reset(): void {
379
+ this.assertNotDisposed()
380
+ this.store.resetHasMany(
381
+ this.entityType,
382
+ this.entityId,
383
+ this.fieldName,
384
+ this.alias,
385
+ )
386
+ }
387
+
388
+ /**
389
+ * Disposes the handle.
390
+ */
391
+ override dispose(): void {
392
+ super.dispose()
393
+
394
+ for (const handle of this.itemHandleCache.values()) {
395
+ handle.dispose()
396
+ }
397
+ this.itemHandleCache.clear()
398
+ }
399
+
400
+ /**
401
+ * Type brand - ensures HasManyRef<Author> is not assignable to HasManyRef<Tag>.
402
+ * This is a phantom property that only exists in the type system.
403
+ */
404
+ get __entityType(): TEntity {
405
+ return undefined as unknown as TEntity
406
+ }
407
+
408
+ /**
409
+ * Gets the list of errors on this relation.
410
+ */
411
+ get errors(): readonly FieldError[] {
412
+ return this.store.getRelationErrors(this.entityType, this.entityId, this.fieldName)
413
+ }
414
+
415
+ /**
416
+ * Checks if this relation has any errors.
417
+ */
418
+ get hasError(): boolean {
419
+ return this.errors.length > 0
420
+ }
421
+
422
+ /**
423
+ * Adds a client-side error to this relation.
424
+ */
425
+ addError(error: ErrorInput): void {
426
+ this.assertNotDisposed()
427
+ this.dispatcher.dispatch(
428
+ addRelationError(this.entityType, this.entityId, this.fieldName, createClientError(error)),
429
+ )
430
+ }
431
+
432
+ /**
433
+ * Clears all errors from this relation.
434
+ */
435
+ clearErrors(): void {
436
+ this.assertNotDisposed()
437
+ this.dispatcher.dispatch(
438
+ clearRelationErrors(this.entityType, this.entityId, this.fieldName),
439
+ )
440
+ }
441
+
442
+ // ==================== Event Subscriptions ====================
443
+
444
+ /**
445
+ * Subscribe to item connected events.
446
+ */
447
+ onItemConnected(listener: EventListener<HasManyConnectedEvent>): Unsubscribe {
448
+ const emitter = this.dispatcher.getEventEmitter()
449
+ return emitter.onField(
450
+ 'hasMany:connected',
451
+ this.entityType,
452
+ this.entityId,
453
+ this.fieldName,
454
+ listener,
455
+ )
456
+ }
457
+
458
+ /**
459
+ * Subscribe to item disconnected events.
460
+ */
461
+ onItemDisconnected(listener: EventListener<HasManyDisconnectedEvent>): Unsubscribe {
462
+ const emitter = this.dispatcher.getEventEmitter()
463
+ return emitter.onField(
464
+ 'hasMany:disconnected',
465
+ this.entityType,
466
+ this.entityId,
467
+ this.fieldName,
468
+ listener,
469
+ )
470
+ }
471
+
472
+ /**
473
+ * Intercept item connection (can cancel).
474
+ */
475
+ interceptItemConnecting(interceptor: Interceptor<HasManyConnectingEvent>): Unsubscribe {
476
+ const emitter = this.dispatcher.getEventEmitter()
477
+ return emitter.interceptField(
478
+ 'hasMany:connecting',
479
+ this.entityType,
480
+ this.entityId,
481
+ this.fieldName,
482
+ interceptor,
483
+ )
484
+ }
485
+
486
+ /**
487
+ * Intercept item disconnection (can cancel).
488
+ */
489
+ interceptItemDisconnecting(interceptor: Interceptor<HasManyDisconnectingEvent>): Unsubscribe {
490
+ const emitter = this.dispatcher.getEventEmitter()
491
+ return emitter.interceptField(
492
+ 'hasMany:disconnecting',
493
+ this.entityType,
494
+ this.entityId,
495
+ this.fieldName,
496
+ interceptor,
497
+ )
498
+ }
499
+
500
+ }