@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,567 @@
|
|
|
1
|
+
import { type SnapshotStore, isPersistedId, isTempId } from '../store/SnapshotStore.js'
|
|
2
|
+
import type { EntitySnapshot } from '../store/snapshots.js'
|
|
3
|
+
import type { MutationSchemaProvider } from './MutationSchemaProvider.js'
|
|
4
|
+
import type { MutationDataCollector } from './types.js'
|
|
5
|
+
import { deepEqual } from '../utils/deepEqual.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result of collecting mutations for an entity
|
|
9
|
+
*/
|
|
10
|
+
export interface EntityMutationResult {
|
|
11
|
+
/** Type of mutation: create, update, or delete */
|
|
12
|
+
type: 'create' | 'update' | 'delete'
|
|
13
|
+
/** Entity type name */
|
|
14
|
+
entityType: string
|
|
15
|
+
/** Entity ID (may be temp ID for creates) */
|
|
16
|
+
entityId: string
|
|
17
|
+
/** The mutation data (for create/update) */
|
|
18
|
+
data?: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MutationCollector builds Contember-compatible mutation input
|
|
23
|
+
* by collecting changes from SnapshotStore including:
|
|
24
|
+
* - Scalar field changes
|
|
25
|
+
* - Has-one relation operations (connect, disconnect, delete, create, update)
|
|
26
|
+
* - Has-many relation operations (connect, disconnect, delete, create, update)
|
|
27
|
+
* - Entity creates and deletes
|
|
28
|
+
*
|
|
29
|
+
* This is a unified implementation that works with any schema provider
|
|
30
|
+
* implementing MutationSchemaProvider interface (SchemaRegistry, Contember SchemaNames via adapter).
|
|
31
|
+
*/
|
|
32
|
+
export class MutationCollector implements MutationDataCollector {
|
|
33
|
+
private excludedEntityIds: ReadonlySet<string> = new Set()
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly store: SnapshotStore,
|
|
37
|
+
private readonly schemaProvider: MutationSchemaProvider,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sets entity IDs that should be excluded from nested mutation generation.
|
|
42
|
+
* These entities get their own top-level mutations, so nested updates are skipped
|
|
43
|
+
* to avoid duplicate changes.
|
|
44
|
+
*/
|
|
45
|
+
setExcludedEntities(ids: ReadonlySet<string>): void {
|
|
46
|
+
this.excludedEntityIds = ids
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ==================== Main Collection Methods ====================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Collects all changes for an entity into appropriate mutation format.
|
|
53
|
+
* Returns the mutation result or null if no changes/entity doesn't exist.
|
|
54
|
+
*/
|
|
55
|
+
collectMutation(
|
|
56
|
+
entityType: string,
|
|
57
|
+
entityId: string,
|
|
58
|
+
): EntityMutationResult | null {
|
|
59
|
+
// Check if entity is scheduled for deletion
|
|
60
|
+
if (this.store.isScheduledForDeletion(entityType, entityId)) {
|
|
61
|
+
// Only delete if it exists on server
|
|
62
|
+
if (this.store.existsOnServer(entityType, entityId)) {
|
|
63
|
+
return {
|
|
64
|
+
type: 'delete',
|
|
65
|
+
entityType,
|
|
66
|
+
entityId,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const snapshot = this.store.getEntitySnapshot(entityType, entityId)
|
|
73
|
+
if (!snapshot) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if entity exists on server
|
|
78
|
+
const existsOnServer = this.store.existsOnServer(entityType, entityId)
|
|
79
|
+
|
|
80
|
+
if (!existsOnServer) {
|
|
81
|
+
// This is a create
|
|
82
|
+
const createData = this.collectCreateData(entityType, entityId)
|
|
83
|
+
if (createData && Object.keys(createData).length > 0) {
|
|
84
|
+
return {
|
|
85
|
+
type: 'create',
|
|
86
|
+
entityType,
|
|
87
|
+
entityId,
|
|
88
|
+
data: createData,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// This is an update
|
|
95
|
+
const updateData = this.collectUpdateData(entityType, entityId)
|
|
96
|
+
if (updateData && Object.keys(updateData).length > 0) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'update',
|
|
99
|
+
entityType,
|
|
100
|
+
entityId,
|
|
101
|
+
data: updateData,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Collects all changes for an entity into Contember UpdateDataInput format.
|
|
110
|
+
* Returns the mutation data or null if no changes.
|
|
111
|
+
*/
|
|
112
|
+
collectUpdateData(
|
|
113
|
+
entityType: string,
|
|
114
|
+
entityId: string,
|
|
115
|
+
): Record<string, unknown> | null {
|
|
116
|
+
const snapshot = this.store.getEntitySnapshot(entityType, entityId)
|
|
117
|
+
if (!snapshot) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!this.schemaProvider.hasEntity(entityType)) {
|
|
122
|
+
throw new Error(`Entity type '${entityType}' not found in schema`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const mutation: Record<string, unknown> = {}
|
|
126
|
+
|
|
127
|
+
// Collect scalar field changes
|
|
128
|
+
this.collectScalarChanges(entityType, snapshot, mutation)
|
|
129
|
+
|
|
130
|
+
// Collect relation changes
|
|
131
|
+
this.collectRelationChanges(entityType, entityId, mutation)
|
|
132
|
+
|
|
133
|
+
return Object.keys(mutation).length > 0 ? mutation : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Collects data for creating a new entity.
|
|
138
|
+
* Returns the create data or null if empty.
|
|
139
|
+
*/
|
|
140
|
+
collectCreateData(
|
|
141
|
+
entityType: string,
|
|
142
|
+
entityId: string,
|
|
143
|
+
): Record<string, unknown> | null {
|
|
144
|
+
const snapshot = this.store.getEntitySnapshot(entityType, entityId)
|
|
145
|
+
if (!snapshot) {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!this.schemaProvider.hasEntity(entityType)) {
|
|
150
|
+
throw new Error(`Entity type '${entityType}' not found in schema`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = snapshot.data as Record<string, unknown>
|
|
154
|
+
const createData: Record<string, unknown> = {}
|
|
155
|
+
|
|
156
|
+
// Collect scalar field values
|
|
157
|
+
const scalarFields = this.schemaProvider.getScalarFields(entityType)
|
|
158
|
+
for (const fieldName of scalarFields) {
|
|
159
|
+
if (fieldName === 'id') continue // ID is auto-generated
|
|
160
|
+
|
|
161
|
+
const value = data[fieldName]
|
|
162
|
+
if (value !== undefined && value !== null) {
|
|
163
|
+
createData[fieldName] = value
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Collect relation values
|
|
168
|
+
const relationFields = this.schemaProvider.getRelationFields(entityType)
|
|
169
|
+
for (const fieldName of relationFields) {
|
|
170
|
+
const value = data[fieldName]
|
|
171
|
+
const relationType = this.schemaProvider.getRelationType(entityType, fieldName)
|
|
172
|
+
|
|
173
|
+
if (relationType === 'hasOne') {
|
|
174
|
+
const relationOp = this.collectCreateOneRelation(value)
|
|
175
|
+
if (relationOp !== null) {
|
|
176
|
+
createData[fieldName] = relationOp
|
|
177
|
+
}
|
|
178
|
+
} else if (relationType === 'hasMany') {
|
|
179
|
+
const relationOps = this.collectCreateManyRelation(value)
|
|
180
|
+
if (relationOps !== null && relationOps.length > 0) {
|
|
181
|
+
createData[fieldName] = relationOps
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return Object.keys(createData).length > 0 ? createData : null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Collects mutation data for specific fields only.
|
|
191
|
+
* Returns the mutation data containing only the specified fields.
|
|
192
|
+
*/
|
|
193
|
+
collectFieldsData(
|
|
194
|
+
entityType: string,
|
|
195
|
+
entityId: string,
|
|
196
|
+
fieldNames: readonly string[],
|
|
197
|
+
): Record<string, unknown> | null {
|
|
198
|
+
const snapshot = this.store.getEntitySnapshot(entityType, entityId)
|
|
199
|
+
if (!snapshot) {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!this.schemaProvider.hasEntity(entityType)) {
|
|
204
|
+
throw new Error(`Entity type '${entityType}' not found in schema`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const data = snapshot.data as Record<string, unknown>
|
|
208
|
+
const serverData = snapshot.serverData as Record<string, unknown>
|
|
209
|
+
const result: Record<string, unknown> = {}
|
|
210
|
+
|
|
211
|
+
const scalarFields = new Set(this.schemaProvider.getScalarFields(entityType))
|
|
212
|
+
|
|
213
|
+
for (const fieldName of fieldNames) {
|
|
214
|
+
if (scalarFields.has(fieldName)) {
|
|
215
|
+
// Scalar field - include if changed
|
|
216
|
+
const currentValue = data[fieldName]
|
|
217
|
+
const serverValue = serverData[fieldName]
|
|
218
|
+
if (!deepEqual(currentValue, serverValue)) {
|
|
219
|
+
result[fieldName] = currentValue
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const relationType = this.schemaProvider.getRelationType(entityType, fieldName)
|
|
223
|
+
if (relationType === 'hasOne') {
|
|
224
|
+
const operation = this.collectHasOneOperation(entityType, entityId, fieldName)
|
|
225
|
+
if (operation !== null) {
|
|
226
|
+
result[fieldName] = operation
|
|
227
|
+
}
|
|
228
|
+
} else if (relationType === 'hasMany') {
|
|
229
|
+
const operations = this.collectHasManyOperations(entityType, entityId, fieldName)
|
|
230
|
+
if (operations !== null && operations.length > 0) {
|
|
231
|
+
result[fieldName] = operations
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Object.keys(result).length > 0 ? result : null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ==================== Scalar Changes ====================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Collects scalar field changes by comparing data to serverData.
|
|
244
|
+
*/
|
|
245
|
+
private collectScalarChanges(
|
|
246
|
+
entityType: string,
|
|
247
|
+
snapshot: EntitySnapshot,
|
|
248
|
+
mutation: Record<string, unknown>,
|
|
249
|
+
): void {
|
|
250
|
+
const data = snapshot.data as Record<string, unknown>
|
|
251
|
+
const serverData = snapshot.serverData as Record<string, unknown>
|
|
252
|
+
|
|
253
|
+
const scalarFields = this.schemaProvider.getScalarFields(entityType)
|
|
254
|
+
for (const fieldName of scalarFields) {
|
|
255
|
+
const currentValue = data[fieldName]
|
|
256
|
+
const serverValue = serverData[fieldName]
|
|
257
|
+
|
|
258
|
+
if (!deepEqual(currentValue, serverValue)) {
|
|
259
|
+
mutation[fieldName] = currentValue
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ==================== Relation Changes ====================
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Collects has-one and has-many relation changes.
|
|
268
|
+
*/
|
|
269
|
+
private collectRelationChanges(
|
|
270
|
+
entityType: string,
|
|
271
|
+
entityId: string,
|
|
272
|
+
mutation: Record<string, unknown>,
|
|
273
|
+
): void {
|
|
274
|
+
const relationFields = this.schemaProvider.getRelationFields(entityType)
|
|
275
|
+
|
|
276
|
+
for (const fieldName of relationFields) {
|
|
277
|
+
const relationType = this.schemaProvider.getRelationType(entityType, fieldName)
|
|
278
|
+
|
|
279
|
+
if (relationType === 'hasOne') {
|
|
280
|
+
const operation = this.collectHasOneOperation(entityType, entityId, fieldName)
|
|
281
|
+
if (operation !== null) {
|
|
282
|
+
mutation[fieldName] = operation
|
|
283
|
+
}
|
|
284
|
+
} else if (relationType === 'hasMany') {
|
|
285
|
+
const operations = this.collectHasManyOperations(entityType, entityId, fieldName)
|
|
286
|
+
if (operations !== null && operations.length > 0) {
|
|
287
|
+
mutation[fieldName] = operations
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Collects has-one relation operation.
|
|
295
|
+
* Returns the operation object or null if no change.
|
|
296
|
+
*/
|
|
297
|
+
private collectHasOneOperation(
|
|
298
|
+
entityType: string,
|
|
299
|
+
entityId: string,
|
|
300
|
+
fieldName: string,
|
|
301
|
+
): Record<string, unknown> | null {
|
|
302
|
+
const relationState = this.store.getRelation(entityType, entityId, fieldName)
|
|
303
|
+
if (!relationState) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { state, serverState, currentId, serverId, placeholderData } = relationState
|
|
308
|
+
|
|
309
|
+
switch (state) {
|
|
310
|
+
case 'connected':
|
|
311
|
+
if (currentId !== serverId) {
|
|
312
|
+
// Check if current entity exists on server
|
|
313
|
+
if (currentId && this.isExistingEntity(currentId)) {
|
|
314
|
+
return { connect: { id: currentId } }
|
|
315
|
+
} else if (currentId) {
|
|
316
|
+
// Entity doesn't exist - might need to create it
|
|
317
|
+
// For now, just connect (the entity should be created separately)
|
|
318
|
+
return { connect: { id: currentId } }
|
|
319
|
+
}
|
|
320
|
+
} else if (currentId && serverId && currentId === serverId) {
|
|
321
|
+
// Skip if entity has its own top-level mutation
|
|
322
|
+
if (this.excludedEntityIds.has(currentId)) {
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
// Same entity - check if we need to update it
|
|
326
|
+
const targetType = this.schemaProvider.getRelationTarget(entityType, fieldName)
|
|
327
|
+
if (targetType) {
|
|
328
|
+
const nestedChanges = this.collectUpdateData(targetType, currentId)
|
|
329
|
+
if (nestedChanges) {
|
|
330
|
+
return { update: nestedChanges }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null
|
|
335
|
+
|
|
336
|
+
case 'disconnected':
|
|
337
|
+
// Only emit disconnect if server had a connection
|
|
338
|
+
if (serverState === 'connected' && serverId !== null) {
|
|
339
|
+
return { disconnect: true }
|
|
340
|
+
}
|
|
341
|
+
return null
|
|
342
|
+
|
|
343
|
+
case 'deleted':
|
|
344
|
+
// Delete the related entity
|
|
345
|
+
return { delete: true }
|
|
346
|
+
|
|
347
|
+
case 'creating':
|
|
348
|
+
// Create new entity with placeholder data
|
|
349
|
+
if (Object.keys(placeholderData).length > 0) {
|
|
350
|
+
const createData = this.processNestedData(placeholderData)
|
|
351
|
+
return { create: createData }
|
|
352
|
+
}
|
|
353
|
+
return null
|
|
354
|
+
|
|
355
|
+
default:
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Collects has-one relation for create mutation.
|
|
362
|
+
*/
|
|
363
|
+
private collectCreateOneRelation(
|
|
364
|
+
value: unknown,
|
|
365
|
+
): Record<string, unknown> | null {
|
|
366
|
+
if (value === null || value === undefined) {
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (typeof value === 'object') {
|
|
371
|
+
const obj = value as Record<string, unknown>
|
|
372
|
+
const id = obj['id'] as string | undefined
|
|
373
|
+
|
|
374
|
+
if (id && !isTempId(id) && this.isExistingEntity(id)) {
|
|
375
|
+
// Connect to existing entity
|
|
376
|
+
return { connect: { id } }
|
|
377
|
+
} else if (id && isTempId(id)) {
|
|
378
|
+
// Create new entity (temp ID)
|
|
379
|
+
const createData = { ...obj }
|
|
380
|
+
delete createData['id']
|
|
381
|
+
return { create: this.processNestedData(createData) }
|
|
382
|
+
} else if (!id) {
|
|
383
|
+
// Create new entity (no ID)
|
|
384
|
+
return { create: this.processNestedData(obj) }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ==================== Has-Many Relation Changes ====================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Collects has-many relation operations.
|
|
395
|
+
* Uses StoredHasManyState as the single source of truth for all operations.
|
|
396
|
+
* Returns array of operations or null if no changes.
|
|
397
|
+
*/
|
|
398
|
+
private collectHasManyOperations(
|
|
399
|
+
entityType: string,
|
|
400
|
+
entityId: string,
|
|
401
|
+
fieldName: string,
|
|
402
|
+
): Array<Record<string, unknown>> | null {
|
|
403
|
+
const hasManyState = this.store.getHasMany(entityType, entityId, fieldName)
|
|
404
|
+
if (!hasManyState) return null
|
|
405
|
+
|
|
406
|
+
const operations: Array<Record<string, unknown>> = []
|
|
407
|
+
const targetType = this.schemaProvider.getRelationTarget(entityType, fieldName)
|
|
408
|
+
|
|
409
|
+
// Planned removals -> disconnect/delete
|
|
410
|
+
for (const [removedId, removalType] of hasManyState.plannedRemovals) {
|
|
411
|
+
if (removalType === 'delete') {
|
|
412
|
+
operations.push({ delete: { id: removedId }, alias: removedId })
|
|
413
|
+
} else {
|
|
414
|
+
operations.push({ disconnect: { id: removedId }, alias: removedId })
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Created entities -> create (using entity snapshot data)
|
|
419
|
+
if (targetType) {
|
|
420
|
+
for (const tempId of hasManyState.createdEntities) {
|
|
421
|
+
const itemSnapshot = this.store.getEntitySnapshot(targetType, tempId)
|
|
422
|
+
if (itemSnapshot) {
|
|
423
|
+
const createData = { ...itemSnapshot.data as Record<string, unknown> }
|
|
424
|
+
delete createData['id']
|
|
425
|
+
if (Object.keys(createData).length > 0) {
|
|
426
|
+
operations.push({ create: this.processNestedData(createData), alias: tempId })
|
|
427
|
+
} else {
|
|
428
|
+
operations.push({ create: {}, alias: tempId })
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Planned connections (minus created entities) -> connect
|
|
435
|
+
for (const connectedId of hasManyState.plannedConnections) {
|
|
436
|
+
if (hasManyState.createdEntities.has(connectedId)) continue
|
|
437
|
+
operations.push({ connect: { id: connectedId }, alias: connectedId })
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Server items that aren't removed -> check for updates via entity snapshots
|
|
441
|
+
if (targetType) {
|
|
442
|
+
for (const itemId of hasManyState.serverIds) {
|
|
443
|
+
if (hasManyState.plannedRemovals.has(itemId)) continue
|
|
444
|
+
if (this.excludedEntityIds.has(itemId)) continue
|
|
445
|
+
|
|
446
|
+
const itemSnapshot = this.store.getEntitySnapshot(targetType, itemId)
|
|
447
|
+
if (!itemSnapshot) continue
|
|
448
|
+
|
|
449
|
+
const itemData = itemSnapshot.data as Record<string, unknown>
|
|
450
|
+
const itemServerData = itemSnapshot.serverData as Record<string, unknown>
|
|
451
|
+
|
|
452
|
+
const changes: Record<string, unknown> = {}
|
|
453
|
+
for (const [key, value] of Object.entries(itemData)) {
|
|
454
|
+
if (key === 'id') continue
|
|
455
|
+
if (!deepEqual(value, itemServerData[key])) {
|
|
456
|
+
changes[key] = value
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (Object.keys(changes).length > 0) {
|
|
461
|
+
operations.push({
|
|
462
|
+
update: {
|
|
463
|
+
by: { id: itemId },
|
|
464
|
+
data: changes,
|
|
465
|
+
},
|
|
466
|
+
alias: itemId,
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return operations.length > 0 ? operations : null
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Collects has-many relations for create mutation.
|
|
477
|
+
*/
|
|
478
|
+
private collectCreateManyRelation(
|
|
479
|
+
value: unknown,
|
|
480
|
+
): Array<Record<string, unknown>> | null {
|
|
481
|
+
if (!Array.isArray(value)) return null
|
|
482
|
+
|
|
483
|
+
const operations: Array<Record<string, unknown>> = []
|
|
484
|
+
|
|
485
|
+
for (const item of value) {
|
|
486
|
+
if (typeof item !== 'object' || item === null) continue
|
|
487
|
+
|
|
488
|
+
const obj = item as Record<string, unknown>
|
|
489
|
+
const id = obj['id'] as string | undefined
|
|
490
|
+
|
|
491
|
+
if (id && !isTempId(id) && this.isExistingEntity(id)) {
|
|
492
|
+
// Connect to existing entity
|
|
493
|
+
operations.push({ connect: { id }, alias: id })
|
|
494
|
+
} else {
|
|
495
|
+
// Create new entity
|
|
496
|
+
const createData = { ...obj }
|
|
497
|
+
delete createData['id']
|
|
498
|
+
if (Object.keys(createData).length > 0) {
|
|
499
|
+
operations.push({ create: this.processNestedData(createData), alias: id })
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return operations.length > 0 ? operations : null
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ==================== Helper Methods ====================
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Processes nested data to handle relation objects.
|
|
511
|
+
* Converts nested objects with 'id' to connect operations.
|
|
512
|
+
*/
|
|
513
|
+
private processNestedData(data: Record<string, unknown>): Record<string, unknown> {
|
|
514
|
+
const result: Record<string, unknown> = {}
|
|
515
|
+
|
|
516
|
+
for (const [key, value] of Object.entries(data)) {
|
|
517
|
+
if (value === null || value === undefined) {
|
|
518
|
+
result[key] = value
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (Array.isArray(value)) {
|
|
523
|
+
// Has-many: convert each item
|
|
524
|
+
result[key] = value.map(item => {
|
|
525
|
+
if (typeof item === 'object' && item !== null) {
|
|
526
|
+
const itemObj = item as Record<string, unknown>
|
|
527
|
+
if (itemObj['id'] && !isTempId(String(itemObj['id']))) {
|
|
528
|
+
// Existing item - connect
|
|
529
|
+
return { connect: { id: itemObj['id'] } }
|
|
530
|
+
} else {
|
|
531
|
+
// New item - create
|
|
532
|
+
const createData = { ...itemObj }
|
|
533
|
+
delete createData['id']
|
|
534
|
+
return { create: this.processNestedData(createData) }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return item
|
|
538
|
+
})
|
|
539
|
+
} else if (typeof value === 'object') {
|
|
540
|
+
const obj = value as Record<string, unknown>
|
|
541
|
+
if (obj['id'] && !isTempId(String(obj['id']))) {
|
|
542
|
+
// Has-one with existing entity - connect
|
|
543
|
+
result[key] = { connect: { id: obj['id'] } }
|
|
544
|
+
} else if (obj['id'] && isTempId(String(obj['id']))) {
|
|
545
|
+
// Has-one with temp entity - create
|
|
546
|
+
const createData = { ...obj }
|
|
547
|
+
delete createData['id']
|
|
548
|
+
result[key] = { create: this.processNestedData(createData) }
|
|
549
|
+
} else {
|
|
550
|
+
// Regular nested object
|
|
551
|
+
result[key] = value
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
result[key] = value
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return result
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Checks if an entity ID represents an existing entity (persisted, not temp or placeholder).
|
|
563
|
+
*/
|
|
564
|
+
private isExistingEntity(id: string): boolean {
|
|
565
|
+
return isPersistedId(id)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for schema providers used by MutationCollector.
|
|
3
|
+
* Abstracts schema access to allow different schema implementations
|
|
4
|
+
* (SchemaRegistry, Contember SchemaNames) to be used interchangeably.
|
|
5
|
+
*/
|
|
6
|
+
export interface MutationSchemaProvider {
|
|
7
|
+
/**
|
|
8
|
+
* Gets all scalar (non-relation) field names for an entity.
|
|
9
|
+
*/
|
|
10
|
+
getScalarFields(entityType: string): readonly string[]
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets all relation field names for an entity.
|
|
14
|
+
*/
|
|
15
|
+
getRelationFields(entityType: string): readonly string[]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gets the relation type for a field.
|
|
19
|
+
* Returns undefined if the field is not a relation.
|
|
20
|
+
*/
|
|
21
|
+
getRelationType(entityType: string, fieldName: string): 'hasOne' | 'hasMany' | undefined
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the target entity type for a relation field.
|
|
25
|
+
* Returns undefined if the field is not a relation.
|
|
26
|
+
*/
|
|
27
|
+
getRelationTarget(entityType: string, fieldName: string): string | undefined
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if an entity type exists in the schema.
|
|
31
|
+
*/
|
|
32
|
+
hasEntity(entityType: string): boolean
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { BatchPersister, type BatchPersisterOptions } from './BatchPersister.js'
|
|
2
|
+
export { ChangeRegistry, type DirtyEntity } from './ChangeRegistry.js'
|
|
3
|
+
export { MutationCollector, type EntityMutationResult } from './MutationCollector.js'
|
|
4
|
+
export type { MutationSchemaProvider } from './MutationSchemaProvider.js'
|
|
5
|
+
export { ContemberSchemaMutationAdapter } from './ContemberSchemaMutationAdapter.js'
|
|
6
|
+
export type {
|
|
7
|
+
MutationDataCollector,
|
|
8
|
+
AllScope,
|
|
9
|
+
EntityScope,
|
|
10
|
+
FieldsScope,
|
|
11
|
+
RelationScope,
|
|
12
|
+
CustomScope,
|
|
13
|
+
PersistScope,
|
|
14
|
+
FieldPersistResult,
|
|
15
|
+
EntityPersistResult,
|
|
16
|
+
PersistError,
|
|
17
|
+
PersistenceResult,
|
|
18
|
+
TransactionMutation,
|
|
19
|
+
TransactionMutationResult,
|
|
20
|
+
TransactionResult,
|
|
21
|
+
BatchPersistOptions,
|
|
22
|
+
UpdateMode,
|
|
23
|
+
} from './types.js'
|