@contember/bindx 0.1.5 → 0.1.8

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 (76) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +11 -14
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -1
  3. package/dist/brand/ComponentBrand.d.ts +1 -24
  4. package/dist/brand/ComponentBrand.d.ts.map +1 -1
  5. package/dist/contember/index.d.ts +1 -2
  6. package/dist/contember/index.d.ts.map +1 -1
  7. package/dist/handles/EntityHandle.d.ts +18 -29
  8. package/dist/handles/EntityHandle.d.ts.map +1 -1
  9. package/dist/handles/FieldHandle.d.ts +5 -3
  10. package/dist/handles/FieldHandle.d.ts.map +1 -1
  11. package/dist/handles/HasManyListHandle.d.ts +4 -3
  12. package/dist/handles/HasManyListHandle.d.ts.map +1 -1
  13. package/dist/handles/HasOneHandle.d.ts +33 -36
  14. package/dist/handles/HasOneHandle.d.ts.map +1 -1
  15. package/dist/handles/PlaceholderHandle.d.ts +9 -19
  16. package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
  17. package/dist/handles/proxyFactory.d.ts +9 -37
  18. package/dist/handles/proxyFactory.d.ts.map +1 -1
  19. package/dist/handles/types.d.ts +4 -0
  20. package/dist/handles/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +24 -1
  24. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -1
  25. package/dist/schema/ContemberSchema.d.ts +1 -97
  26. package/dist/schema/ContemberSchema.d.ts.map +1 -1
  27. package/dist/schema/SchemaLoader.d.ts +1 -27
  28. package/dist/schema/SchemaLoader.d.ts.map +1 -1
  29. package/dist/schema/SchemaRegistry.d.ts +1 -94
  30. package/dist/schema/SchemaRegistry.d.ts.map +1 -1
  31. package/dist/schema/roles.d.ts +1 -61
  32. package/dist/schema/roles.d.ts.map +1 -1
  33. package/dist/schema/types.d.ts +1 -178
  34. package/dist/schema/types.d.ts.map +1 -1
  35. package/dist/selection/SelectionMetaCollector.d.ts +1 -31
  36. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -1
  37. package/dist/selection/SelectionScope.d.ts +1 -76
  38. package/dist/selection/SelectionScope.d.ts.map +1 -1
  39. package/dist/selection/buildQuery.d.ts +1 -44
  40. package/dist/selection/buildQuery.d.ts.map +1 -1
  41. package/dist/selection/createFragment.d.ts +1 -28
  42. package/dist/selection/createFragment.d.ts.map +1 -1
  43. package/dist/selection/createSelectionBuilder.d.ts +1 -17
  44. package/dist/selection/createSelectionBuilder.d.ts.map +1 -1
  45. package/dist/selection/queryTypes.d.ts +1 -83
  46. package/dist/selection/queryTypes.d.ts.map +1 -1
  47. package/dist/selection/types.d.ts +1 -237
  48. package/dist/selection/types.d.ts.map +1 -1
  49. package/dist/utils/aliasGenerator.d.ts +1 -24
  50. package/dist/utils/aliasGenerator.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/adapter/ContemberAdapter.ts +193 -123
  53. package/src/brand/ComponentBrand.ts +1 -28
  54. package/src/contember/index.ts +1 -8
  55. package/src/handles/EntityHandle.ts +131 -61
  56. package/src/handles/FieldHandle.ts +19 -3
  57. package/src/handles/HasManyListHandle.ts +15 -12
  58. package/src/handles/HasOneHandle.ts +98 -66
  59. package/src/handles/PlaceholderHandle.ts +26 -24
  60. package/src/handles/proxyFactory.ts +29 -128
  61. package/src/handles/types.ts +6 -0
  62. package/src/index.ts +22 -3
  63. package/src/persistence/ContemberSchemaMutationAdapter.ts +19 -1
  64. package/src/schema/ContemberSchema.ts +14 -132
  65. package/src/schema/SchemaLoader.ts +1 -93
  66. package/src/schema/SchemaRegistry.ts +1 -286
  67. package/src/schema/roles.ts +10 -83
  68. package/src/schema/types.ts +21 -238
  69. package/src/selection/SelectionMetaCollector.ts +1 -122
  70. package/src/selection/SelectionScope.ts +1 -208
  71. package/src/selection/buildQuery.ts +1 -134
  72. package/src/selection/createFragment.ts +1 -50
  73. package/src/selection/createSelectionBuilder.ts +1 -283
  74. package/src/selection/queryTypes.ts +8 -135
  75. package/src/selection/types.ts +13 -320
  76. package/src/utils/aliasGenerator.ts +1 -56
@@ -12,11 +12,11 @@ import {
12
12
  } from '../core/actions.js'
13
13
  import {
14
14
  FIELD_REF_META,
15
- type HasOneRef,
16
15
  type FieldRefMeta,
17
16
  type SelectedEntityFields,
18
17
  type Unsubscribe,
19
18
  type EntityAccessor,
19
+ type HasOneAccessor,
20
20
  } from './types.js'
21
21
  import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
22
22
  import type {
@@ -34,7 +34,7 @@ import type {
34
34
  } from '../events/types.js'
35
35
  import { EntityHandle } from './EntityHandle.js'
36
36
  import { PlaceholderHandle } from './PlaceholderHandle.js'
37
- import { createHandleProxy, HAS_ONE_HANDLE_PROPERTIES } from './proxyFactory.js'
37
+ import { createHandleProxy } from './proxyFactory.js'
38
38
 
39
39
 
40
40
  /**
@@ -44,35 +44,15 @@ import { createHandleProxy, HAS_ONE_HANDLE_PROPERTIES } from './proxyFactory.js'
44
44
  * @typeParam TEntity - The full entity type of the related entity
45
45
  * @typeParam TSelected - The selected subset of fields (defaults to TEntity for backwards compatibility)
46
46
  */
47
- export class HasOneHandle<TEntity extends object = object, TSelected = TEntity> extends EntityRelatedHandle implements HasOneRef<TEntity, TSelected> {
48
- private entityHandleCache: EntityHandle<TEntity, TSelected> | null = null
49
- private placeholderCache: PlaceholderHandle<TEntity, TSelected> | null = null
47
+ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity> extends EntityRelatedHandle {
48
+ private entityHandleCacheRaw: EntityHandle<TEntity, TSelected> | null = null
49
+ private entityHandleCacheProxy: EntityAccessor<TEntity, TSelected> | null = null
50
+ private placeholderCacheRaw: PlaceholderHandle<TEntity, TSelected> | null = null
51
+ private placeholderCacheProxy: EntityAccessor<TEntity, TSelected> | null = null
50
52
 
51
53
  /** Runtime brand symbols for validation */
52
54
  readonly __brands?: Set<symbol>
53
55
 
54
- /** Type brand for schema - placeholder at runtime */
55
- declare readonly __schema: Record<string, object>
56
-
57
- // $ aliases - handled by proxy at runtime, declared for TypeScript
58
- declare readonly $id: string
59
- declare readonly $isDirty: boolean
60
- declare readonly $state: 'connected' | 'disconnected' | 'deleted' | 'creating'
61
- declare readonly $fields: SelectedEntityFields<TEntity, TSelected>
62
- declare readonly $entity: EntityAccessor<TEntity, TSelected>
63
- declare readonly $errors: readonly FieldError[]
64
- declare readonly $hasError: boolean
65
- declare $connect: (id: string) => void
66
- declare $disconnect: () => void
67
- declare $delete: () => void
68
- declare $reset: () => void
69
- declare $addError: (error: ErrorInput) => void
70
- declare $clearErrors: () => void
71
- declare $onConnect: (listener: EventListener<RelationConnectedEvent>) => Unsubscribe
72
- declare $onDisconnect: (listener: EventListener<RelationDisconnectedEvent>) => Unsubscribe
73
- declare $interceptConnect: (interceptor: Interceptor<RelationConnectingEvent>) => Unsubscribe
74
- declare $interceptDisconnect: (interceptor: Interceptor<RelationDisconnectingEvent>) => Unsubscribe
75
-
76
56
  private constructor(
77
57
  parentEntityType: string,
78
58
  parentEntityId: string,
@@ -98,11 +78,26 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
98
78
  schema: SchemaRegistry,
99
79
  brands?: Set<symbol>,
100
80
  selection?: SelectionMeta,
81
+ ): HasOneAccessor<TEntity, TSelected> {
82
+ return createHandleProxy<HasOneHandle<TEntity, TSelected>, HasOneAccessor<TEntity, TSelected>>(new HasOneHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, schema, brands, selection), (target) => target.entityRaw.fields)
83
+ }
84
+
85
+ static createRaw<TEntity extends object = object, TSelected = TEntity>(
86
+ parentEntityType: string,
87
+ parentEntityId: string,
88
+ fieldName: string,
89
+ targetType: string,
90
+ store: SnapshotStore,
91
+ dispatcher: ActionDispatcher,
92
+ schema: SchemaRegistry,
93
+ brands?: Set<symbol>,
94
+ selection?: SelectionMeta,
101
95
  ): HasOneHandle<TEntity, TSelected> {
102
- return createHandleProxy(new HasOneHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, schema, brands, selection), {
103
- knownProperties: HAS_ONE_HANDLE_PROPERTIES,
104
- getFields: (target) => target.entity.$fields,
105
- })
96
+ return new HasOneHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, schema, brands, selection)
97
+ }
98
+
99
+ static wrapProxy<TEntity extends object, TSelected>(handle: HasOneHandle<TEntity, TSelected>): HasOneAccessor<TEntity, TSelected> {
100
+ return createHandleProxy<HasOneHandle<TEntity, TSelected>, HasOneAccessor<TEntity, TSelected>>(handle, (target) => target.entityRaw.fields)
106
101
  }
107
102
 
108
103
  /**
@@ -198,21 +193,18 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
198
193
  }
199
194
 
200
195
  /**
201
- * Gets the related entity accessor with direct field access.
202
- * Implements HasOneRef.$entity - returns EntityAccessor for the related entity.
203
- * Returns PlaceholderHandle (with placeholder ID) if the relation is disconnected.
196
+ * Gets the raw (unproxied) related entity handle.
197
+ * Returns raw EntityHandle or raw PlaceholderHandle.
198
+ * Used internally to avoid going through the proxy layer.
204
199
  */
205
- get entity(): EntityAccessor<TEntity, TSelected> {
200
+ get entityRaw(): EntityHandle<TEntity, TSelected> | PlaceholderHandle<TEntity, TSelected> {
206
201
  const id = this.relatedId
207
202
 
208
203
  if (id) {
209
- // Ensure the related entity has a snapshot in the store
210
- // (it may be embedded in the parent entity's data)
211
204
  this.ensureRelatedEntitySnapshot(id)
212
205
 
213
- // Connected - return real entity handle
214
- if (!this.entityHandleCache || this.entityHandleCache.id !== id) {
215
- this.entityHandleCache = EntityHandle.create<TEntity, TSelected>(
206
+ if (!this.entityHandleCacheRaw || this.entityHandleCacheRaw.id !== id) {
207
+ this.entityHandleCacheRaw = EntityHandle.createRaw<TEntity, TSelected>(
216
208
  id,
217
209
  this.targetType,
218
210
  this.store,
@@ -221,14 +213,13 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
221
213
  this.__brands,
222
214
  this.selection,
223
215
  )
216
+ this.entityHandleCacheProxy = EntityHandle.wrapProxy(this.entityHandleCacheRaw)
224
217
  }
225
- // EntityHandle constructor returns a Proxy that implements EntityAccessor
226
- return this.entityHandleCache as unknown as EntityAccessor<TEntity, TSelected>
218
+ return this.entityHandleCacheRaw
227
219
  }
228
220
 
229
- // Disconnected - return placeholder handle
230
- if (!this.placeholderCache) {
231
- this.placeholderCache = PlaceholderHandle.create<TEntity, TSelected>(
221
+ if (!this.placeholderCacheRaw) {
222
+ this.placeholderCacheRaw = PlaceholderHandle.createRaw<TEntity, TSelected>(
232
223
  this.entityType,
233
224
  this.entityId,
234
225
  this.fieldName,
@@ -237,9 +228,38 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
237
228
  this.dispatcher,
238
229
  this.__brands,
239
230
  )
231
+ this.placeholderCacheProxy = PlaceholderHandle.wrapProxy(this.placeholderCacheRaw)
232
+ }
233
+ return this.placeholderCacheRaw
234
+ }
235
+
236
+ /**
237
+ * Gets the related entity accessor with direct field access.
238
+ * Implements HasOneRef.$entity - returns EntityAccessor for the related entity.
239
+ * Returns PlaceholderHandle (with placeholder ID) if the relation is disconnected.
240
+ */
241
+ get entity(): EntityAccessor<TEntity, TSelected> {
242
+ const id = this.relatedId
243
+
244
+ if (id) {
245
+ // Ensure the related entity has a snapshot in the store
246
+ // (it may be embedded in the parent entity's data)
247
+ this.ensureRelatedEntitySnapshot(id)
248
+
249
+ // Connected - return real entity handle (populate raw cache via entityRaw if needed)
250
+ if (!this.entityHandleCacheRaw || this.entityHandleCacheRaw.id !== id) {
251
+ // entityRaw populates both raw and proxy caches
252
+ this.entityRaw
253
+ }
254
+ return this.entityHandleCacheProxy!
255
+ }
256
+
257
+ // Disconnected - return placeholder handle
258
+ if (!this.placeholderCacheRaw) {
259
+ // entityRaw populates both raw and proxy caches
260
+ this.entityRaw
240
261
  }
241
- // PlaceholderHandle implements EntityRef but for API consistency return as EntityAccessor
242
- return this.placeholderCache as unknown as EntityAccessor<TEntity, TSelected>
262
+ return this.placeholderCacheProxy!
243
263
  }
244
264
 
245
265
  /**
@@ -288,6 +308,15 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
288
308
  )
289
309
  }
290
310
 
311
+ /**
312
+ * Checks if the related entity is currently being persisted.
313
+ */
314
+ get isPersisting(): boolean {
315
+ const id = this.relatedId
316
+ if (!id) return false
317
+ return this.store.isPersisting(this.targetType, id)
318
+ }
319
+
291
320
  /**
292
321
  * Checks if the relation is dirty.
293
322
  */
@@ -349,8 +378,11 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
349
378
  */
350
379
  override dispose(): void {
351
380
  super.dispose()
352
- this.entityHandleCache?.dispose()
353
- this.entityHandleCache = null
381
+ this.entityHandleCacheRaw?.dispose()
382
+ this.entityHandleCacheRaw = null
383
+ this.entityHandleCacheProxy = null
384
+ this.placeholderCacheRaw = null
385
+ this.placeholderCacheProxy = null
354
386
  }
355
387
 
356
388
  /**
@@ -454,47 +486,47 @@ export class HasOneHandle<TEntity extends object = object, TSelected = TEntity>
454
486
  }
455
487
 
456
488
  // ==================== EntityRef-compatible Properties ====================
457
- // These make HasOneAccessor structurally compatible with EntityAccessor
489
+ // These delegate to entityRaw so that proxy resolution ($data→data, $isNew→isNew, etc.) works correctly.
458
490
 
459
- /** Raw data snapshot of the related entity - delegates to $entity */
460
- get $data(): TSelected | null { return this.entity.$data }
491
+ /** Raw data snapshot of the related entity - delegates to entityRaw */
492
+ get data(): TSelected | null { return this.entityRaw.data }
461
493
 
462
- /** Whether this entity is new - delegates to $entity */
463
- get $isNew(): boolean { return this.entity.$isNew }
494
+ /** Whether this entity is new - delegates to entityRaw */
495
+ get isNew(): boolean { return this.entityRaw.isNew }
464
496
 
465
- /** Server-assigned ID after persistence - delegates to $entity */
466
- get $persistedId(): string | null { return this.entity.$persistedId }
497
+ /** Server-assigned ID after persistence - delegates to entityRaw */
498
+ get persistedId(): string | null { return this.entityRaw.persistedId }
467
499
 
468
500
  /** Type brand for entity name */
469
501
  get __entityName(): string { return this.targetType }
470
502
 
471
- /** Clear all errors - delegates to $entity */
472
- $clearAllErrors(): void { this.entity.$clearAllErrors() }
503
+ /** Clear all errors - delegates to entityRaw */
504
+ clearAllErrors(): void { this.entityRaw.clearAllErrors() }
473
505
 
474
506
  /** Subscribe to any event on the related entity */
475
- $on<E extends AfterEventTypes>(
507
+ on<E extends AfterEventTypes>(
476
508
  eventType: E,
477
509
  listener: EventListener<EventTypeMap[E]>,
478
510
  ): Unsubscribe {
479
- return this.entity.$on(eventType, listener)
511
+ return this.entityRaw.on(eventType, listener)
480
512
  }
481
513
 
482
514
  /** Intercept any before event on the related entity */
483
- $intercept<E extends BeforeEventTypes>(
515
+ intercept<E extends BeforeEventTypes>(
484
516
  eventType: E,
485
517
  interceptor: Interceptor<EventTypeMap[E]>,
486
518
  ): Unsubscribe {
487
- return this.entity.$intercept(eventType, interceptor)
519
+ return this.entityRaw.intercept(eventType, interceptor)
488
520
  }
489
521
 
490
522
  /** Subscribe to persist success events on the related entity */
491
- $onPersisted(listener: EventListener<EntityPersistedEvent>): Unsubscribe {
492
- return this.entity.$onPersisted(listener)
523
+ onPersisted(listener: EventListener<EntityPersistedEvent>): Unsubscribe {
524
+ return this.entityRaw.onPersisted(listener)
493
525
  }
494
526
 
495
527
  /** Intercept persist on the related entity */
496
- $interceptPersisting(interceptor: Interceptor<EntityPersistingEvent>): Unsubscribe {
497
- return this.entity.$interceptPersisting(interceptor)
528
+ interceptPersisting(interceptor: Interceptor<EntityPersistingEvent>): Unsubscribe {
529
+ return this.entityRaw.interceptPersisting(interceptor)
498
530
  }
499
531
 
500
532
  }
@@ -3,10 +3,10 @@ import type { SnapshotStore } from '../store/SnapshotStore.js'
3
3
  import { generatePlaceholderId } from '../store/SnapshotStore.js'
4
4
  import {
5
5
  FIELD_REF_META,
6
- type EntityRef,
7
6
  type SelectedEntityFields,
8
7
  type FieldRefMeta,
9
8
  type Unsubscribe,
9
+ type EntityAccessor,
10
10
  } from './types.js'
11
11
  import type { ErrorInput, FieldError } from '../errors/types.js'
12
12
  import type {
@@ -28,34 +28,13 @@ import { createAliasProxy } from './proxyFactory.js'
28
28
  * @typeParam TEntity - The full entity type
29
29
  * @typeParam TSelected - The selected subset of fields
30
30
  */
31
- export class PlaceholderHandle<TEntity extends object = object, TSelected = TEntity>
32
- implements EntityRef<TEntity, TSelected>
33
- {
31
+ export class PlaceholderHandle<TEntity extends object = object, TSelected = TEntity> {
34
32
  /** Runtime brand symbols for validation */
35
33
  readonly __brands?: Set<symbol>
36
34
 
37
- /** Type brand for schema - placeholder at runtime */
38
- declare readonly __schema: Record<string, object>
39
-
40
35
  /** Placeholder ID for this handle */
41
36
  private readonly placeholderId: string
42
37
 
43
- // $ aliases - handled by proxy at runtime, declared for TypeScript
44
- declare readonly $fields: SelectedEntityFields<TEntity, TSelected>
45
- declare readonly $data: TSelected | null
46
- declare readonly $isDirty: boolean
47
- declare readonly $persistedId: null
48
- declare readonly $isNew: boolean
49
- declare readonly $errors: readonly FieldError[]
50
- declare readonly $hasError: boolean
51
- declare $addError: (error: ErrorInput) => void
52
- declare $clearErrors: () => void
53
- declare $clearAllErrors: () => void
54
- declare $on: <E extends AfterEventTypes>(eventType: E, listener: EventListener<EventTypeMap[E]>) => Unsubscribe
55
- declare $intercept: <E extends BeforeEventTypes>(eventType: E, interceptor: Interceptor<EventTypeMap[E]>) => Unsubscribe
56
- declare $onPersisted: (listener: EventListener<EntityPersistedEvent>) => Unsubscribe
57
- declare $interceptPersisting: (interceptor: Interceptor<EntityPersistingEvent>) => Unsubscribe
58
-
59
38
  private constructor(
60
39
  private readonly parentEntityType: string,
61
40
  private readonly parentEntityId: string,
@@ -77,8 +56,24 @@ export class PlaceholderHandle<TEntity extends object = object, TSelected = TEnt
77
56
  store: SnapshotStore,
78
57
  dispatcher: ActionDispatcher,
79
58
  brands?: Set<symbol>,
59
+ ): EntityAccessor<TEntity, TSelected> {
60
+ return PlaceholderHandle.wrapProxy(new PlaceholderHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, brands))
61
+ }
62
+
63
+ static createRaw<TEntity extends object = object, TSelected = TEntity>(
64
+ parentEntityType: string,
65
+ parentEntityId: string,
66
+ fieldName: string,
67
+ targetType: string,
68
+ store: SnapshotStore,
69
+ dispatcher: ActionDispatcher,
70
+ brands?: Set<symbol>,
80
71
  ): PlaceholderHandle<TEntity, TSelected> {
81
- return createAliasProxy(new PlaceholderHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, brands))
72
+ return new PlaceholderHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, targetType, store, dispatcher, brands)
73
+ }
74
+
75
+ static wrapProxy<TEntity extends object, TSelected>(handle: PlaceholderHandle<TEntity, TSelected>): EntityAccessor<TEntity, TSelected> {
76
+ return createAliasProxy<PlaceholderHandle<TEntity, TSelected>, EntityAccessor<TEntity, TSelected>>(handle)
82
77
  }
83
78
 
84
79
  /**
@@ -115,6 +110,13 @@ export class PlaceholderHandle<TEntity extends object = object, TSelected = TEnt
115
110
  return relation ? Object.keys(relation.placeholderData).length > 0 : false
116
111
  }
117
112
 
113
+ /**
114
+ * Placeholder entities are never being persisted.
115
+ */
116
+ get isPersisting(): boolean {
117
+ return false
118
+ }
119
+
118
120
  /**
119
121
  * Placeholder entities are always new (not yet persisted).
120
122
  */
@@ -8,132 +8,40 @@
8
8
  * The $ prefix convention allows handle properties to avoid collision with entity field names:
9
9
  * - `entity.title` → field access (returns FieldHandle)
10
10
  * - `entity.$isDirty` → handle property
11
+ * - `entity.id` → entity ID (special case, always passes through)
11
12
  */
12
13
 
13
14
  import { FIELD_REF_META } from './types.js'
14
15
 
15
16
  /**
16
- * Shared base properties for entity-like handles (EntityHandle, HasOneHandle).
17
- * Extracting a common set prevents bugs from inconsistent maintenance (e.g. missing `id`).
17
+ * Properties that always pass through to the handle, never treated as field access.
18
+ * Kept minimal to avoid collisions with entity field names.
18
19
  */
19
- const BASE_ENTITY_PROPERTIES: (string | symbol)[] = [
20
- // Entity identity — id is always the entity ID, not a field handle
20
+ const HANDLE_PASSTHROUGH_PROPERTIES = new Set<string | symbol>([
21
+ // Entity identity — `entity.id` must return the entity ID string, not a FieldHandle
21
22
  'id',
22
- // Type brands (phantom types)
23
- '__entityType', '__entityName', '__brands', '__schema',
24
- // Symbol
23
+ // Symbol for field reference metadata
25
24
  FIELD_REF_META,
26
- // Core internals shared by all entity-like handles
27
- 'entityType', 'entityId', 'store', 'dispatcher', 'schema',
28
- 'getEntityData', 'getServerData', 'assertNotDisposed', 'isDisposed',
29
- 'dispose', 'subscribe',
30
- ]
31
-
32
- /**
33
- * Known properties on EntityHandle that should NOT be treated as field access.
34
- * Properties like `fields`, `data`, `errors` are NOT included - use $ prefix for those.
35
- */
36
- export const ENTITY_HANDLE_PROPERTIES = new Set<string | symbol>([
37
- ...BASE_ENTITY_PROPERTIES,
38
- // Entity-specific internals
39
- 'type', 'fieldHandleCache', 'relationHandleCache',
40
- // Internal methods (unlikely to be field names)
41
- 'field', 'hasOne', 'hasMany', 'getSnapshot',
42
- 'reset', 'commit',
43
- 'getDirtyFields', 'getDirtyRelations',
44
- // State properties (unlikely to be field names)
45
- 'serverData', 'isLoaded', 'isLoading', 'isError', 'error', 'isPersisting',
46
- ])
47
-
48
- /**
49
- * Known properties on HasOneHandle that should NOT be treated as field access.
50
- * Properties like `fields`, `entity`, `errors`, `state` are NOT included - they require $ prefix.
51
- */
52
- export const HAS_ONE_HANDLE_PROPERTIES = new Set<string | symbol>([
53
- ...BASE_ENTITY_PROPERTIES,
54
- // HasOne-specific internals
55
- 'fieldName', 'targetType',
56
- 'entityHandleCache', 'placeholderCache', 'ensureRelatedEntitySnapshot', 'relatedId',
57
- // Methods that are unlikely to be field names
58
- 'connect', 'disconnect', 'delete', 'reset',
59
- ])
60
-
61
- /**
62
- * All properties on FieldHandle (no field access proxy needed, just $ aliasing).
63
- */
64
- export const FIELD_HANDLE_PROPERTIES = new Set<string | symbol>([
65
- FIELD_REF_META,
66
- // Core properties
67
- 'value', 'serverValue', 'isDirty', 'inputProps', 'errors', 'hasError',
68
- 'path', 'fieldName',
69
- // Methods
70
- 'setValue', 'addError', 'clearErrors', 'onChange', 'onChanging', 'nested',
71
- // Internal
72
- 'entityType', 'entityId', 'fieldPath', 'store', 'dispatcher',
73
- 'getEntityData', 'getServerData', 'assertNotDisposed', 'isDisposed',
74
- 'subscribe', 'dispose',
75
- ])
76
-
77
- /**
78
- * All properties on HasManyListHandle (no field access proxy needed, just $ aliasing).
79
- */
80
- export const HAS_MANY_HANDLE_PROPERTIES = new Set<string | symbol>([
81
- FIELD_REF_META,
82
- // Type brands
83
- '__entityType', '__brands',
84
- // Core properties
85
- 'items', 'length', 'isDirty', 'errors', 'hasError',
86
- // Methods
87
- 'map', 'add', 'remove', 'move', 'connect', 'disconnect', 'reset',
88
- 'getItemHandle', 'addError', 'clearErrors',
89
- 'onItemConnected', 'onItemDisconnected', 'interceptItemConnecting', 'interceptItemDisconnecting',
90
- // Internal
91
- 'entityType', 'entityId', 'fieldName', 'itemType', 'store', 'dispatcher', 'schema',
92
- 'itemHandleCache', 'getEntityData', 'getServerData', 'assertNotDisposed', 'isDisposed',
93
- 'subscribe', 'dispose', 'ensureItemSnapshots',
94
25
  ])
95
26
 
96
- /**
97
- * All properties on PlaceholderHandle (no field access proxy needed, just $ aliasing).
98
- */
99
- export const PLACEHOLDER_HANDLE_PROPERTIES = new Set<string | symbol>([
100
- // Core properties
101
- 'id', 'data', 'isDirty', 'persistedId', 'isNew', 'fields', 'errors', 'hasError',
102
- // Type brands
103
- '__entityType', '__entityName', '__brands',
104
- // Methods
105
- 'addError', 'clearErrors', 'clearAllErrors',
106
- 'on', 'intercept', 'onPersisted', 'interceptPersisting',
107
- // Internal
108
- 'parentEntityType', 'parentEntityId', 'fieldName', 'targetType', 'store', 'dispatcher',
109
- 'placeholderId', 'createPlaceholderFieldHandle',
110
- ])
111
-
112
- /**
113
- * Configuration for creating a handle proxy.
114
- */
115
- export interface HandleProxyConfig<T extends object> {
116
- /** Set of property names that should pass through to the handle */
117
- knownProperties: Set<string | symbol>
118
- /** Function to get the fields proxy from the handle */
119
- getFields: (target: T) => object
120
- }
121
-
122
27
  /**
123
28
  * Creates a proxy around a handle that supports direct field access.
124
29
  *
125
- * - `handle.fieldName` is equivalent to `handle.$fields.fieldName`
126
- * - `handle.$propertyName` accesses handle properties with $ prefix stripped
127
- * - Known properties pass through directly to the handle
30
+ * Resolution order:
31
+ * 1. Symbols → pass through to handle
32
+ * 2. `id` / FIELD_REF_META → pass through to handle
33
+ * 3. `$xxx` → strip `$`, access handle property
34
+ * 4. Everything else → field access (returns FieldHandle/HasOneHandle/HasManyListHandle)
128
35
  *
129
36
  * @param handle - The handle instance to wrap
130
- * @param config - Configuration for the proxy
37
+ * @param getFields - Function to get the fields object from the handle
131
38
  * @returns Proxied handle with direct field access support
132
39
  */
133
- export function createHandleProxy<T extends object>(
40
+ export function createHandleProxy<T extends object, TResult = T>(
134
41
  handle: T,
135
- config: HandleProxyConfig<T>,
136
- ): T {
42
+ getFields: (target: T) => object,
43
+ ): TResult {
44
+ // The proxy adds $ alias support and field access, making the handle satisfy the public type TResult at runtime
137
45
  return new Proxy(handle, {
138
46
  get(target, prop, _receiver) {
139
47
  // Symbols and non-string props - pass through
@@ -141,41 +49,34 @@ export function createHandleProxy<T extends object>(
141
49
  return Reflect.get(target, prop, target)
142
50
  }
143
51
 
52
+ // Special properties that always pass through
53
+ if (HANDLE_PASSTHROUGH_PROPERTIES.has(prop)) {
54
+ return Reflect.get(target, prop, target)
55
+ }
56
+
144
57
  // $ prefixed - strip $ and access handle property
145
58
  if (prop.startsWith('$')) {
146
59
  const realProp = prop.slice(1)
147
- // Use target as receiver so getters use target as `this`, not the Proxy
148
60
  const value = Reflect.get(target, realProp, target)
149
- // Bind methods to preserve `this`
150
- if (typeof value === 'function') {
151
- return value.bind(target)
152
- }
153
- return value
154
- }
155
-
156
- // Known handle properties - pass through for backwards compatibility
157
- if (config.knownProperties.has(prop)) {
158
- const value = Reflect.get(target, prop, target)
159
61
  if (typeof value === 'function') {
160
62
  return value.bind(target)
161
63
  }
162
64
  return value
163
65
  }
164
66
 
165
- // Otherwise, treat as field access
166
- const fields = config.getFields(target)
67
+ // Everything else → field access
68
+ const fields = getFields(target)
167
69
  return (fields as Record<string, unknown>)[prop]
168
70
  },
169
71
 
170
72
  has(target, prop) {
171
- if (typeof prop === 'string' && !config.knownProperties.has(prop) && !prop.startsWith('$')) {
172
- // Check if it's a field
173
- const fields = config.getFields(target)
73
+ if (typeof prop === 'string' && !HANDLE_PASSTHROUGH_PROPERTIES.has(prop) && !prop.startsWith('$')) {
74
+ const fields = getFields(target)
174
75
  return prop in fields
175
76
  }
176
77
  return Reflect.has(target, prop)
177
78
  },
178
- })
79
+ }) as unknown as TResult
179
80
  }
180
81
 
181
82
  /**
@@ -188,7 +89,8 @@ export function createHandleProxy<T extends object>(
188
89
  * @param handle - The handle instance to wrap
189
90
  * @returns Proxied handle with $ alias support
190
91
  */
191
- export function createAliasProxy<T extends object>(handle: T): T {
92
+ export function createAliasProxy<T extends object, TResult = T>(handle: T): TResult {
93
+ // The proxy adds $ alias support, making the handle satisfy the public type TResult at runtime
192
94
  return new Proxy(handle, {
193
95
  get(target, prop, _receiver) {
194
96
  // Symbols and non-string props - pass through
@@ -200,7 +102,6 @@ export function createAliasProxy<T extends object>(handle: T): T {
200
102
  if (prop.startsWith('$')) {
201
103
  const realProp = prop.slice(1)
202
104
  const value = Reflect.get(target, realProp, target)
203
- // Bind methods to preserve `this`
204
105
  if (typeof value === 'function') {
205
106
  return value.bind(target)
206
107
  }
@@ -214,5 +115,5 @@ export function createAliasProxy<T extends object>(handle: T): T {
214
115
  }
215
116
  return value
216
117
  },
217
- })
118
+ }) as unknown as TResult
218
119
  }
@@ -396,6 +396,9 @@ export interface HasOneRefBase<
396
396
  /** Whether this entity is new */
397
397
  readonly $isNew: boolean
398
398
 
399
+ /** Whether entity is currently being persisted */
400
+ readonly $isPersisting: boolean
401
+
399
402
  /** Server-assigned ID after persistence */
400
403
  readonly $persistedId: string | null
401
404
 
@@ -539,6 +542,9 @@ export interface EntityRefBase<
539
542
  /** Whether entity is dirty */
540
543
  readonly $isDirty: boolean
541
544
 
545
+ /** Whether entity is currently being persisted */
546
+ readonly $isPersisting: boolean
547
+
542
548
  /** Server-assigned ID after persistence */
543
549
  readonly $persistedId: string | null
544
550
 
package/src/index.ts CHANGED
@@ -224,7 +224,7 @@ export { deepEqual } from './utils/deepEqual.js'
224
224
  export { generateHasManyAlias } from './utils/aliasGenerator.js'
225
225
 
226
226
  // Contember integration
227
- export { MutationCollector, ContemberSchemaMutationAdapter } from './contember/index.js'
227
+ export { MutationCollector, ContemberSchemaMutationAdapter, type SchemaNames } from './contember/index.js'
228
228
  export type { MutationSchemaProvider, EntityMutationResult } from './contember/index.js'
229
229
 
230
230
  // Undo/Redo
@@ -328,8 +328,27 @@ export {
328
328
  } from './errors/index.js'
329
329
 
330
330
 
331
- // Re-export Contember types for convenience
332
- export type { SchemaNames, SchemaEntityNames } from '@contember/client-content'
331
+ // Re-export bindx-client types
332
+ export {
333
+ ContentClient,
334
+ type ContentClientOptions,
335
+ type ContentQuery,
336
+ type ContentMutation,
337
+ ContentOperation,
338
+ type MutationResult as ClientMutationResult,
339
+ MutationFailedError,
340
+ type QuerySpecContext,
341
+ querySpecToGraphQl,
342
+ buildGetArgs,
343
+ buildListArgs,
344
+ buildCreateArgs,
345
+ buildUpdateArgs,
346
+ buildDeleteArgs,
347
+ buildMutationSelection,
348
+ mutationFragments,
349
+ unwrapPaginateResult,
350
+ } from '@contember/bindx-client'
351
+ export { qb } from '@contember/bindx-client'
333
352
 
334
353
  // DataView / DataGrid
335
354
  export type {
@@ -1,4 +1,22 @@
1
- import type { SchemaNames } from '@contember/client-content'
1
+ /**
2
+ * Runtime schema names format (from Contember API / generated code).
3
+ */
4
+ export interface SchemaNames {
5
+ readonly entities: {
6
+ readonly [entityName: string]: {
7
+ readonly name: string
8
+ readonly scalars: readonly string[]
9
+ readonly fields: {
10
+ readonly [fieldName: string]:
11
+ | { readonly type: 'column'; readonly enumName?: string; readonly columnType?: string }
12
+ | { readonly type: 'many' | 'one'; readonly entity: string }
13
+ }
14
+ }
15
+ }
16
+ readonly enums?: {
17
+ readonly [enumName: string]: readonly string[]
18
+ }
19
+ }
2
20
  import type { MutationSchemaProvider } from './MutationSchemaProvider.js'
3
21
 
4
22
  /**