@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,538 @@
1
+ import { EntityRelatedHandle } from './BaseHandle.js'
2
+ import { FieldHandle } from './FieldHandle.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 { EntitySnapshot } from '../store/snapshots.js'
7
+ import {
8
+ resetEntity,
9
+ commitEntity,
10
+ addEntityError,
11
+ clearEntityErrors,
12
+ clearAllErrors as clearAllErrorsAction,
13
+ } from '../core/actions.js'
14
+ import { type EntityRef, type SelectedEntityFields, type Unsubscribe, type EntityAccessor } from './types.js'
15
+ import { deepEqual } from '../utils/deepEqual.js'
16
+ import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
17
+ import type {
18
+ EventTypeMap,
19
+ AfterEventTypes,
20
+ BeforeEventTypes,
21
+ EventListener,
22
+ Interceptor,
23
+ EntityPersistedEvent,
24
+ EntityPersistingEvent,
25
+ } from '../events/types.js'
26
+ import { HasOneHandle } from './HasOneHandle.js'
27
+ import { HasManyListHandle } from './HasManyListHandle.js'
28
+ import { createHandleProxy, ENTITY_HANDLE_PROPERTIES } from './proxyFactory.js'
29
+ import type { SelectionMeta } from '../selection/types.js'
30
+ import { UnfetchedFieldError } from '../errors/UnfetchedFieldError.js'
31
+
32
+ // Type for relation handle cache
33
+ type RelationHandle = HasOneHandle<object> | HasManyListHandle<object>
34
+
35
+ /**
36
+ * EntityHandle provides stable access to an entity.
37
+ * Implements EntityRef interface for consistent usage across the system.
38
+ *
39
+ * Supports direct field access for shorter chains:
40
+ * - `entity.fieldName` is equivalent to `entity.$fields.fieldName`
41
+ * - Handle properties use $ prefix to avoid collision with field names: `$id`, `$data`, `$isDirty`, etc.
42
+ *
43
+ * Key characteristics:
44
+ * - Stable identity (same instance across renders when id doesn't change)
45
+ * - Provides field handles for individual fields
46
+ * - Provides relation handles for has-one and has-many relations
47
+ * - Tracks dirty state across all fields
48
+ *
49
+ * @typeParam T - The full entity type
50
+ * @typeParam TSelected - The selected subset of fields (defaults to T for backwards compatibility)
51
+ */
52
+ export class EntityHandle<T extends object = object, TSelected = T> extends EntityRelatedHandle implements EntityRef<T, TSelected> {
53
+ /** Cache for field handles to ensure stable identity */
54
+ private readonly fieldHandleCache = new Map<string, FieldHandle<unknown>>()
55
+
56
+ /** Cache for relation handles */
57
+ private readonly relationHandleCache = new Map<string, RelationHandle>()
58
+
59
+ /** Runtime brand symbols for validation */
60
+ readonly __brands?: Set<symbol>
61
+
62
+ /** Type brand for schema - placeholder at runtime */
63
+ declare readonly __schema: Record<string, object>
64
+
65
+ // $ aliases - handled by proxy at runtime, declared for TypeScript
66
+ declare readonly $fields: SelectedEntityFields<T, TSelected>
67
+ declare readonly $data: TSelected | null
68
+ declare readonly $isDirty: boolean
69
+ declare readonly $persistedId: string | null
70
+ declare readonly $isNew: boolean
71
+ declare readonly $errors: readonly FieldError[]
72
+ declare readonly $hasError: boolean
73
+ declare $addError: (error: ErrorInput) => void
74
+ declare $clearErrors: () => void
75
+ declare $clearAllErrors: () => void
76
+ declare $on: <E extends AfterEventTypes>(eventType: E, listener: EventListener<EventTypeMap[E]>) => Unsubscribe
77
+ declare $intercept: <E extends BeforeEventTypes>(eventType: E, interceptor: Interceptor<EventTypeMap[E]>) => Unsubscribe
78
+ declare $onPersisted: (listener: EventListener<EntityPersistedEvent>) => Unsubscribe
79
+ declare $interceptPersisting: (interceptor: Interceptor<EntityPersistingEvent>) => Unsubscribe
80
+
81
+ private constructor(
82
+ id: string,
83
+ entityType: string,
84
+ store: SnapshotStore,
85
+ dispatcher: ActionDispatcher,
86
+ private readonly schema: SchemaRegistry,
87
+ brands?: Set<symbol>,
88
+ private readonly selection?: SelectionMeta,
89
+ ) {
90
+ super(entityType, id, store, dispatcher)
91
+ this.__brands = brands
92
+ }
93
+
94
+ static create<T extends object = object, TSelected = T>(
95
+ id: string,
96
+ entityType: string,
97
+ store: SnapshotStore,
98
+ dispatcher: ActionDispatcher,
99
+ schema: SchemaRegistry,
100
+ brands?: Set<symbol>,
101
+ selection?: SelectionMeta,
102
+ ): EntityHandle<T, TSelected> {
103
+ return createHandleProxy(new EntityHandle<T, TSelected>(id, entityType, store, dispatcher, schema, brands, selection), {
104
+ knownProperties: ENTITY_HANDLE_PROPERTIES,
105
+ getFields: (target) => target.fields,
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Gets the entity ID.
111
+ */
112
+ get id(): string {
113
+ return this.entityId
114
+ }
115
+
116
+ /**
117
+ * Gets the entity type.
118
+ */
119
+ get type(): string {
120
+ return this.entityType
121
+ }
122
+
123
+ /**
124
+ * Gets the current entity snapshot.
125
+ */
126
+ getSnapshot(): EntitySnapshot<T> | undefined {
127
+ return this.store.getEntitySnapshot<T>(this.entityType, this.entityId)
128
+ }
129
+
130
+ /**
131
+ * Gets the current entity data.
132
+ * Returns selected fields subset as specified by TSelected type parameter.
133
+ */
134
+ get data(): TSelected | null {
135
+ const snapshot = this.getSnapshot()
136
+ return (snapshot?.data ?? null) as TSelected | null
137
+ }
138
+
139
+ /**
140
+ * Gets the server data.
141
+ */
142
+ get serverData(): T | undefined {
143
+ const snapshot = this.getSnapshot()
144
+ return snapshot?.serverData
145
+ }
146
+
147
+ /**
148
+ * Checks if the entity has been loaded.
149
+ */
150
+ get isLoaded(): boolean {
151
+ return this.getSnapshot() !== undefined
152
+ }
153
+
154
+ /**
155
+ * Checks if the entity is currently loading.
156
+ */
157
+ get isLoading(): boolean {
158
+ const loadState = this.store.getLoadState(this.entityType, this.entityId)
159
+ return loadState?.status === 'loading'
160
+ }
161
+
162
+ /**
163
+ * Checks if there was an error loading the entity.
164
+ */
165
+ get isError(): boolean {
166
+ const loadState = this.store.getLoadState(this.entityType, this.entityId)
167
+ return loadState?.status === 'error'
168
+ }
169
+
170
+ /**
171
+ * Gets the loading error if any.
172
+ */
173
+ get error(): FieldError | undefined {
174
+ const loadState = this.store.getLoadState(this.entityType, this.entityId)
175
+ return loadState?.error
176
+ }
177
+
178
+ /**
179
+ * Checks if the entity is currently being persisted.
180
+ */
181
+ get isPersisting(): boolean {
182
+ return this.store.isPersisting(this.entityType, this.entityId)
183
+ }
184
+
185
+ /**
186
+ * Checks if any field on the entity has been modified.
187
+ * This includes scalar field changes and relation changes (hasOne, hasMany).
188
+ */
189
+ get isDirty(): boolean {
190
+ const snapshot = this.getSnapshot()
191
+ if (!snapshot) return false
192
+
193
+ // Check scalar field changes
194
+ if (!deepEqual(snapshot.data, snapshot.serverData)) {
195
+ return true
196
+ }
197
+
198
+ // Check if any relation is dirty
199
+ const relationFields = this.schema.getRelationFields(this.entityType)
200
+ for (const fieldName of relationFields) {
201
+ const relationType = this.schema.getRelationType(this.entityType, fieldName)
202
+
203
+ if (relationType === 'hasOne') {
204
+ const handle = this.hasOne(fieldName)
205
+ // Check if the relation itself is dirty (connect/disconnect)
206
+ if (handle.$isDirty) {
207
+ return true
208
+ }
209
+ // Check if the target entity has dirty fields
210
+ if (handle.$entity.$isDirty) {
211
+ return true
212
+ }
213
+ } else if (relationType === 'hasMany') {
214
+ const handle = this.hasMany(fieldName)
215
+ if (handle.isDirty) {
216
+ return true
217
+ }
218
+ }
219
+ }
220
+
221
+ return false
222
+ }
223
+
224
+ /**
225
+ * Gets the persisted ID (server-assigned ID after create).
226
+ * Returns the ID itself if it's already a real ID, null if it's a temp ID without mapping.
227
+ */
228
+ get persistedId(): string | null {
229
+ return this.store.getPersistedId(this.entityType, this.entityId)
230
+ }
231
+
232
+ /**
233
+ * Checks if this entity is new (created locally, not yet persisted to server).
234
+ */
235
+ get isNew(): boolean {
236
+ return this.store.isNewEntity(this.entityType, this.entityId)
237
+ }
238
+
239
+ /**
240
+ * Gets a field handle for a specific field.
241
+ * Returns cached handle to ensure stable identity.
242
+ */
243
+ field<K extends keyof T>(fieldName: K): FieldHandle<T[K]> {
244
+ const cacheKey = String(fieldName)
245
+ const cached = this.fieldHandleCache.get(cacheKey)
246
+
247
+ if (cached) {
248
+ return cached as FieldHandle<T[K]>
249
+ }
250
+
251
+ const enumName = this.schema.getEnumName(this.entityType, cacheKey)
252
+ const columnType = this.schema.getColumnType(this.entityType, cacheKey)
253
+ const handle = FieldHandle.create<T[K]>(
254
+ this.entityType,
255
+ this.entityId,
256
+ [cacheKey],
257
+ this.store,
258
+ this.dispatcher,
259
+ enumName,
260
+ columnType,
261
+ )
262
+ this.fieldHandleCache.set(cacheKey, handle as FieldHandle<unknown>)
263
+
264
+ return handle
265
+ }
266
+
267
+ /**
268
+ * Gets a has-one relation handle.
269
+ */
270
+ hasOne<TRelated extends object>(fieldName: string, nestedSelection?: SelectionMeta): HasOneHandle<TRelated> {
271
+ const cacheKey = `hasOne:${fieldName}`
272
+ const cached = this.relationHandleCache.get(cacheKey)
273
+
274
+ if (cached) {
275
+ return cached as HasOneHandle<TRelated>
276
+ }
277
+
278
+ const targetType = this.schema.getRelationTarget(this.entityType, fieldName)
279
+ if (!targetType) {
280
+ throw new Error(
281
+ `Field "${fieldName}" is not a relation on entity "${this.entityType}"`,
282
+ )
283
+ }
284
+
285
+ const handle = HasOneHandle.create<TRelated>(
286
+ this.entityType,
287
+ this.entityId,
288
+ fieldName,
289
+ targetType,
290
+ this.store,
291
+ this.dispatcher,
292
+ this.schema,
293
+ undefined,
294
+ nestedSelection,
295
+ )
296
+ this.relationHandleCache.set(cacheKey, handle as RelationHandle)
297
+
298
+ return handle
299
+ }
300
+
301
+ /**
302
+ * Gets a has-many relation handle.
303
+ *
304
+ * @param alias - Optional alias for the relation. When the same field is used multiple times
305
+ * with different parameters (filter, orderBy, limit), each needs a unique alias.
306
+ */
307
+ hasMany<TItem extends object>(fieldName: string, alias?: string, nestedSelection?: SelectionMeta): HasManyListHandle<TItem> {
308
+ const effectiveAlias = alias ?? fieldName
309
+ const cacheKey = `hasMany:${effectiveAlias}`
310
+ const cached = this.relationHandleCache.get(cacheKey)
311
+
312
+ if (cached) {
313
+ return cached as HasManyListHandle<TItem>
314
+ }
315
+
316
+ const targetType = this.schema.getRelationTarget(this.entityType, fieldName)
317
+ if (!targetType) {
318
+ throw new Error(
319
+ `Field "${fieldName}" is not a relation on entity "${this.entityType}"`,
320
+ )
321
+ }
322
+
323
+ const handle = HasManyListHandle.create<TItem>(
324
+ this.entityType,
325
+ this.entityId,
326
+ fieldName,
327
+ targetType,
328
+ this.store,
329
+ this.dispatcher,
330
+ this.schema,
331
+ this.__brands,
332
+ effectiveAlias,
333
+ nestedSelection,
334
+ )
335
+ this.relationHandleCache.set(cacheKey, handle as RelationHandle)
336
+
337
+ return handle
338
+ }
339
+
340
+ /**
341
+ * Resets the entity to server data.
342
+ * Also resets all relation states (hasOne, hasMany).
343
+ */
344
+ reset(): void {
345
+ this.assertNotDisposed()
346
+ this.dispatcher.dispatch(resetEntity(this.entityType, this.entityId))
347
+
348
+ // Reset all cached relation handles
349
+ for (const handle of this.relationHandleCache.values()) {
350
+ handle.reset()
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Commits changes (serverData = data).
356
+ */
357
+ commit(): void {
358
+ this.assertNotDisposed()
359
+ this.dispatcher.dispatch(commitEntity(this.entityType, this.entityId))
360
+ }
361
+
362
+ /**
363
+ * Disposes the handle and all cached child handles.
364
+ */
365
+ override dispose(): void {
366
+ super.dispose()
367
+
368
+ for (const handle of this.fieldHandleCache.values()) {
369
+ handle.dispose()
370
+ }
371
+ this.fieldHandleCache.clear()
372
+
373
+ for (const handle of this.relationHandleCache.values()) {
374
+ handle.dispose()
375
+ }
376
+ this.relationHandleCache.clear()
377
+ }
378
+
379
+ /**
380
+ * Creates a proxy for field access.
381
+ * Returns appropriate handle type based on schema field definition:
382
+ * - Scalar fields -> FieldHandle
383
+ * - HasOne relations -> HasOneHandle
384
+ * - HasMany relations -> HasManyListHandle
385
+ *
386
+ * Implements EntityRef.fields - returns selection-aware field accessors.
387
+ */
388
+ get fields(): SelectedEntityFields<T, TSelected> {
389
+ return new Proxy({} as SelectedEntityFields<T, TSelected>, {
390
+ get: (_, fieldName: string) => {
391
+ // Selection validation
392
+ if (this.selection && !this.selection.fields.has(fieldName)) {
393
+ throw new UnfetchedFieldError(this.entityType, this.entityId, [fieldName])
394
+ }
395
+
396
+ const nestedSelection = this.selection?.fields.get(fieldName)?.nested
397
+
398
+ // Use schema to determine field type
399
+ const fieldDef = this.schema.getFieldDef(this.entityType, fieldName)
400
+
401
+ if (!fieldDef || fieldDef.type === 'scalar') {
402
+ // Scalar field - return FieldHandle
403
+ return this.field(fieldName as keyof T)
404
+ }
405
+
406
+ if (fieldDef.type === 'hasOne') {
407
+ // Has-one relation - return HasOneHandle
408
+ return this.hasOne(fieldName, nestedSelection)
409
+ }
410
+
411
+ if (fieldDef.type === 'hasMany') {
412
+ // Has-many relation - return HasManyListHandle
413
+ return this.hasMany(fieldName, undefined, nestedSelection)
414
+ }
415
+
416
+ // Unknown field type - fallback to FieldHandle
417
+ return this.field(fieldName as keyof T)
418
+ },
419
+ })
420
+ }
421
+
422
+ /**
423
+ * Type brand - ensures EntityRef<Author> is not assignable to EntityRef<Tag>.
424
+ * This is a phantom property that only exists in the type system.
425
+ * Implements EntityRef.__entityType.
426
+ */
427
+ get __entityType(): T {
428
+ return undefined as unknown as T
429
+ }
430
+
431
+ /**
432
+ * Entity name for type inference.
433
+ * Carries the entity type name (e.g., 'Article', 'Author').
434
+ * Implements EntityRef.__entityName.
435
+ */
436
+ get __entityName(): string {
437
+ return this.entityType
438
+ }
439
+
440
+ /**
441
+ * Gets the list of entity-level errors (not including field or relation errors).
442
+ */
443
+ get errors(): readonly FieldError[] {
444
+ return this.store.getEntityErrors(this.entityType, this.entityId)
445
+ }
446
+
447
+ /**
448
+ * Checks if this entity has any errors (entity-level, fields, or relations).
449
+ */
450
+ get hasError(): boolean {
451
+ return this.store.hasAnyErrors(this.entityType, this.entityId)
452
+ }
453
+
454
+ /**
455
+ * Adds a client-side error to this entity.
456
+ */
457
+ addError(error: ErrorInput): void {
458
+ this.assertNotDisposed()
459
+ this.dispatcher.dispatch(
460
+ addEntityError(this.entityType, this.entityId, createClientError(error)),
461
+ )
462
+ }
463
+
464
+ /**
465
+ * Clears entity-level errors.
466
+ */
467
+ clearErrors(): void {
468
+ this.assertNotDisposed()
469
+ this.dispatcher.dispatch(
470
+ clearEntityErrors(this.entityType, this.entityId),
471
+ )
472
+ }
473
+
474
+ /**
475
+ * Clears all errors (entity-level, fields, and relations).
476
+ */
477
+ clearAllErrors(): void {
478
+ this.assertNotDisposed()
479
+ this.dispatcher.dispatch(
480
+ clearAllErrorsAction(this.entityType, this.entityId),
481
+ )
482
+ }
483
+
484
+ // ==================== Dirty Tracking ====================
485
+
486
+ /**
487
+ * Gets the list of dirty scalar fields.
488
+ */
489
+ getDirtyFields(): readonly string[] {
490
+ return this.store.getDirtyFields(this.entityType, this.entityId)
491
+ }
492
+
493
+ /**
494
+ * Gets the list of dirty relations.
495
+ */
496
+ getDirtyRelations(): readonly string[] {
497
+ return this.store.getDirtyRelations(this.entityType, this.entityId)
498
+ }
499
+
500
+ // ==================== Event Subscriptions ====================
501
+
502
+ /**
503
+ * Subscribe to any event on this entity.
504
+ */
505
+ on<E extends AfterEventTypes>(
506
+ eventType: E,
507
+ listener: EventListener<EventTypeMap[E]>,
508
+ ): Unsubscribe {
509
+ const emitter = this.dispatcher.getEventEmitter()
510
+ return emitter.onEntity(eventType, this.entityType, this.entityId, listener)
511
+ }
512
+
513
+ /**
514
+ * Intercept any before event on this entity.
515
+ */
516
+ intercept<E extends BeforeEventTypes>(
517
+ eventType: E,
518
+ interceptor: Interceptor<EventTypeMap[E]>,
519
+ ): Unsubscribe {
520
+ const emitter = this.dispatcher.getEventEmitter()
521
+ return emitter.interceptEntity(eventType, this.entityType, this.entityId, interceptor)
522
+ }
523
+
524
+ /**
525
+ * Subscribe to persist success events.
526
+ */
527
+ onPersisted(listener: EventListener<EntityPersistedEvent>): Unsubscribe {
528
+ return this.on('entity:persisted', listener)
529
+ }
530
+
531
+ /**
532
+ * Intercept persist (can cancel).
533
+ */
534
+ interceptPersisting(interceptor: Interceptor<EntityPersistingEvent>): Unsubscribe {
535
+ return this.intercept('entity:persisting', interceptor)
536
+ }
537
+
538
+ }