@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,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEntitySnapshot,
|
|
3
|
+
type EntitySnapshot,
|
|
4
|
+
} from './snapshots.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages entity snapshots — core CRUD for immutable entity data.
|
|
8
|
+
*
|
|
9
|
+
* Entity snapshots are keyed by composite strings (e.g., "entityType:id").
|
|
10
|
+
* All snapshots are immutable (frozen) objects. Mutations create new snapshot instances.
|
|
11
|
+
*
|
|
12
|
+
* Follows the same sub-store pattern as ErrorStore, RelationStore, etc.
|
|
13
|
+
* SnapshotStore delegates entity snapshot operations here.
|
|
14
|
+
*/
|
|
15
|
+
export class EntitySnapshotStore {
|
|
16
|
+
private readonly snapshots = new Map<string, EntitySnapshot>()
|
|
17
|
+
|
|
18
|
+
get(key: string): EntitySnapshot | undefined {
|
|
19
|
+
return this.snapshots.get(key)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
has(key: string): boolean {
|
|
23
|
+
return this.snapshots.has(key)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sets entity data, merging with existing data if present.
|
|
28
|
+
* Returns the new snapshot. Does NOT handle notification or meta updates.
|
|
29
|
+
*/
|
|
30
|
+
setData<T extends object>(
|
|
31
|
+
key: string,
|
|
32
|
+
id: string,
|
|
33
|
+
entityType: string,
|
|
34
|
+
data: T,
|
|
35
|
+
isServerData: boolean,
|
|
36
|
+
): EntitySnapshot<T> {
|
|
37
|
+
const existing = this.snapshots.get(key)
|
|
38
|
+
|
|
39
|
+
const mergedData = existing?.data
|
|
40
|
+
? { ...existing.data, ...data } as T
|
|
41
|
+
: data
|
|
42
|
+
|
|
43
|
+
const serverData = isServerData
|
|
44
|
+
? (existing?.serverData ? { ...existing.serverData, ...data } as T : data)
|
|
45
|
+
: (existing?.serverData as T) ?? mergedData
|
|
46
|
+
|
|
47
|
+
const newSnapshot = createEntitySnapshot(
|
|
48
|
+
id,
|
|
49
|
+
entityType,
|
|
50
|
+
mergedData,
|
|
51
|
+
serverData,
|
|
52
|
+
(existing?.version ?? 0) + 1,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
this.snapshots.set(key, newSnapshot)
|
|
56
|
+
return newSnapshot
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Updates specific fields on an existing entity snapshot.
|
|
61
|
+
* Returns the new snapshot, or undefined if entity doesn't exist.
|
|
62
|
+
*/
|
|
63
|
+
updateFields<T extends object>(
|
|
64
|
+
key: string,
|
|
65
|
+
updates: Partial<T>,
|
|
66
|
+
): EntitySnapshot<T> | undefined {
|
|
67
|
+
const existing = this.snapshots.get(key)
|
|
68
|
+
if (!existing) return undefined
|
|
69
|
+
|
|
70
|
+
const newData = { ...existing.data, ...updates } as T
|
|
71
|
+
const newSnapshot = createEntitySnapshot(
|
|
72
|
+
existing.id,
|
|
73
|
+
existing.entityType,
|
|
74
|
+
newData,
|
|
75
|
+
existing.serverData as T,
|
|
76
|
+
existing.version + 1,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
this.snapshots.set(key, newSnapshot)
|
|
80
|
+
return newSnapshot
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sets a nested field value on an existing entity snapshot.
|
|
85
|
+
* Returns true if the snapshot was updated, false if entity doesn't exist.
|
|
86
|
+
*/
|
|
87
|
+
setFieldValue(key: string, fieldPath: string[], value: unknown): boolean {
|
|
88
|
+
const existing = this.snapshots.get(key)
|
|
89
|
+
if (!existing) return false
|
|
90
|
+
|
|
91
|
+
const newData = setNestedValue({ ...existing.data }, fieldPath, value)
|
|
92
|
+
const newSnapshot = createEntitySnapshot(
|
|
93
|
+
existing.id,
|
|
94
|
+
existing.entityType,
|
|
95
|
+
newData,
|
|
96
|
+
existing.serverData,
|
|
97
|
+
existing.version + 1,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
this.snapshots.set(key, newSnapshot)
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Commits current data as server data (serverData = data).
|
|
106
|
+
*/
|
|
107
|
+
commit(key: string): void {
|
|
108
|
+
const existing = this.snapshots.get(key)
|
|
109
|
+
if (!existing) return
|
|
110
|
+
|
|
111
|
+
const newSnapshot = createEntitySnapshot(
|
|
112
|
+
existing.id,
|
|
113
|
+
existing.entityType,
|
|
114
|
+
existing.data,
|
|
115
|
+
existing.data,
|
|
116
|
+
existing.version + 1,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
this.snapshots.set(key, newSnapshot)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resets data to server data (data = serverData).
|
|
124
|
+
*/
|
|
125
|
+
reset(key: string): void {
|
|
126
|
+
const existing = this.snapshots.get(key)
|
|
127
|
+
if (!existing) return
|
|
128
|
+
|
|
129
|
+
const newSnapshot = createEntitySnapshot(
|
|
130
|
+
existing.id,
|
|
131
|
+
existing.entityType,
|
|
132
|
+
existing.serverData,
|
|
133
|
+
existing.serverData,
|
|
134
|
+
existing.version + 1,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
this.snapshots.set(key, newSnapshot)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Removes an entity snapshot.
|
|
142
|
+
*/
|
|
143
|
+
remove(key: string): void {
|
|
144
|
+
this.snapshots.delete(key)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Bumps the version of an entity snapshot without changing data.
|
|
149
|
+
* Used by SubscriptionManager when child entities change.
|
|
150
|
+
*/
|
|
151
|
+
bumpVersion(key: string): void {
|
|
152
|
+
const existing = this.snapshots.get(key)
|
|
153
|
+
if (!existing) return
|
|
154
|
+
|
|
155
|
+
const newSnapshot = createEntitySnapshot(
|
|
156
|
+
existing.id,
|
|
157
|
+
existing.entityType,
|
|
158
|
+
existing.data,
|
|
159
|
+
existing.serverData,
|
|
160
|
+
existing.version + 1,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
this.snapshots.set(key, newSnapshot)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Commits specific fields by copying their current values to server data.
|
|
168
|
+
*/
|
|
169
|
+
commitFields(key: string, fieldNames: string[]): void {
|
|
170
|
+
const existing = this.snapshots.get(key)
|
|
171
|
+
if (!existing) return
|
|
172
|
+
|
|
173
|
+
const data = existing.data as Record<string, unknown>
|
|
174
|
+
const serverData = existing.serverData as Record<string, unknown>
|
|
175
|
+
|
|
176
|
+
const newServerData = { ...serverData }
|
|
177
|
+
for (const fieldName of fieldNames) {
|
|
178
|
+
if (fieldName in data) {
|
|
179
|
+
newServerData[fieldName] = data[fieldName]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const newSnapshot = createEntitySnapshot(
|
|
184
|
+
existing.id,
|
|
185
|
+
existing.entityType,
|
|
186
|
+
data,
|
|
187
|
+
newServerData,
|
|
188
|
+
existing.version + 1,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
this.snapshots.set(key, newSnapshot)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Exports snapshots for the given keys.
|
|
196
|
+
*/
|
|
197
|
+
exportSnapshots(keys: string[]): Map<string, EntitySnapshot> {
|
|
198
|
+
const result = new Map<string, EntitySnapshot>()
|
|
199
|
+
for (const key of keys) {
|
|
200
|
+
const snapshot = this.snapshots.get(key)
|
|
201
|
+
if (snapshot) {
|
|
202
|
+
result.set(key, snapshot)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return result
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Imports snapshots, overwriting any existing entries.
|
|
210
|
+
* Returns the set of keys that were imported.
|
|
211
|
+
*/
|
|
212
|
+
importSnapshots(snapshots: Map<string, EntitySnapshot>): Set<string> {
|
|
213
|
+
const keys = new Set<string>()
|
|
214
|
+
for (const [key, snapshot] of snapshots) {
|
|
215
|
+
this.snapshots.set(key, snapshot)
|
|
216
|
+
keys.add(key)
|
|
217
|
+
}
|
|
218
|
+
return keys
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
keys(): IterableIterator<string> {
|
|
222
|
+
return this.snapshots.keys()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
clear(): void {
|
|
226
|
+
this.snapshots.clear()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sets a nested value in an object, returning a new object.
|
|
232
|
+
*/
|
|
233
|
+
function setNestedValue<T extends Record<string, unknown>>(
|
|
234
|
+
obj: T,
|
|
235
|
+
path: string[],
|
|
236
|
+
value: unknown,
|
|
237
|
+
): T {
|
|
238
|
+
if (path.length === 0) return obj
|
|
239
|
+
|
|
240
|
+
const result = { ...obj }
|
|
241
|
+
let current: Record<string, unknown> = result
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
244
|
+
const key = path[i]!
|
|
245
|
+
const nextValue = current[key]
|
|
246
|
+
|
|
247
|
+
if (typeof nextValue === 'object' && nextValue !== null) {
|
|
248
|
+
current[key] = { ...nextValue as Record<string, unknown> }
|
|
249
|
+
} else {
|
|
250
|
+
current[key] = {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
current = current[key] as Record<string, unknown>
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const lastKey = path[path.length - 1]!
|
|
257
|
+
current[lastKey] = value
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ErrorState, FieldError } from '../errors/types.js'
|
|
2
|
+
import { filterStickyErrors } from '../errors/types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages error state for fields, entities, and relations.
|
|
6
|
+
*
|
|
7
|
+
* Errors are keyed by composite strings:
|
|
8
|
+
* - Field errors: "entityType:id:fieldName"
|
|
9
|
+
* - Entity errors: "entityType:id"
|
|
10
|
+
* - Relation errors: "entityType:id:relationName"
|
|
11
|
+
*/
|
|
12
|
+
export class ErrorStore {
|
|
13
|
+
/** Field errors keyed by "entityType:id:fieldName" */
|
|
14
|
+
private readonly fieldErrors = new Map<string, ErrorState>()
|
|
15
|
+
|
|
16
|
+
/** Entity-level errors keyed by "entityType:id" */
|
|
17
|
+
private readonly entityErrors = new Map<string, ErrorState>()
|
|
18
|
+
|
|
19
|
+
/** Relation errors keyed by "entityType:id:relationName" */
|
|
20
|
+
private readonly relationErrors = new Map<string, ErrorState>()
|
|
21
|
+
|
|
22
|
+
// ==================== Field Errors ====================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets field errors for a specific field.
|
|
26
|
+
*/
|
|
27
|
+
getFieldErrors(fieldKey: string): readonly FieldError[] {
|
|
28
|
+
return this.fieldErrors.get(fieldKey)?.errors ?? []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Adds an error to a field.
|
|
33
|
+
*/
|
|
34
|
+
addFieldError(fieldKey: string, error: FieldError): void {
|
|
35
|
+
const existing = this.fieldErrors.get(fieldKey)
|
|
36
|
+
const errors = existing ? [...existing.errors, error] : [error]
|
|
37
|
+
this.fieldErrors.set(fieldKey, { errors, version: (existing?.version ?? 0) + 1 })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clears field errors, optionally filtering by source.
|
|
42
|
+
*/
|
|
43
|
+
clearFieldErrors(fieldKey: string, source?: 'client' | 'server'): void {
|
|
44
|
+
const existing = this.fieldErrors.get(fieldKey)
|
|
45
|
+
if (!existing) return
|
|
46
|
+
|
|
47
|
+
if (source === undefined) {
|
|
48
|
+
this.fieldErrors.delete(fieldKey)
|
|
49
|
+
} else {
|
|
50
|
+
const filtered = existing.errors.filter(e => e.source !== source)
|
|
51
|
+
if (filtered.length === 0) {
|
|
52
|
+
this.fieldErrors.delete(fieldKey)
|
|
53
|
+
} else {
|
|
54
|
+
this.fieldErrors.set(fieldKey, { errors: filtered, version: existing.version + 1 })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Clears non-sticky client errors for a field.
|
|
61
|
+
* Called when field value changes.
|
|
62
|
+
* Returns true if any errors were removed.
|
|
63
|
+
*/
|
|
64
|
+
clearNonStickyFieldErrors(fieldKey: string): boolean {
|
|
65
|
+
const existing = this.fieldErrors.get(fieldKey)
|
|
66
|
+
if (!existing) return false
|
|
67
|
+
|
|
68
|
+
const filtered = filterStickyErrors(existing.errors)
|
|
69
|
+
if (filtered.length === existing.errors.length) return false
|
|
70
|
+
|
|
71
|
+
if (filtered.length === 0) {
|
|
72
|
+
this.fieldErrors.delete(fieldKey)
|
|
73
|
+
} else {
|
|
74
|
+
this.fieldErrors.set(fieldKey, { errors: filtered, version: existing.version + 1 })
|
|
75
|
+
}
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ==================== Entity Errors ====================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets entity-level errors.
|
|
83
|
+
*/
|
|
84
|
+
getEntityErrors(entityKey: string): readonly FieldError[] {
|
|
85
|
+
return this.entityErrors.get(entityKey)?.errors ?? []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Adds an entity-level error.
|
|
90
|
+
*/
|
|
91
|
+
addEntityError(entityKey: string, error: FieldError): void {
|
|
92
|
+
const existing = this.entityErrors.get(entityKey)
|
|
93
|
+
const errors = existing ? [...existing.errors, error] : [error]
|
|
94
|
+
this.entityErrors.set(entityKey, { errors, version: (existing?.version ?? 0) + 1 })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Clears entity-level errors, optionally filtering by source.
|
|
99
|
+
*/
|
|
100
|
+
clearEntityErrors(entityKey: string, source?: 'client' | 'server'): void {
|
|
101
|
+
const existing = this.entityErrors.get(entityKey)
|
|
102
|
+
if (!existing) return
|
|
103
|
+
|
|
104
|
+
if (source === undefined) {
|
|
105
|
+
this.entityErrors.delete(entityKey)
|
|
106
|
+
} else {
|
|
107
|
+
const filtered = existing.errors.filter(e => e.source !== source)
|
|
108
|
+
if (filtered.length === 0) {
|
|
109
|
+
this.entityErrors.delete(entityKey)
|
|
110
|
+
} else {
|
|
111
|
+
this.entityErrors.set(entityKey, { errors: filtered, version: existing.version + 1 })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ==================== Relation Errors ====================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Gets relation errors.
|
|
120
|
+
*/
|
|
121
|
+
getRelationErrors(relationKey: string): readonly FieldError[] {
|
|
122
|
+
return this.relationErrors.get(relationKey)?.errors ?? []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Adds a relation error.
|
|
127
|
+
*/
|
|
128
|
+
addRelationError(relationKey: string, error: FieldError): void {
|
|
129
|
+
const existing = this.relationErrors.get(relationKey)
|
|
130
|
+
const errors = existing ? [...existing.errors, error] : [error]
|
|
131
|
+
this.relationErrors.set(relationKey, { errors, version: (existing?.version ?? 0) + 1 })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clears relation errors, optionally filtering by source.
|
|
136
|
+
*/
|
|
137
|
+
clearRelationErrors(relationKey: string, source?: 'client' | 'server'): void {
|
|
138
|
+
const existing = this.relationErrors.get(relationKey)
|
|
139
|
+
if (!existing) return
|
|
140
|
+
|
|
141
|
+
if (source === undefined) {
|
|
142
|
+
this.relationErrors.delete(relationKey)
|
|
143
|
+
} else {
|
|
144
|
+
const filtered = existing.errors.filter(e => e.source !== source)
|
|
145
|
+
if (filtered.length === 0) {
|
|
146
|
+
this.relationErrors.delete(relationKey)
|
|
147
|
+
} else {
|
|
148
|
+
this.relationErrors.set(relationKey, { errors: filtered, version: existing.version + 1 })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ==================== Bulk Operations ====================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Clears all server errors for an entity (entity-level, fields, and relations).
|
|
157
|
+
* Called before persist to clear stale server errors.
|
|
158
|
+
*/
|
|
159
|
+
clearAllServerErrors(entityKey: string, keyPrefix: string): void {
|
|
160
|
+
this.clearEntityErrors(entityKey, 'server')
|
|
161
|
+
|
|
162
|
+
for (const key of this.fieldErrors.keys()) {
|
|
163
|
+
if (key.startsWith(keyPrefix)) {
|
|
164
|
+
this.clearFieldErrors(key, 'server')
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const key of this.relationErrors.keys()) {
|
|
169
|
+
if (key.startsWith(keyPrefix)) {
|
|
170
|
+
this.clearRelationErrors(key, 'server')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Clears all errors for an entity (entity-level, fields, and relations).
|
|
177
|
+
*/
|
|
178
|
+
clearAllErrors(entityKey: string, keyPrefix: string): void {
|
|
179
|
+
this.entityErrors.delete(entityKey)
|
|
180
|
+
|
|
181
|
+
for (const key of [...this.fieldErrors.keys()]) {
|
|
182
|
+
if (key.startsWith(keyPrefix)) {
|
|
183
|
+
this.fieldErrors.delete(key)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const key of [...this.relationErrors.keys()]) {
|
|
188
|
+
if (key.startsWith(keyPrefix)) {
|
|
189
|
+
this.relationErrors.delete(key)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Checks if an entity has any client errors (entity-level, fields, or relations).
|
|
196
|
+
* Used to block persist when validation errors exist.
|
|
197
|
+
*/
|
|
198
|
+
hasClientErrors(entityKey: string, keyPrefix: string): boolean {
|
|
199
|
+
const entityErrs = this.entityErrors.get(entityKey)
|
|
200
|
+
if (entityErrs?.errors.some(e => e.source === 'client')) {
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const [key, state] of this.fieldErrors) {
|
|
205
|
+
if (key.startsWith(keyPrefix) && state.errors.some(e => e.source === 'client')) {
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const [key, state] of this.relationErrors) {
|
|
211
|
+
if (key.startsWith(keyPrefix) && state.errors.some(e => e.source === 'client')) {
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Checks if an entity has any errors at all.
|
|
221
|
+
*/
|
|
222
|
+
hasAnyErrors(entityKey: string, keyPrefix: string): boolean {
|
|
223
|
+
const entityErrs = this.entityErrors.get(entityKey)
|
|
224
|
+
if (entityErrs && entityErrs.errors.length > 0) {
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const [key, state] of this.fieldErrors) {
|
|
229
|
+
if (key.startsWith(keyPrefix) && state.errors.length > 0) {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const [key, state] of this.relationErrors) {
|
|
235
|
+
if (key.startsWith(keyPrefix) && state.errors.length > 0) {
|
|
236
|
+
return true
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Clears all error data.
|
|
245
|
+
*/
|
|
246
|
+
clear(): void {
|
|
247
|
+
this.fieldErrors.clear()
|
|
248
|
+
this.entityErrors.clear()
|
|
249
|
+
this.relationErrors.clear()
|
|
250
|
+
}
|
|
251
|
+
}
|