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