@contember/bindx 0.1.14 → 0.1.15

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.
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Unified type definitions for the Handle system.
3
3
  *
4
- * This module provides type-safe field accessor mapping that correctly
5
- * distinguishes between scalar fields, hasOne relations, and hasMany relations.
4
+ * Type hierarchy: Ref (pointer, no data access) → Accessor (live data, extends Ref).
6
5
  *
7
- * Type hierarchy uses inheritance for implicit/explicit selection modes:
8
- * - Base types (e.g., FieldRefBase) - work everywhere, no direct value access
9
- * - Full types (e.g., FieldRef) extend Base - add .value, .length, etc.
10
- *
11
- * Components accept Base types, so they work with both implicit and explicit selection.
6
+ * - FieldRef / FieldAccessor
7
+ * - HasManyRef / HasManyAccessor
8
+ * - HasOneRef / HasOneAccessor
9
+ * - EntityRef / EntityAccessor
12
10
  */
13
11
 
14
12
  import type { FieldHandle } from './FieldHandle.js'
@@ -17,14 +15,11 @@ import type { FieldHandle } from './FieldHandle.js'
17
15
  // Relation State Types
18
16
  // ============================================================================
19
17
 
20
- /**
21
- * State of a has-one relation.
22
- */
23
18
  export type HasOneRelationState =
24
- | 'connected' // Points to an existing entity
25
- | 'disconnected' // Explicitly set to null
26
- | 'deleted' // Related entity marked for deletion
27
- | 'creating' // Placeholder entity being filled for implicit create
19
+ | 'connected'
20
+ | 'disconnected'
21
+ | 'deleted'
22
+ | 'creating'
28
23
 
29
24
  import type { ComponentBrand, AnyBrand } from '../brand/ComponentBrand.js'
30
25
  import type { FieldError, ErrorInput } from '../errors/types.js'
@@ -49,17 +44,12 @@ import type {
49
44
  Unsubscribe as UnsubscribeType,
50
45
  } from '../events/types.js'
51
46
 
52
- // Re-export Unsubscribe for convenience
53
47
  export type { UnsubscribeType as Unsubscribe }
54
48
 
55
49
  // ============================================================================
56
50
  // Field Type Detection Utilities
57
51
  // ============================================================================
58
52
 
59
- /**
60
- * Extracts scalar field keys from a type.
61
- * A field is scalar if it's not an array and not an object (except 'id' which is always scalar).
62
- */
63
53
  export type ScalarKeys<T> = {
64
54
  [K in keyof T]: T[K] extends (infer _U)[]
65
55
  ? never
@@ -70,10 +60,6 @@ export type ScalarKeys<T> = {
70
60
  : K
71
61
  }[keyof T]
72
62
 
73
- /**
74
- * Extracts has-many relation keys from a type.
75
- * A field is has-many if it's an array of objects.
76
- */
77
63
  export type HasManyKeys<T> = {
78
64
  [K in keyof T]: T[K] extends (infer U)[]
79
65
  ? U extends object
@@ -82,10 +68,6 @@ export type HasManyKeys<T> = {
82
68
  : never
83
69
  }[keyof T]
84
70
 
85
- /**
86
- * Extracts has-one relation keys from a type.
87
- * A field is has-one if it's an object (but not 'id' and not an array).
88
- */
89
71
  export type HasOneKeys<T> = {
90
72
  [K in keyof T]: T[K] extends (infer _U)[]
91
73
  ? never
@@ -100,19 +82,12 @@ export type HasOneKeys<T> = {
100
82
  // Symbols
101
83
  // ============================================================================
102
84
 
103
- /**
104
- * Marker symbol for field reference metadata
105
- */
106
85
  export const FIELD_REF_META = Symbol('FIELD_REF_META')
107
86
 
108
87
  // ============================================================================
109
88
  // Helper Types
110
89
  // ============================================================================
111
90
 
112
- /**
113
- * Base metadata interface for all field references.
114
- * Used by JSX components for selection collection.
115
- */
116
91
  export interface FieldRefMeta<TEntityName extends string = string> {
117
92
  readonly entityType: TEntityName
118
93
  readonly entityId: string
@@ -120,32 +95,20 @@ export interface FieldRefMeta<TEntityName extends string = string> {
120
95
  readonly fieldName: string
121
96
  readonly isArray: boolean
122
97
  readonly isRelation: boolean
123
- /** For enum fields: the enum type name (e.g. 'DeploymentStatus') */
124
98
  readonly enumName?: string
125
- /** For scalar fields: the column type (e.g. 'String', 'Integer', 'Date') */
126
99
  readonly columnType?: string
127
- /** For relation fields: the target entity type name */
128
100
  readonly targetType?: string
129
101
  }
130
102
 
131
- /**
132
- * Input props interface for form binding.
133
- */
134
103
  export interface InputProps<T> {
135
104
  readonly value: T | null
136
105
  readonly setValue: (value: T | null) => void
137
106
  readonly onChange: (value: T | null) => void
138
107
  }
139
108
 
140
- /**
141
- * Extracts the nested selection type for a relation field.
142
- */
143
109
  type ExtractNestedSelection<TSelected, K extends PropertyKey> =
144
110
  K extends keyof TSelected ? TSelected[K] : never
145
111
 
146
- /**
147
- * Reverse lookup: finds entity name (schema key) from entity type.
148
- */
149
112
  export type EntityNameFromType<TSchema extends Record<string, object>, TEntityType> = {
150
113
  [K in keyof TSchema]: TSchema[K] extends TEntityType ? K : never
151
114
  }[keyof TSchema] & string
@@ -155,78 +118,29 @@ export type EntityNameFromType<TSchema extends Record<string, object>, TEntityTy
155
118
  // ============================================================================
156
119
 
157
120
  /**
158
- * Base interface for scalar field references.
159
- * Works everywhere, can be passed to components.
160
- * Does NOT include .value, .serverValue, .isDirty - use FieldRef for those.
161
- *
162
- * @example
163
- * ```tsx
164
- * // In implicit mode, task.title is FieldRefBase<string>
165
- * // Can be passed to <Field>:
166
- * <Field field={task.title} />
167
- *
168
- * // Can use mutations:
169
- * task.title.setValue('new value')
170
- *
171
- * // Cannot read value directly (use FieldRef for that):
172
- * // task.title.value // ❌ type error
173
- * ```
121
+ * Pointer to a scalar field. No .value access.
122
+ * Safe to pass to components, returned by createComponent() implicit mode.
174
123
  */
175
- export interface FieldRefBase<T> {
176
- /** Internal metadata for collection phase */
124
+ export interface FieldRef<T> {
177
125
  readonly [FIELD_REF_META]: FieldRefMeta
178
-
179
- /** Whether field has been touched (interacted with by user) */
180
126
  readonly isTouched: boolean
181
-
182
- /** Mark the field as touched */
183
127
  touch(): void
184
-
185
- /** Update the value */
186
128
  setValue(value: T | null): void
187
-
188
- /** Input binding props */
189
129
  readonly inputProps: InputProps<T>
190
-
191
- /** List of errors on this field */
192
130
  readonly errors: readonly FieldError[]
193
-
194
- /** Whether this field has any errors */
195
131
  readonly hasError: boolean
196
-
197
- /** Add a client-side error to this field */
198
132
  addError(error: ErrorInput): void
199
-
200
- /** Clear all errors from this field */
201
133
  clearErrors(): void
202
-
203
- /** Subscribe to field value changes */
204
134
  onChange(listener: EventListener<FieldChangedEvent>): UnsubscribeType
205
-
206
- /** Intercept field value changes (can cancel or modify) */
207
135
  onChanging(interceptor: Interceptor<FieldChangingEvent>): UnsubscribeType
208
136
  }
209
137
 
210
138
  /**
211
- * Full interface for scalar field references.
212
- * Extends FieldRefBase with value access properties.
213
- *
214
- * @example
215
- * ```tsx
216
- * // In explicit mode, task.title is FieldRef<string>
217
- * // Full access including value:
218
- * const title = task.title.value
219
- * const isDirty = task.title.isDirty
220
- * ```
139
+ * Live scalar field accessor with value access. Created by hooks (useField).
221
140
  */
222
- export interface FieldRef<T> extends FieldRefBase<T> {
223
- /** Current value (null in collection phase, real value in runtime) */
141
+ export interface FieldAccessor<T> extends FieldRef<T> {
224
142
  readonly value: T | null
225
-
226
- /** Server value for dirty tracking */
227
143
  readonly serverValue: T | null
228
-
229
- /** Whether value differs from server */
230
144
  readonly isDirty: boolean
231
145
  }
232
146
 
@@ -235,116 +149,53 @@ export interface FieldRef<T> extends FieldRefBase<T> {
235
149
  // ============================================================================
236
150
 
237
151
  /**
238
- * Base interface for has-many relation references.
239
- * Works everywhere, can be passed to components.
240
- * Does NOT include .length, .items, .map - use HasManyRef for those.
241
- *
242
- * @example
243
- * ```tsx
244
- * // In implicit mode, task.developers is HasManyRefBase
245
- * // Can be passed to <HasMany>:
246
- * <HasMany field={task.developers}>{dev => ...}</HasMany>
247
- *
248
- * // Can use mutations:
249
- * task.developers.add()
250
- * task.developers.remove(id)
251
- *
252
- * // Cannot read length directly (use HasManyRef for that):
253
- * // task.developers.length // ❌ type error
254
- * ```
152
+ * Pointer to a has-many relation. No .items/.length/.map access.
153
+ * Safe to pass to components.
255
154
  */
256
- export interface HasManyRefBase<
155
+ export interface HasManyRef<
257
156
  TEntity,
258
157
  TSelected = TEntity,
259
158
  TBrand extends AnyBrand = AnyBrand,
260
159
  TEntityName extends string = string,
261
160
  TSchema extends Record<string, object> = Record<string, object>,
262
161
  > {
263
- /** Internal metadata for collection phase */
264
162
  readonly [FIELD_REF_META]: FieldRefMeta<TEntityName>
265
-
266
- /** Whether any item has been modified */
267
163
  readonly isDirty: boolean
268
-
269
- /** Add a new item - returns the new entity's ID (temp ID) */
270
164
  add(data?: Partial<TEntity>): string
271
-
272
- /** Remove item by ID */
273
165
  remove(itemId: string): void
274
-
275
- /** Move item from one position to another */
276
166
  move(fromIndex: number, toIndex: number): void
277
-
278
- /** Connect an existing entity to this has-many relation */
279
167
  connect(itemId: string): void
280
-
281
- /** Disconnect an entity from this has-many relation */
282
168
  disconnect(itemId: string | null): void
283
-
284
- /** Reset the relation to server state */
285
169
  reset(): void
286
-
287
- /** Type brand - ensures HasManyRef<Author> is not assignable to HasManyRef<Tag> */
288
170
  readonly __entityType: TEntity
289
-
290
- /** Type brand for entity name */
171
+ readonly __selected?: TSelected
291
172
  readonly __entityName: TEntityName
292
-
293
- /** Type brand for schema */
294
173
  readonly __schema?: TSchema & any
295
-
296
- /** Runtime brand symbols for validation */
297
174
  readonly __brands?: Set<symbol>
298
-
299
- /** List of errors on this relation */
300
175
  readonly errors: readonly FieldError[]
301
-
302
- /** Whether this relation has any errors */
303
176
  readonly hasError: boolean
304
-
305
- /** Add a client-side error to this relation */
306
177
  addError(error: ErrorInput): void
307
-
308
- /** Clear all errors from this relation */
309
178
  clearErrors(): void
310
-
311
- /** Subscribe to item connected events */
312
179
  onItemConnected(listener: EventListener<HasManyConnectedEvent>): UnsubscribeType
313
-
314
- /** Subscribe to item disconnected events */
315
180
  onItemDisconnected(listener: EventListener<HasManyDisconnectedEvent>): UnsubscribeType
316
-
317
- /** Intercept item connection (can cancel) */
318
181
  interceptItemConnecting(interceptor: Interceptor<HasManyConnectingEvent>): UnsubscribeType
319
-
320
- /** Intercept item disconnection (can cancel) */
321
182
  interceptItemDisconnecting(interceptor: Interceptor<HasManyDisconnectingEvent>): UnsubscribeType
322
183
  }
323
184
 
324
185
  /**
325
- * Full interface for has-many relation references.
326
- * Extends HasManyRefBase with collection access properties.
186
+ * Live has-many accessor with collection access. Created by hooks (useHasMany).
327
187
  */
328
- export interface HasManyRef<
188
+ export interface HasManyAccessor<
329
189
  TEntity,
330
190
  TSelected = TEntity,
331
191
  TBrand extends AnyBrand = AnyBrand,
332
192
  TEntityName extends string = string,
333
193
  TSchema extends Record<string, object> = Record<string, object>,
334
- > extends HasManyRefBase<TEntity, TSelected, TBrand, TEntityName, TSchema> {
335
- /** Number of items */
194
+ > extends HasManyRef<TEntity, TSelected, TBrand, TEntityName, TSchema> {
336
195
  readonly length: number
337
-
338
- /** Total count from paginateRelation (undefined if not available) */
339
196
  readonly totalCount: number | undefined
340
-
341
- /** Direct access to items array */
342
197
  readonly items: EntityAccessor<TEntity, TSelected, TBrand, TEntityName, TSchema>[]
343
-
344
- /** Iterate over items */
345
198
  map<R>(fn: (item: EntityAccessor<TEntity, TSelected, TBrand, TEntityName, TSchema>, index: number) => R): R[]
346
-
347
- /** Get an item handle by entity ID */
348
199
  getById(id: string): EntityAccessor<TEntity, TSelected, TBrand, TEntityName, TSchema>
349
200
  }
350
201
 
@@ -353,163 +204,62 @@ export interface HasManyRef<
353
204
  // ============================================================================
354
205
 
355
206
  /**
356
- * Base interface for has-one relation references.
357
- * Works everywhere, can be passed to components.
358
- * Does NOT include .$state, .$entity, .$fields, .$data - use HasOneRef for those.
359
- * DOES include direct field access (e.g., task.project.name).
360
- *
361
- * @example
362
- * ```tsx
363
- * // In implicit mode, task.project is HasOneRefBase (with direct field access)
364
- * // Can be passed to <HasOne>:
365
- * <HasOne field={task.project}>{project => ...}</HasOne>
366
- *
367
- * // Can access nested fields (they are also restricted):
368
- * task.project.name // FieldRefBase<string>
369
- *
370
- * // Can use mutations:
371
- * task.project.$connect('project-1')
372
- *
373
- * // Cannot read state directly (use HasOneRef for that):
374
- * // task.project.$state // ❌ type error
375
- * ```
207
+ * Pointer to a has-one relation with proxy field access (fields return Ref variants).
208
+ * Includes $connect/$disconnect/$delete but no $state/$data/$fields/$entity.
376
209
  */
377
- export interface HasOneRefBase<
210
+ export interface HasOneRefInterface<
378
211
  TEntity,
379
212
  TSelected = TEntity,
380
213
  TBrand extends AnyBrand = AnyBrand,
381
214
  TEntityName extends string = string,
382
215
  TSchema extends Record<string, object> = Record<string, object>,
383
216
  > {
384
- /** Internal metadata for collection phase */
385
217
  readonly [FIELD_REF_META]: FieldRefMeta<TEntityName>
386
-
387
- /** Entity ID (placeholder ID if disconnected) */
388
218
  readonly id: string
389
-
390
- /** Entity ID (alias with $ prefix) */
391
219
  readonly $id: string
392
-
393
- /** Whether relation is dirty */
394
220
  readonly $isDirty: boolean
395
-
396
- /** Whether this entity is new */
397
221
  readonly $isNew: boolean
398
-
399
- /** Whether entity is currently being persisted */
400
222
  readonly $isPersisting: boolean
401
-
402
- /** Server-assigned ID after persistence */
403
223
  readonly $persistedId: string | null
404
-
405
- /** Type brand for entity name */
406
224
  readonly __entityName: TEntityName
407
-
408
- /** Type brand for schema */
409
225
  readonly __schema?: TSchema & any
410
-
411
- /** Type brand - ensures type safety */
412
226
  readonly __entityType: TEntity
413
-
414
- /** Runtime brand symbols for validation */
227
+ readonly __selected?: TSelected
415
228
  readonly __brands?: Set<symbol>
416
-
417
- /** Connect to existing entity */
418
229
  $connect(id: string): void
419
-
420
- /** Disconnect relation */
421
230
  $disconnect(): void
422
-
423
- /** Mark relation for deletion */
424
231
  $delete(): void
425
-
426
- /** Reset the relation to server state */
427
232
  $reset(): void
428
-
429
- /** List of errors on this relation */
430
233
  readonly $errors: readonly FieldError[]
431
-
432
- /** Whether this relation has any errors */
433
234
  readonly $hasError: boolean
434
-
435
- /** Add a client-side error to this relation */
436
235
  $addError(error: ErrorInput): void
437
-
438
- /** Clear all errors from this relation */
439
236
  $clearErrors(): void
440
-
441
- /** Clear all errors (entity-level, fields, and relations) */
442
237
  $clearAllErrors(): void
443
-
444
- /** Subscribe to connection events */
445
238
  $onConnect(listener: EventListener<RelationConnectedEvent>): UnsubscribeType
446
-
447
- /** Subscribe to disconnection events */
448
239
  $onDisconnect(listener: EventListener<RelationDisconnectedEvent>): UnsubscribeType
449
-
450
- /** Intercept connection (can cancel or modify target) */
451
240
  $interceptConnect(interceptor: Interceptor<RelationConnectingEvent>): UnsubscribeType
452
-
453
- /** Intercept disconnection (can cancel) */
454
241
  $interceptDisconnect(interceptor: Interceptor<RelationDisconnectingEvent>): UnsubscribeType
455
-
456
- /** Subscribe to any event on the related entity */
457
- $on<E extends AfterEventTypes>(
458
- eventType: E,
459
- listener: EventListener<EventTypeMap[E]>,
460
- ): UnsubscribeType
461
-
462
- /** Intercept any before event on the related entity */
463
- $intercept<E extends BeforeEventTypes>(
464
- eventType: E,
465
- interceptor: Interceptor<EventTypeMap[E]>,
466
- ): UnsubscribeType
467
-
468
- /** Subscribe to persist success events */
242
+ $on<E extends AfterEventTypes>(eventType: E, listener: EventListener<EventTypeMap[E]>): UnsubscribeType
243
+ $intercept<E extends BeforeEventTypes>(eventType: E, interceptor: Interceptor<EventTypeMap[E]>): UnsubscribeType
469
244
  $onPersisted(listener: EventListener<EntityPersistedEvent>): UnsubscribeType
470
-
471
- /** Intercept persist (can cancel) */
472
245
  $interceptPersisting(interceptor: Interceptor<EntityPersistingEvent>): UnsubscribeType
473
246
  }
474
247
 
475
248
  /**
476
- * Full interface for has-one relation references.
477
- * Extends HasOneRefBase with state and entity access properties.
249
+ * HasOneRef = interface props + proxy field access returning Ref variants.
478
250
  */
479
- export interface HasOneRef<
251
+ export type HasOneRef<
480
252
  TEntity,
481
253
  TSelected = TEntity,
482
254
  TBrand extends AnyBrand = AnyBrand,
483
255
  TEntityName extends string = string,
484
256
  TSchema extends Record<string, object> = Record<string, object>,
485
- > extends HasOneRefBase<TEntity, TSelected, TBrand, TEntityName, TSchema> {
486
- /** Raw data snapshot of the related entity */
487
- readonly $data: TSelected | null
488
-
489
- /** Relation state */
490
- readonly $state: HasOneRelationState
491
-
492
- /** Nested entity fields - only selected fields are accessible */
493
- readonly $fields: SelectedEntityFields<TEntity, TSelected, TSchema>
494
-
495
- /** Related entity accessor with direct field access */
496
- readonly $entity: EntityAccessor<TEntity, TSelected, TBrand, TEntityName, TSchema>
497
- }
257
+ > = HasOneRefInterface<TEntity, TSelected, TBrand, TEntityName, TSchema> &
258
+ EntityFieldsRef<TEntity, TSelected, TSchema>
498
259
 
499
260
  /**
500
- * HasOneRefBase with direct field access via Proxy (for implicit mode).
501
- */
502
- export type HasOneAccessorBase<
503
- TEntity,
504
- TSelected = TEntity,
505
- TBrand extends AnyBrand = AnyBrand,
506
- TEntityName extends string = string,
507
- TSchema extends Record<string, object> = Record<string, object>,
508
- > = HasOneRefBase<TEntity, TSelected, TBrand, TEntityName, TSchema> &
509
- SelectedEntityFieldsBase<TEntity, TSelected, TSchema>
510
-
511
- /**
512
- * HasOneRef with direct field access via Proxy (for explicit mode).
261
+ * HasOneAccessor = HasOneRef + $state/$data/$fields/$entity, proxy returns Accessor variants.
262
+ * Includes HasOneRef in definition so TypeScript can infer generic params.
513
263
  */
514
264
  export type HasOneAccessor<
515
265
  TEntity,
@@ -517,124 +267,66 @@ export type HasOneAccessor<
517
267
  TBrand extends AnyBrand = AnyBrand,
518
268
  TEntityName extends string = string,
519
269
  TSchema extends Record<string, object> = Record<string, object>,
520
- > = HasOneRef<TEntity, TSelected, TBrand, TEntityName, TSchema> &
521
- SelectedEntityFields<TEntity, TSelected, TSchema>
270
+ > = HasOneRef<TEntity, TSelected, TBrand, TEntityName, TSchema> & {
271
+ readonly $data: TSelected | null
272
+ readonly $state: HasOneRelationState
273
+ readonly $fields: EntityFieldsAccessor<TEntity, TSelected, TSchema>
274
+ readonly $entity: EntityAccessor<TEntity, TSelected, TBrand, TEntityName, TSchema>
275
+ } & EntityFieldsAccessor<TEntity, TSelected, TSchema>
522
276
 
523
277
  // ============================================================================
524
278
  // ENTITY TYPES
525
279
  // ============================================================================
526
280
 
527
281
  /**
528
- * Base interface for entity references.
529
- * Works everywhere, can be passed to components.
530
- * Does NOT include .$data, .$fields - use EntityRef for those.
282
+ * Entity ref interface props (without proxy field access).
531
283
  */
532
- export interface EntityRefBase<
284
+ export interface EntityRefInterface<
533
285
  TEntity,
534
286
  TSelected = TEntity,
535
287
  TBrand extends AnyBrand = AnyBrand,
536
288
  TEntityName extends string = string,
537
289
  TSchema extends Record<string, object> = Record<string, object>,
538
290
  > {
539
- /** Entity ID */
540
291
  readonly id: string
541
-
542
- /** Whether entity is dirty */
543
292
  readonly $isDirty: boolean
544
-
545
- /** Whether entity is currently being persisted */
546
293
  readonly $isPersisting: boolean
547
-
548
- /** Server-assigned ID after persistence */
549
294
  readonly $persistedId: string | null
550
-
551
- /** Whether this entity is new */
552
295
  readonly $isNew: boolean
553
-
554
- /** Type brand - ensures type safety */
555
296
  readonly __entityType: TEntity
556
-
557
- /** Type brand for schema */
297
+ readonly __selected?: TSelected
558
298
  readonly __schema?: TSchema & any
559
-
560
- /** Type brand for entity name */
561
299
  readonly __entityName: TEntityName
562
-
563
- /** Runtime brand symbols for validation */
564
300
  readonly __brands?: Set<symbol>
565
-
566
- /** List of entity-level errors */
567
301
  readonly $errors: readonly FieldError[]
568
-
569
- /** Whether this entity has any errors */
570
302
  readonly $hasError: boolean
571
-
572
- /** Add a client-side error to this entity */
573
303
  $addError(error: ErrorInput): void
574
-
575
- /** Clear entity-level errors */
576
304
  $clearErrors(): void
577
-
578
- /** Clear all errors (entity-level, fields, and relations) */
579
305
  $clearAllErrors(): void
580
-
581
- /** Subscribe to any event on this entity */
582
- $on<E extends AfterEventTypes>(
583
- eventType: E,
584
- listener: EventListener<EventTypeMap[E]>,
585
- ): UnsubscribeType
586
-
587
- /** Intercept any before event on this entity */
588
- $intercept<E extends BeforeEventTypes>(
589
- eventType: E,
590
- interceptor: Interceptor<EventTypeMap[E]>,
591
- ): UnsubscribeType
592
-
593
- /** Subscribe to persist success events */
306
+ $on<E extends AfterEventTypes>(eventType: E, listener: EventListener<EventTypeMap[E]>): UnsubscribeType
307
+ $intercept<E extends BeforeEventTypes>(eventType: E, interceptor: Interceptor<EventTypeMap[E]>): UnsubscribeType
594
308
  $onPersisted(listener: EventListener<EntityPersistedEvent>): UnsubscribeType
595
-
596
- /** Intercept persist (can cancel) */
597
309
  $interceptPersisting(interceptor: Interceptor<EntityPersistingEvent>): UnsubscribeType
598
310
  }
599
311
 
600
312
  /**
601
- * Full interface for entity references.
602
- * Extends EntityRefBase with data access properties.
313
+ * EntityRef = interface props + proxy field access returning Ref variants.
603
314
  */
604
- export interface EntityRef<
605
- TEntity,
606
- TSelected = TEntity,
607
- TBrand extends AnyBrand = AnyBrand,
608
- TEntityName extends string = string,
609
- TSchema extends Record<string, object> = Record<string, object>,
610
- > extends EntityRefBase<TEntity, TSelected, TBrand, TEntityName, TSchema> {
611
- /** Typed field accessors */
612
- readonly $fields: SelectedEntityFields<TEntity, TSelected, TSchema>
613
-
614
- /** Raw data snapshot */
615
- readonly $data: TSelected | null
616
- }
617
-
618
- /**
619
- * EntityRefBase with direct field access via Proxy (for implicit mode).
620
- * TRoleMap carries the role map for HasRole type inference.
621
- */
622
- export type EntityAccessorBase<
315
+ export type EntityRef<
623
316
  TEntity,
624
317
  TSelected = TEntity,
625
318
  TBrand extends AnyBrand = AnyBrand,
626
319
  TEntityName extends string = string,
627
320
  TSchema extends Record<string, object> = Record<string, object>,
628
321
  TRoleMap extends Record<string, object> = Record<string, object>,
629
- > = EntityRefBase<TEntity, TSelected, TBrand, TEntityName, TSchema> &
630
- SelectedEntityFieldsBase<TEntity, TSelected, TSchema> & {
631
- /** @internal phantom — role map for HasRole inference */
322
+ > = EntityRefInterface<TEntity, TSelected, TBrand, TEntityName, TSchema> &
323
+ EntityFieldsRef<TEntity, TSelected, TSchema> & {
632
324
  readonly __roleMap?: TRoleMap
633
325
  }
634
326
 
635
327
  /**
636
- * EntityRef with direct field access via Proxy (for explicit mode).
637
- * TRoleMap carries the role map for HasRole type inference.
328
+ * EntityAccessor = EntityRef + $data/$fields, proxy returns Accessor variants.
329
+ * Includes EntityRef in definition so TypeScript can infer generic params.
638
330
  */
639
331
  export type EntityAccessor<
640
332
  TEntity,
@@ -643,107 +335,109 @@ export type EntityAccessor<
643
335
  TEntityName extends string = string,
644
336
  TSchema extends Record<string, object> = Record<string, object>,
645
337
  TRoleMap extends Record<string, object> = Record<string, object>,
646
- > = EntityRef<TEntity, TSelected, TBrand, TEntityName, TSchema> &
647
- SelectedEntityFields<TEntity, TSelected, TSchema> & {
648
- /** @internal phantom — role map for HasRole inference */
649
- readonly __roleMap?: TRoleMap
650
- }
338
+ > = EntityRef<TEntity, TSelected, TBrand, TEntityName, TSchema, TRoleMap> & {
339
+ readonly $fields: EntityFieldsAccessor<TEntity, TSelected, TSchema>
340
+ readonly $data: TSelected | null
341
+ } & EntityFieldsAccessor<TEntity, TSelected, TSchema>
651
342
 
652
343
  // ============================================================================
653
344
  // ENTITY FIELDS MAPPING TYPES
654
345
  // ============================================================================
655
346
 
656
347
  /**
657
- * Maps entity fields to their correct Handle types based on structural typing.
658
- * Uses full types (FieldHandle, HasManyRef, HasOneRef) for backwards compatibility.
348
+ * Maps entity fields to their Handle types (internal use).
659
349
  */
660
350
  export type EntityFields<T> = {
661
351
  [K in ScalarKeys<T>]: FieldHandle<T[K]>
662
352
  } & {
663
353
  [K in HasManyKeys<T>]: T[K] extends (infer U)[]
664
354
  ? U extends object
665
- ? HasManyRef<U>
355
+ ? HasManyAccessor<U>
666
356
  : never
667
357
  : never
668
358
  } & {
669
- [K in HasOneKeys<T>]: HasOneRef<NonNullable<T[K]>>
359
+ [K in HasOneKeys<T>]: HasOneAccessor<NonNullable<T[K]>>
670
360
  }
671
361
 
672
362
  /**
673
- * Maps entity fields to Base types (restricted, for implicit mode).
674
- * These types can be passed to components but don't allow direct value access.
363
+ * Resolves a single field to its Ref type based on field kind.
675
364
  */
676
- export type SelectedEntityFieldsBase<
677
- TEntity,
678
- TSelected,
679
- TSchema extends Record<string, object> = Record<string, object>,
680
- > = {
681
- [K in ScalarKeys<TEntity> & keyof TSelected]: FieldRefBase<TEntity[K]>
682
- } & {
683
- [K in HasManyKeys<TEntity> & keyof TSelected]: TEntity[K] extends (infer U)[]
365
+ type FieldRefType<TEntity, TSelected, TSchema extends Record<string, object>, K extends keyof TEntity & keyof TSelected> =
366
+ K extends ScalarKeys<TEntity> ? FieldRef<TEntity[K]> :
367
+ K extends HasManyKeys<TEntity> ? (TEntity[K] extends (infer U)[]
684
368
  ? U extends object
685
- ? HasManyRefBase<U, ExtractNestedSelection<TSelected, K> extends (infer S)[] ? S : U, AnyBrand, EntityNameFromType<TSchema, U>, TSchema>
369
+ ? HasManyRef<U, ExtractNestedSelection<TSelected, K> extends (infer S)[] ? S : U, AnyBrand, EntityNameFromType<TSchema, U>, TSchema>
686
370
  : never
687
- : never
688
- } & {
689
- [K in HasOneKeys<TEntity> & keyof TSelected]: HasOneAccessorBase<
371
+ : never) :
372
+ K extends HasOneKeys<TEntity> ? HasOneRef<
690
373
  NonNullable<TEntity[K]>,
691
374
  ExtractNestedSelection<TSelected, K> extends object ? ExtractNestedSelection<TSelected, K> : NonNullable<TEntity[K]>,
692
375
  AnyBrand,
693
376
  EntityNameFromType<TSchema, NonNullable<TEntity[K]>>,
694
377
  TSchema
695
- >
696
- }
378
+ > :
379
+ never
697
380
 
698
381
  /**
699
- * Maps entity fields to Full types (for explicit mode).
700
- * These types allow direct value access.
382
+ * Resolves a single field to its Accessor type based on field kind.
701
383
  */
702
- export type SelectedEntityFields<
703
- TEntity,
704
- TSelected,
705
- TSchema extends Record<string, object> = Record<string, object>,
706
- > = {
707
- [K in ScalarKeys<TEntity> & keyof TSelected]: FieldRef<TEntity[K]>
708
- } & {
709
- [K in HasManyKeys<TEntity> & keyof TSelected]: TEntity[K] extends (infer U)[]
384
+ type FieldAccessorType<TEntity, TSelected, TSchema extends Record<string, object>, K extends keyof TEntity & keyof TSelected> =
385
+ K extends ScalarKeys<TEntity> ? FieldAccessor<TEntity[K]> :
386
+ K extends HasManyKeys<TEntity> ? (TEntity[K] extends (infer U)[]
710
387
  ? U extends object
711
- ? HasManyRef<U, ExtractNestedSelection<TSelected, K> extends (infer S)[] ? S : U, AnyBrand, EntityNameFromType<TSchema, U>, TSchema>
388
+ ? HasManyAccessor<U, ExtractNestedSelection<TSelected, K> extends (infer S)[] ? S : U, AnyBrand, EntityNameFromType<TSchema, U>, TSchema>
712
389
  : never
713
- : never
714
- } & {
715
- [K in HasOneKeys<TEntity> & keyof TSelected]: HasOneAccessor<
390
+ : never) :
391
+ K extends HasOneKeys<TEntity> ? HasOneAccessor<
716
392
  NonNullable<TEntity[K]>,
717
393
  ExtractNestedSelection<TSelected, K> extends object ? ExtractNestedSelection<TSelected, K> : NonNullable<TEntity[K]>,
718
394
  AnyBrand,
719
395
  EntityNameFromType<TSchema, NonNullable<TEntity[K]>>,
720
396
  TSchema
721
- >
397
+ > :
398
+ never
399
+
400
+ /**
401
+ * Maps selected entity fields to Ref variants (for implicit mode / createComponent).
402
+ */
403
+ export type EntityFieldsRef<
404
+ TEntity,
405
+ TSelected,
406
+ TSchema extends Record<string, object> = Record<string, object>,
407
+ > = {
408
+ [K in (ScalarKeys<TEntity> | HasManyKeys<TEntity> | HasOneKeys<TEntity>) & keyof TSelected]: FieldRefType<TEntity, TSelected, TSchema, K & keyof TEntity>
409
+ }
410
+
411
+ /**
412
+ * Maps selected entity fields to Accessor variants (for explicit mode / hooks).
413
+ */
414
+ export type EntityFieldsAccessor<
415
+ TEntity,
416
+ TSelected,
417
+ TSchema extends Record<string, object> = Record<string, object>,
418
+ > = {
419
+ [K in (ScalarKeys<TEntity> | HasManyKeys<TEntity> | HasOneKeys<TEntity>) & keyof TSelected]: FieldAccessorType<TEntity, TSelected, TSchema, K & keyof TEntity>
722
420
  }
723
421
 
724
422
  // ============================================================================
725
423
  // Type Extraction Helpers
726
424
  // ============================================================================
727
425
 
728
- /**
729
- * Extracts the role map from an EntityAccessor or EntityAccessorBase.
730
- */
731
426
  export type ExtractRoleMap<T> =
732
427
  T extends { readonly __roleMap?: infer TRoleMap extends Record<string, object> }
733
428
  ? TRoleMap
734
429
  : Record<string, object>
735
430
 
736
431
  export type ExtractHasOneEntityName<T> =
737
- T extends HasOneRef<any, any, any, infer TEntityName, any>
432
+ T extends HasOneAccessor<any, any, any, infer TEntityName, any>
738
433
  ? TEntityName
739
- : T extends HasOneRefBase<any, any, any, infer TEntityName, any>
434
+ : T extends HasOneRef<any, any, any, infer TEntityName, any>
740
435
  ? TEntityName
741
436
  : never
742
437
 
743
438
  export type ExtractHasManyEntityName<T> =
744
- T extends HasManyRef<any, any, any, infer TEntityName, any>
439
+ T extends HasManyAccessor<any, any, any, infer TEntityName, any>
745
440
  ? TEntityName
746
- : T extends HasManyRefBase<any, any, any, infer TEntityName, any>
441
+ : T extends HasManyRef<any, any, any, infer TEntityName, any>
747
442
  ? TEntityName
748
443
  : never
749
-