@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.
- package/dist/adapter/ContemberAdapter.d.ts +40 -0
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
- package/dist/adapter/MockAdapter.d.ts +81 -0
- package/dist/adapter/MockAdapter.d.ts.map +1 -0
- package/dist/adapter/MockQueryEngine.d.ts +43 -0
- package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +184 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/brand/ComponentBrand.d.ts +25 -0
- package/dist/brand/ComponentBrand.d.ts.map +1 -0
- package/dist/contember/index.d.ts +10 -0
- package/dist/contember/index.d.ts.map +1 -0
- package/dist/core/ActionDispatcher.d.ts +64 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -0
- package/dist/core/Disposable.d.ts +57 -0
- package/dist/core/Disposable.d.ts.map +1 -0
- package/dist/core/EntityLoader.d.ts +80 -0
- package/dist/core/EntityLoader.d.ts.map +1 -0
- package/dist/core/PersistenceManager.d.ts +126 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/SelectionResolver.d.ts +21 -0
- package/dist/core/SelectionResolver.d.ts.map +1 -0
- package/dist/core/actionClassification.d.ts +48 -0
- package/dist/core/actionClassification.d.ts.map +1 -0
- package/dist/core/actions.d.ts +301 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/dataview/filterHandlers.d.ts +18 -0
- package/dist/dataview/filterHandlers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +3 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/types.d.ts +164 -0
- package/dist/dataview/types.d.ts.map +1 -0
- package/dist/errors/UnfetchedFieldError.d.ts +11 -0
- package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
- package/dist/errors/errorPathResolver.d.ts +52 -0
- package/dist/errors/errorPathResolver.d.ts.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/pathMapper.d.ts +42 -0
- package/dist/errors/pathMapper.d.ts.map +1 -0
- package/dist/errors/types.d.ts +131 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/events/EventEmitter.d.ts +97 -0
- package/dist/events/EventEmitter.d.ts.map +1 -0
- package/dist/events/eventFactory.d.ts +40 -0
- package/dist/events/eventFactory.d.ts.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +264 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/handles/BaseHandle.d.ts +64 -0
- package/dist/handles/BaseHandle.d.ts.map +1 -0
- package/dist/handles/EntityHandle.d.ts +208 -0
- package/dist/handles/EntityHandle.d.ts.map +1 -0
- package/dist/handles/FieldHandle.d.ts +111 -0
- package/dist/handles/FieldHandle.d.ts.map +1 -0
- package/dist/handles/HasManyListHandle.d.ts +166 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -0
- package/dist/handles/HasOneHandle.d.ts +168 -0
- package/dist/handles/HasOneHandle.d.ts.map +1 -0
- package/dist/handles/PlaceholderHandle.d.ts +117 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
- package/dist/handles/index.d.ts +8 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/proxyFactory.d.ts +66 -0
- package/dist/handles/proxyFactory.d.ts.map +1 -0
- package/dist/handles/types.d.ts +430 -0
- package/dist/handles/types.d.ts.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/persistence/BatchPersister.d.ts +142 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -0
- package/dist/persistence/ChangeRegistry.d.ts +87 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
- package/dist/persistence/MutationCollector.d.ts +96 -0
- package/dist/persistence/MutationCollector.d.ts.map +1 -0
- package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
- package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +189 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
- package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
- package/dist/roles/index.d.ts +11 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/types.d.ts +242 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/schema/ContemberSchema.d.ts +98 -0
- package/dist/schema/ContemberSchema.d.ts.map +1 -0
- package/dist/schema/SchemaLoader.d.ts +28 -0
- package/dist/schema/SchemaLoader.d.ts.map +1 -0
- package/dist/schema/SchemaRegistry.d.ts +95 -0
- package/dist/schema/SchemaRegistry.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/roles.d.ts +62 -0
- package/dist/schema/roles.d.ts.map +1 -0
- package/dist/schema/types.d.ts +179 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/selection/SelectionMetaCollector.d.ts +32 -0
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
- package/dist/selection/SelectionScope.d.ts +77 -0
- package/dist/selection/SelectionScope.d.ts.map +1 -0
- package/dist/selection/buildQuery.d.ts +45 -0
- package/dist/selection/buildQuery.d.ts.map +1 -0
- package/dist/selection/createFragment.d.ts +29 -0
- package/dist/selection/createFragment.d.ts.map +1 -0
- package/dist/selection/createSelectionBuilder.d.ts +18 -0
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/queryTypes.d.ts +84 -0
- package/dist/selection/queryTypes.d.ts.map +1 -0
- package/dist/selection/types.d.ts +238 -0
- package/dist/selection/types.d.ts.map +1 -0
- package/dist/store/DirtyTracker.d.ts +26 -0
- package/dist/store/DirtyTracker.d.ts.map +1 -0
- package/dist/store/EntityMetaStore.d.ts +52 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -0
- package/dist/store/EntitySnapshotStore.d.ts +63 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
- package/dist/store/ErrorStore.d.ts +82 -0
- package/dist/store/ErrorStore.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +175 -0
- package/dist/store/RelationStore.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +143 -0
- package/dist/store/SnapshotStore.d.ts.map +1 -0
- package/dist/store/SubscriptionManager.d.ts +90 -0
- package/dist/store/SubscriptionManager.d.ts.map +1 -0
- package/dist/store/TouchedStore.d.ts +24 -0
- package/dist/store/TouchedStore.d.ts.map +1 -0
- package/dist/store/entityId.d.ts +29 -0
- package/dist/store/entityId.d.ts.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/snapshots.d.ts +144 -0
- package/dist/store/snapshots.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +120 -0
- package/dist/undo/UndoManager.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/types.d.ts +74 -0
- package/dist/undo/types.d.ts.map +1 -0
- package/dist/utils/aliasGenerator.d.ts +25 -0
- package/dist/utils/aliasGenerator.d.ts.map +1 -0
- package/dist/utils/deepEqual.d.ts +6 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/adapter/ContemberAdapter.ts +239 -0
- package/src/adapter/MockAdapter.ts +394 -0
- package/src/adapter/MockQueryEngine.ts +236 -0
- package/src/adapter/index.ts +21 -0
- package/src/adapter/types.ts +199 -0
- package/src/brand/ComponentBrand.ts +28 -0
- package/src/contember/index.ts +17 -0
- package/src/core/ActionDispatcher.ts +450 -0
- package/src/core/Disposable.ts +126 -0
- package/src/core/EntityLoader.ts +169 -0
- package/src/core/SelectionResolver.ts +78 -0
- package/src/core/actionClassification.ts +196 -0
- package/src/core/actions.ts +491 -0
- package/src/core/index.ts +20 -0
- package/src/dataview/filterHandlers.ts +443 -0
- package/src/dataview/index.ts +36 -0
- package/src/dataview/types.ts +220 -0
- package/src/errors/UnfetchedFieldError.ts +18 -0
- package/src/errors/errorPathResolver.ts +239 -0
- package/src/errors/index.ts +40 -0
- package/src/errors/pathMapper.ts +40 -0
- package/src/errors/types.ts +223 -0
- package/src/events/EventEmitter.ts +447 -0
- package/src/events/eventFactory.ts +430 -0
- package/src/events/index.ts +89 -0
- package/src/events/types.ts +374 -0
- package/src/handles/BaseHandle.ts +100 -0
- package/src/handles/EntityHandle.ts +538 -0
- package/src/handles/FieldHandle.ts +268 -0
- package/src/handles/HasManyListHandle.ts +500 -0
- package/src/handles/HasOneHandle.ts +500 -0
- package/src/handles/PlaceholderHandle.ts +321 -0
- package/src/handles/index.ts +37 -0
- package/src/handles/proxyFactory.ts +218 -0
- package/src/handles/types.ts +743 -0
- package/src/index.ts +384 -0
- package/src/internal.ts +19 -0
- package/src/persistence/BatchPersister.ts +911 -0
- package/src/persistence/ChangeRegistry.ts +150 -0
- package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
- package/src/persistence/MutationCollector.ts +567 -0
- package/src/persistence/MutationSchemaProvider.ts +33 -0
- package/src/persistence/index.ts +23 -0
- package/src/persistence/types.ts +220 -0
- package/src/schema/ContemberSchema.ts +132 -0
- package/src/schema/SchemaLoader.ts +93 -0
- package/src/schema/SchemaRegistry.ts +286 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/roles.ts +83 -0
- package/src/schema/types.ts +238 -0
- package/src/selection/SelectionMetaCollector.ts +122 -0
- package/src/selection/SelectionScope.ts +208 -0
- package/src/selection/buildQuery.ts +134 -0
- package/src/selection/createFragment.ts +50 -0
- package/src/selection/createSelectionBuilder.ts +283 -0
- package/src/selection/index.ts +31 -0
- package/src/selection/queryTypes.ts +135 -0
- package/src/selection/types.ts +320 -0
- package/src/store/DirtyTracker.ts +117 -0
- package/src/store/EntityMetaStore.ts +143 -0
- package/src/store/EntitySnapshotStore.ts +260 -0
- package/src/store/ErrorStore.ts +251 -0
- package/src/store/RelationStore.ts +696 -0
- package/src/store/SnapshotStore.ts +765 -0
- package/src/store/SubscriptionManager.ts +241 -0
- package/src/store/TouchedStore.ts +42 -0
- package/src/store/entityId.ts +43 -0
- package/src/store/index.ts +2 -0
- package/src/store/snapshots.ts +203 -0
- package/src/undo/UndoManager.ts +418 -0
- package/src/undo/index.ts +9 -0
- package/src/undo/types.ts +84 -0
- package/src/utils/aliasGenerator.ts +56 -0
- package/src/utils/deepEqual.ts +33 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { EntityRelatedHandle } from './BaseHandle.js'
|
|
2
|
+
import type { ActionDispatcher } from '../core/ActionDispatcher.js'
|
|
3
|
+
import type { SnapshotStore } from '../store/SnapshotStore.js'
|
|
4
|
+
import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
|
|
5
|
+
import type { SelectionMeta } from '../selection/types.js'
|
|
6
|
+
import {
|
|
7
|
+
connectRelation,
|
|
8
|
+
disconnectRelation,
|
|
9
|
+
deleteRelation,
|
|
10
|
+
addRelationError,
|
|
11
|
+
clearRelationErrors,
|
|
12
|
+
} from '../core/actions.js'
|
|
13
|
+
import {
|
|
14
|
+
FIELD_REF_META,
|
|
15
|
+
type HasOneRef,
|
|
16
|
+
type FieldRefMeta,
|
|
17
|
+
type SelectedEntityFields,
|
|
18
|
+
type Unsubscribe,
|
|
19
|
+
type EntityAccessor,
|
|
20
|
+
} from './types.js'
|
|
21
|
+
import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
|
|
22
|
+
import type {
|
|
23
|
+
EventTypeMap,
|
|
24
|
+
AfterEventTypes,
|
|
25
|
+
BeforeEventTypes,
|
|
26
|
+
EventListener,
|
|
27
|
+
Interceptor,
|
|
28
|
+
EntityPersistedEvent,
|
|
29
|
+
EntityPersistingEvent,
|
|
30
|
+
RelationConnectedEvent,
|
|
31
|
+
RelationDisconnectedEvent,
|
|
32
|
+
RelationConnectingEvent,
|
|
33
|
+
RelationDisconnectingEvent,
|
|
34
|
+
} from '../events/types.js'
|
|
35
|
+
import { EntityHandle } from './EntityHandle.js'
|
|
36
|
+
import { PlaceholderHandle } from './PlaceholderHandle.js'
|
|
37
|
+
import { createHandleProxy, HAS_ONE_HANDLE_PROPERTIES } from './proxyFactory.js'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* HasOneHandle provides access to a has-one relation.
|
|
42
|
+
* Implements HasOneRef interface for JSX compatibility.
|
|
43
|
+
*
|
|
44
|
+
* @typeParam TEntity - The full entity type of the related entity
|
|
45
|
+
* @typeParam TSelected - The selected subset of fields (defaults to TEntity for backwards compatibility)
|
|
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
|
|
50
|
+
|
|
51
|
+
/** Runtime brand symbols for validation */
|
|
52
|
+
readonly __brands?: Set<symbol>
|
|
53
|
+
|
|
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
|
+
private constructor(
|
|
77
|
+
parentEntityType: string,
|
|
78
|
+
parentEntityId: string,
|
|
79
|
+
private readonly fieldName: string,
|
|
80
|
+
private readonly targetType: string,
|
|
81
|
+
store: SnapshotStore,
|
|
82
|
+
dispatcher: ActionDispatcher,
|
|
83
|
+
private readonly schema: SchemaRegistry,
|
|
84
|
+
brands?: Set<symbol>,
|
|
85
|
+
private readonly selection?: SelectionMeta,
|
|
86
|
+
) {
|
|
87
|
+
super(parentEntityType, parentEntityId, store, dispatcher)
|
|
88
|
+
this.__brands = brands
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static create<TEntity extends object = object, TSelected = TEntity>(
|
|
92
|
+
parentEntityType: string,
|
|
93
|
+
parentEntityId: string,
|
|
94
|
+
fieldName: string,
|
|
95
|
+
targetType: string,
|
|
96
|
+
store: SnapshotStore,
|
|
97
|
+
dispatcher: ActionDispatcher,
|
|
98
|
+
schema: SchemaRegistry,
|
|
99
|
+
brands?: Set<symbol>,
|
|
100
|
+
selection?: SelectionMeta,
|
|
101
|
+
): 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
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* JSX field reference metadata for collection phase.
|
|
110
|
+
* Implements HasOneRef interface.
|
|
111
|
+
*/
|
|
112
|
+
get [FIELD_REF_META](): FieldRefMeta {
|
|
113
|
+
return {
|
|
114
|
+
entityType: this.entityType,
|
|
115
|
+
entityId: this.entityId,
|
|
116
|
+
path: [this.fieldName],
|
|
117
|
+
fieldName: this.fieldName,
|
|
118
|
+
isArray: false,
|
|
119
|
+
isRelation: true,
|
|
120
|
+
targetType: this.targetType,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Subscribe to relation changes.
|
|
126
|
+
*/
|
|
127
|
+
override subscribe(callback: () => void): () => void {
|
|
128
|
+
// Subscribe to both parent entity and relation state
|
|
129
|
+
const unsub1 = this.store.subscribeToEntity(this.entityType, this.entityId, callback)
|
|
130
|
+
const unsub2 = this.store.subscribeToRelation(
|
|
131
|
+
this.entityType,
|
|
132
|
+
this.entityId,
|
|
133
|
+
this.fieldName,
|
|
134
|
+
callback,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
unsub1()
|
|
139
|
+
unsub2()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Gets the relation state.
|
|
145
|
+
*/
|
|
146
|
+
get state(): 'connected' | 'disconnected' | 'deleted' | 'creating' {
|
|
147
|
+
const relation = this.store.getRelation(
|
|
148
|
+
this.entityType,
|
|
149
|
+
this.entityId,
|
|
150
|
+
this.fieldName,
|
|
151
|
+
)
|
|
152
|
+
return relation?.state ?? 'disconnected'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets the current related entity ID.
|
|
157
|
+
* Falls back to entity data if relation state is not initialized.
|
|
158
|
+
*/
|
|
159
|
+
get relatedId(): string | null {
|
|
160
|
+
// First check relation state (for manual changes like connect/disconnect)
|
|
161
|
+
const relation = this.store.getRelation(
|
|
162
|
+
this.entityType,
|
|
163
|
+
this.entityId,
|
|
164
|
+
this.fieldName,
|
|
165
|
+
)
|
|
166
|
+
if (relation) {
|
|
167
|
+
return relation.currentId
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fallback to entity snapshot data (for server-loaded data)
|
|
171
|
+
const parentSnapshot = this.store.getEntitySnapshot(this.entityType, this.entityId)
|
|
172
|
+
if (parentSnapshot?.data) {
|
|
173
|
+
const relatedData = (parentSnapshot.data as Record<string, unknown>)[this.fieldName]
|
|
174
|
+
if (relatedData && typeof relatedData === 'object' && 'id' in relatedData) {
|
|
175
|
+
return (relatedData as { id: string }).id
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Gets the related entity ID.
|
|
184
|
+
* Returns the actual ID if connected, or placeholder ID if disconnected.
|
|
185
|
+
* Implements HasOneRef interface.
|
|
186
|
+
*/
|
|
187
|
+
get id(): string {
|
|
188
|
+
return this.entity.id
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Gets the nested entity fields.
|
|
193
|
+
* Implements HasOneRef interface.
|
|
194
|
+
* Delegates to the entity (either real EntityHandle or PlaceholderHandle).
|
|
195
|
+
*/
|
|
196
|
+
get fields(): SelectedEntityFields<TEntity, TSelected> {
|
|
197
|
+
return this.entity.$fields
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
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.
|
|
204
|
+
*/
|
|
205
|
+
get entity(): EntityAccessor<TEntity, TSelected> {
|
|
206
|
+
const id = this.relatedId
|
|
207
|
+
|
|
208
|
+
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
|
+
this.ensureRelatedEntitySnapshot(id)
|
|
212
|
+
|
|
213
|
+
// Connected - return real entity handle
|
|
214
|
+
if (!this.entityHandleCache || this.entityHandleCache.id !== id) {
|
|
215
|
+
this.entityHandleCache = EntityHandle.create<TEntity, TSelected>(
|
|
216
|
+
id,
|
|
217
|
+
this.targetType,
|
|
218
|
+
this.store,
|
|
219
|
+
this.dispatcher,
|
|
220
|
+
this.schema,
|
|
221
|
+
this.__brands,
|
|
222
|
+
this.selection,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
// EntityHandle constructor returns a Proxy that implements EntityAccessor
|
|
226
|
+
return this.entityHandleCache as unknown as EntityAccessor<TEntity, TSelected>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Disconnected - return placeholder handle
|
|
230
|
+
if (!this.placeholderCache) {
|
|
231
|
+
this.placeholderCache = PlaceholderHandle.create<TEntity, TSelected>(
|
|
232
|
+
this.entityType,
|
|
233
|
+
this.entityId,
|
|
234
|
+
this.fieldName,
|
|
235
|
+
this.targetType,
|
|
236
|
+
this.store,
|
|
237
|
+
this.dispatcher,
|
|
238
|
+
this.__brands,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
// PlaceholderHandle implements EntityRef but for API consistency return as EntityAccessor
|
|
242
|
+
return this.placeholderCache as unknown as EntityAccessor<TEntity, TSelected>
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Ensures the related entity has a snapshot in the store.
|
|
247
|
+
* If the related entity is embedded in the parent's data (not yet normalized),
|
|
248
|
+
* creates a snapshot from the embedded data.
|
|
249
|
+
*/
|
|
250
|
+
private ensureRelatedEntitySnapshot(id: string): void {
|
|
251
|
+
// Register parent-child relationship for change propagation
|
|
252
|
+
// This needs to happen even if the snapshot already exists
|
|
253
|
+
this.store.registerParentChild(this.entityType, this.entityId, this.targetType, id)
|
|
254
|
+
|
|
255
|
+
// Check if snapshot already exists
|
|
256
|
+
if (this.store.hasEntity(this.targetType, id)) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get embedded data from parent entity
|
|
261
|
+
const parentSnapshot = this.store.getEntitySnapshot(this.entityType, this.entityId)
|
|
262
|
+
if (!parentSnapshot?.data) {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const embeddedData = (parentSnapshot.data as Record<string, unknown>)[this.fieldName]
|
|
267
|
+
if (!embeddedData || typeof embeddedData !== 'object') {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Only use embedded data if its ID matches the expected related entity ID.
|
|
272
|
+
// After $connect changes the relation to a different entity, the parent's embedded
|
|
273
|
+
// data still contains the OLD related entity — using it would store stale data
|
|
274
|
+
// under the new entity's key.
|
|
275
|
+
const embeddedId = (embeddedData as Record<string, unknown>)['id']
|
|
276
|
+
if (embeddedId !== id) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Create snapshot from embedded data
|
|
281
|
+
// Skip notification to avoid triggering React state updates during render
|
|
282
|
+
this.store.setEntityData(
|
|
283
|
+
this.targetType,
|
|
284
|
+
id,
|
|
285
|
+
embeddedData as Record<string, unknown>,
|
|
286
|
+
true, // isServerData
|
|
287
|
+
true, // skipNotify - called during render, data already exists embedded in parent
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Checks if the relation is dirty.
|
|
293
|
+
*/
|
|
294
|
+
get isDirty(): boolean {
|
|
295
|
+
const relation = this.store.getRelation(
|
|
296
|
+
this.entityType,
|
|
297
|
+
this.entityId,
|
|
298
|
+
this.fieldName,
|
|
299
|
+
)
|
|
300
|
+
if (!relation) return false
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
relation.currentId !== relation.serverId ||
|
|
304
|
+
relation.state !== relation.serverState ||
|
|
305
|
+
Object.keys(relation.placeholderData).length > 0
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Connects the relation to an entity.
|
|
311
|
+
*/
|
|
312
|
+
connect(targetId: string): void {
|
|
313
|
+
this.assertNotDisposed()
|
|
314
|
+
this.dispatcher.dispatch(
|
|
315
|
+
connectRelation(this.entityType, this.entityId, this.fieldName, targetId),
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Disconnects the relation.
|
|
321
|
+
*/
|
|
322
|
+
disconnect(): void {
|
|
323
|
+
this.assertNotDisposed()
|
|
324
|
+
this.dispatcher.dispatch(
|
|
325
|
+
disconnectRelation(this.entityType, this.entityId, this.fieldName),
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Marks the relation for deletion.
|
|
331
|
+
*/
|
|
332
|
+
delete(): void {
|
|
333
|
+
this.assertNotDisposed()
|
|
334
|
+
this.dispatcher.dispatch(
|
|
335
|
+
deleteRelation(this.entityType, this.entityId, this.fieldName),
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Resets the relation to server state.
|
|
341
|
+
*/
|
|
342
|
+
reset(): void {
|
|
343
|
+
this.assertNotDisposed()
|
|
344
|
+
this.store.resetRelation(this.entityType, this.entityId, this.fieldName)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Disposes the handle.
|
|
349
|
+
*/
|
|
350
|
+
override dispose(): void {
|
|
351
|
+
super.dispose()
|
|
352
|
+
this.entityHandleCache?.dispose()
|
|
353
|
+
this.entityHandleCache = null
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Type brand - ensures HasOneRef<Author> is not assignable to HasOneRef<Tag>.
|
|
358
|
+
* This is a phantom property that only exists in the type system.
|
|
359
|
+
*/
|
|
360
|
+
get __entityType(): TEntity {
|
|
361
|
+
return undefined as unknown as TEntity
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Gets the list of errors on this relation.
|
|
366
|
+
*/
|
|
367
|
+
get errors(): readonly FieldError[] {
|
|
368
|
+
return this.store.getRelationErrors(this.entityType, this.entityId, this.fieldName)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Checks if this relation has any errors.
|
|
373
|
+
*/
|
|
374
|
+
get hasError(): boolean {
|
|
375
|
+
return this.errors.length > 0
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Adds a client-side error to this relation.
|
|
380
|
+
*/
|
|
381
|
+
addError(error: ErrorInput): void {
|
|
382
|
+
this.assertNotDisposed()
|
|
383
|
+
this.dispatcher.dispatch(
|
|
384
|
+
addRelationError(this.entityType, this.entityId, this.fieldName, createClientError(error)),
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clears all errors from this relation.
|
|
390
|
+
*/
|
|
391
|
+
clearErrors(): void {
|
|
392
|
+
this.assertNotDisposed()
|
|
393
|
+
this.dispatcher.dispatch(
|
|
394
|
+
clearRelationErrors(this.entityType, this.entityId, this.fieldName),
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ==================== Event Subscriptions ====================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Subscribe to connection events.
|
|
402
|
+
*/
|
|
403
|
+
onConnect(listener: EventListener<RelationConnectedEvent>): Unsubscribe {
|
|
404
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
405
|
+
return emitter.onField(
|
|
406
|
+
'relation:connected',
|
|
407
|
+
this.entityType,
|
|
408
|
+
this.entityId,
|
|
409
|
+
this.fieldName,
|
|
410
|
+
listener,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Subscribe to disconnection events.
|
|
416
|
+
*/
|
|
417
|
+
onDisconnect(listener: EventListener<RelationDisconnectedEvent>): Unsubscribe {
|
|
418
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
419
|
+
return emitter.onField(
|
|
420
|
+
'relation:disconnected',
|
|
421
|
+
this.entityType,
|
|
422
|
+
this.entityId,
|
|
423
|
+
this.fieldName,
|
|
424
|
+
listener,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Intercept connection (can cancel or modify target).
|
|
430
|
+
*/
|
|
431
|
+
interceptConnect(interceptor: Interceptor<RelationConnectingEvent>): Unsubscribe {
|
|
432
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
433
|
+
return emitter.interceptField(
|
|
434
|
+
'relation:connecting',
|
|
435
|
+
this.entityType,
|
|
436
|
+
this.entityId,
|
|
437
|
+
this.fieldName,
|
|
438
|
+
interceptor,
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Intercept disconnection (can cancel).
|
|
444
|
+
*/
|
|
445
|
+
interceptDisconnect(interceptor: Interceptor<RelationDisconnectingEvent>): Unsubscribe {
|
|
446
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
447
|
+
return emitter.interceptField(
|
|
448
|
+
'relation:disconnecting',
|
|
449
|
+
this.entityType,
|
|
450
|
+
this.entityId,
|
|
451
|
+
this.fieldName,
|
|
452
|
+
interceptor,
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ==================== EntityRef-compatible Properties ====================
|
|
457
|
+
// These make HasOneAccessor structurally compatible with EntityAccessor
|
|
458
|
+
|
|
459
|
+
/** Raw data snapshot of the related entity - delegates to $entity */
|
|
460
|
+
get $data(): TSelected | null { return this.entity.$data }
|
|
461
|
+
|
|
462
|
+
/** Whether this entity is new - delegates to $entity */
|
|
463
|
+
get $isNew(): boolean { return this.entity.$isNew }
|
|
464
|
+
|
|
465
|
+
/** Server-assigned ID after persistence - delegates to $entity */
|
|
466
|
+
get $persistedId(): string | null { return this.entity.$persistedId }
|
|
467
|
+
|
|
468
|
+
/** Type brand for entity name */
|
|
469
|
+
get __entityName(): string { return this.targetType }
|
|
470
|
+
|
|
471
|
+
/** Clear all errors - delegates to $entity */
|
|
472
|
+
$clearAllErrors(): void { this.entity.$clearAllErrors() }
|
|
473
|
+
|
|
474
|
+
/** Subscribe to any event on the related entity */
|
|
475
|
+
$on<E extends AfterEventTypes>(
|
|
476
|
+
eventType: E,
|
|
477
|
+
listener: EventListener<EventTypeMap[E]>,
|
|
478
|
+
): Unsubscribe {
|
|
479
|
+
return this.entity.$on(eventType, listener)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Intercept any before event on the related entity */
|
|
483
|
+
$intercept<E extends BeforeEventTypes>(
|
|
484
|
+
eventType: E,
|
|
485
|
+
interceptor: Interceptor<EventTypeMap[E]>,
|
|
486
|
+
): Unsubscribe {
|
|
487
|
+
return this.entity.$intercept(eventType, interceptor)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Subscribe to persist success events on the related entity */
|
|
491
|
+
$onPersisted(listener: EventListener<EntityPersistedEvent>): Unsubscribe {
|
|
492
|
+
return this.entity.$onPersisted(listener)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Intercept persist on the related entity */
|
|
496
|
+
$interceptPersisting(interceptor: Interceptor<EntityPersistingEvent>): Unsubscribe {
|
|
497
|
+
return this.entity.$interceptPersisting(interceptor)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
}
|