@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.
Files changed (234) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +40 -0
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
  3. package/dist/adapter/MockAdapter.d.ts +81 -0
  4. package/dist/adapter/MockAdapter.d.ts.map +1 -0
  5. package/dist/adapter/MockQueryEngine.d.ts +43 -0
  6. package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
  7. package/dist/adapter/index.d.ts +5 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/types.d.ts +184 -0
  10. package/dist/adapter/types.d.ts.map +1 -0
  11. package/dist/brand/ComponentBrand.d.ts +25 -0
  12. package/dist/brand/ComponentBrand.d.ts.map +1 -0
  13. package/dist/contember/index.d.ts +10 -0
  14. package/dist/contember/index.d.ts.map +1 -0
  15. package/dist/core/ActionDispatcher.d.ts +64 -0
  16. package/dist/core/ActionDispatcher.d.ts.map +1 -0
  17. package/dist/core/Disposable.d.ts +57 -0
  18. package/dist/core/Disposable.d.ts.map +1 -0
  19. package/dist/core/EntityLoader.d.ts +80 -0
  20. package/dist/core/EntityLoader.d.ts.map +1 -0
  21. package/dist/core/PersistenceManager.d.ts +126 -0
  22. package/dist/core/PersistenceManager.d.ts.map +1 -0
  23. package/dist/core/SelectionResolver.d.ts +21 -0
  24. package/dist/core/SelectionResolver.d.ts.map +1 -0
  25. package/dist/core/actionClassification.d.ts +48 -0
  26. package/dist/core/actionClassification.d.ts.map +1 -0
  27. package/dist/core/actions.d.ts +301 -0
  28. package/dist/core/actions.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/dataview/filterHandlers.d.ts +18 -0
  32. package/dist/dataview/filterHandlers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +3 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/types.d.ts +164 -0
  36. package/dist/dataview/types.d.ts.map +1 -0
  37. package/dist/errors/UnfetchedFieldError.d.ts +11 -0
  38. package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
  39. package/dist/errors/errorPathResolver.d.ts +52 -0
  40. package/dist/errors/errorPathResolver.d.ts.map +1 -0
  41. package/dist/errors/index.d.ts +5 -0
  42. package/dist/errors/index.d.ts.map +1 -0
  43. package/dist/errors/pathMapper.d.ts +42 -0
  44. package/dist/errors/pathMapper.d.ts.map +1 -0
  45. package/dist/errors/types.d.ts +131 -0
  46. package/dist/errors/types.d.ts.map +1 -0
  47. package/dist/events/EventEmitter.d.ts +97 -0
  48. package/dist/events/EventEmitter.d.ts.map +1 -0
  49. package/dist/events/eventFactory.d.ts +40 -0
  50. package/dist/events/eventFactory.d.ts.map +1 -0
  51. package/dist/events/index.d.ts +15 -0
  52. package/dist/events/index.d.ts.map +1 -0
  53. package/dist/events/types.d.ts +264 -0
  54. package/dist/events/types.d.ts.map +1 -0
  55. package/dist/handles/BaseHandle.d.ts +64 -0
  56. package/dist/handles/BaseHandle.d.ts.map +1 -0
  57. package/dist/handles/EntityHandle.d.ts +208 -0
  58. package/dist/handles/EntityHandle.d.ts.map +1 -0
  59. package/dist/handles/FieldHandle.d.ts +111 -0
  60. package/dist/handles/FieldHandle.d.ts.map +1 -0
  61. package/dist/handles/HasManyListHandle.d.ts +166 -0
  62. package/dist/handles/HasManyListHandle.d.ts.map +1 -0
  63. package/dist/handles/HasOneHandle.d.ts +168 -0
  64. package/dist/handles/HasOneHandle.d.ts.map +1 -0
  65. package/dist/handles/PlaceholderHandle.d.ts +117 -0
  66. package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
  67. package/dist/handles/index.d.ts +8 -0
  68. package/dist/handles/index.d.ts.map +1 -0
  69. package/dist/handles/proxyFactory.d.ts +66 -0
  70. package/dist/handles/proxyFactory.d.ts.map +1 -0
  71. package/dist/handles/types.d.ts +430 -0
  72. package/dist/handles/types.d.ts.map +1 -0
  73. package/dist/index.d.ts +53 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/internal.d.ts +11 -0
  76. package/dist/internal.d.ts.map +1 -0
  77. package/dist/persistence/BatchPersister.d.ts +142 -0
  78. package/dist/persistence/BatchPersister.d.ts.map +1 -0
  79. package/dist/persistence/ChangeRegistry.d.ts +87 -0
  80. package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
  81. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
  82. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
  83. package/dist/persistence/MutationCollector.d.ts +96 -0
  84. package/dist/persistence/MutationCollector.d.ts.map +1 -0
  85. package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
  86. package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
  87. package/dist/persistence/index.d.ts +7 -0
  88. package/dist/persistence/index.d.ts.map +1 -0
  89. package/dist/persistence/types.d.ts +189 -0
  90. package/dist/persistence/types.d.ts.map +1 -0
  91. package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
  92. package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
  93. package/dist/roles/index.d.ts +11 -0
  94. package/dist/roles/index.d.ts.map +1 -0
  95. package/dist/roles/types.d.ts +242 -0
  96. package/dist/roles/types.d.ts.map +1 -0
  97. package/dist/schema/ContemberSchema.d.ts +98 -0
  98. package/dist/schema/ContemberSchema.d.ts.map +1 -0
  99. package/dist/schema/SchemaLoader.d.ts +28 -0
  100. package/dist/schema/SchemaLoader.d.ts.map +1 -0
  101. package/dist/schema/SchemaRegistry.d.ts +95 -0
  102. package/dist/schema/SchemaRegistry.d.ts.map +1 -0
  103. package/dist/schema/index.d.ts +6 -0
  104. package/dist/schema/index.d.ts.map +1 -0
  105. package/dist/schema/roles.d.ts +62 -0
  106. package/dist/schema/roles.d.ts.map +1 -0
  107. package/dist/schema/types.d.ts +179 -0
  108. package/dist/schema/types.d.ts.map +1 -0
  109. package/dist/selection/SelectionMetaCollector.d.ts +32 -0
  110. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
  111. package/dist/selection/SelectionScope.d.ts +77 -0
  112. package/dist/selection/SelectionScope.d.ts.map +1 -0
  113. package/dist/selection/buildQuery.d.ts +45 -0
  114. package/dist/selection/buildQuery.d.ts.map +1 -0
  115. package/dist/selection/createFragment.d.ts +29 -0
  116. package/dist/selection/createFragment.d.ts.map +1 -0
  117. package/dist/selection/createSelectionBuilder.d.ts +18 -0
  118. package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
  119. package/dist/selection/index.d.ts +8 -0
  120. package/dist/selection/index.d.ts.map +1 -0
  121. package/dist/selection/queryTypes.d.ts +84 -0
  122. package/dist/selection/queryTypes.d.ts.map +1 -0
  123. package/dist/selection/types.d.ts +238 -0
  124. package/dist/selection/types.d.ts.map +1 -0
  125. package/dist/store/DirtyTracker.d.ts +26 -0
  126. package/dist/store/DirtyTracker.d.ts.map +1 -0
  127. package/dist/store/EntityMetaStore.d.ts +52 -0
  128. package/dist/store/EntityMetaStore.d.ts.map +1 -0
  129. package/dist/store/EntitySnapshotStore.d.ts +63 -0
  130. package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
  131. package/dist/store/ErrorStore.d.ts +82 -0
  132. package/dist/store/ErrorStore.d.ts.map +1 -0
  133. package/dist/store/RelationStore.d.ts +175 -0
  134. package/dist/store/RelationStore.d.ts.map +1 -0
  135. package/dist/store/SnapshotStore.d.ts +143 -0
  136. package/dist/store/SnapshotStore.d.ts.map +1 -0
  137. package/dist/store/SubscriptionManager.d.ts +90 -0
  138. package/dist/store/SubscriptionManager.d.ts.map +1 -0
  139. package/dist/store/TouchedStore.d.ts +24 -0
  140. package/dist/store/TouchedStore.d.ts.map +1 -0
  141. package/dist/store/entityId.d.ts +29 -0
  142. package/dist/store/entityId.d.ts.map +1 -0
  143. package/dist/store/index.d.ts +1 -0
  144. package/dist/store/index.d.ts.map +1 -0
  145. package/dist/store/snapshots.d.ts +144 -0
  146. package/dist/store/snapshots.d.ts.map +1 -0
  147. package/dist/undo/UndoManager.d.ts +120 -0
  148. package/dist/undo/UndoManager.d.ts.map +1 -0
  149. package/dist/undo/index.d.ts +3 -0
  150. package/dist/undo/index.d.ts.map +1 -0
  151. package/dist/undo/types.d.ts +74 -0
  152. package/dist/undo/types.d.ts.map +1 -0
  153. package/dist/utils/aliasGenerator.d.ts +25 -0
  154. package/dist/utils/aliasGenerator.d.ts.map +1 -0
  155. package/dist/utils/deepEqual.d.ts +6 -0
  156. package/dist/utils/deepEqual.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +3 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/package.json +29 -0
  160. package/src/adapter/ContemberAdapter.ts +239 -0
  161. package/src/adapter/MockAdapter.ts +394 -0
  162. package/src/adapter/MockQueryEngine.ts +236 -0
  163. package/src/adapter/index.ts +21 -0
  164. package/src/adapter/types.ts +199 -0
  165. package/src/brand/ComponentBrand.ts +28 -0
  166. package/src/contember/index.ts +17 -0
  167. package/src/core/ActionDispatcher.ts +450 -0
  168. package/src/core/Disposable.ts +126 -0
  169. package/src/core/EntityLoader.ts +169 -0
  170. package/src/core/SelectionResolver.ts +78 -0
  171. package/src/core/actionClassification.ts +196 -0
  172. package/src/core/actions.ts +491 -0
  173. package/src/core/index.ts +20 -0
  174. package/src/dataview/filterHandlers.ts +443 -0
  175. package/src/dataview/index.ts +36 -0
  176. package/src/dataview/types.ts +220 -0
  177. package/src/errors/UnfetchedFieldError.ts +18 -0
  178. package/src/errors/errorPathResolver.ts +239 -0
  179. package/src/errors/index.ts +40 -0
  180. package/src/errors/pathMapper.ts +40 -0
  181. package/src/errors/types.ts +223 -0
  182. package/src/events/EventEmitter.ts +447 -0
  183. package/src/events/eventFactory.ts +430 -0
  184. package/src/events/index.ts +89 -0
  185. package/src/events/types.ts +374 -0
  186. package/src/handles/BaseHandle.ts +100 -0
  187. package/src/handles/EntityHandle.ts +538 -0
  188. package/src/handles/FieldHandle.ts +268 -0
  189. package/src/handles/HasManyListHandle.ts +500 -0
  190. package/src/handles/HasOneHandle.ts +500 -0
  191. package/src/handles/PlaceholderHandle.ts +321 -0
  192. package/src/handles/index.ts +37 -0
  193. package/src/handles/proxyFactory.ts +218 -0
  194. package/src/handles/types.ts +743 -0
  195. package/src/index.ts +384 -0
  196. package/src/internal.ts +19 -0
  197. package/src/persistence/BatchPersister.ts +911 -0
  198. package/src/persistence/ChangeRegistry.ts +150 -0
  199. package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
  200. package/src/persistence/MutationCollector.ts +567 -0
  201. package/src/persistence/MutationSchemaProvider.ts +33 -0
  202. package/src/persistence/index.ts +23 -0
  203. package/src/persistence/types.ts +220 -0
  204. package/src/schema/ContemberSchema.ts +132 -0
  205. package/src/schema/SchemaLoader.ts +93 -0
  206. package/src/schema/SchemaRegistry.ts +286 -0
  207. package/src/schema/index.ts +51 -0
  208. package/src/schema/roles.ts +83 -0
  209. package/src/schema/types.ts +238 -0
  210. package/src/selection/SelectionMetaCollector.ts +122 -0
  211. package/src/selection/SelectionScope.ts +208 -0
  212. package/src/selection/buildQuery.ts +134 -0
  213. package/src/selection/createFragment.ts +50 -0
  214. package/src/selection/createSelectionBuilder.ts +283 -0
  215. package/src/selection/index.ts +31 -0
  216. package/src/selection/queryTypes.ts +135 -0
  217. package/src/selection/types.ts +320 -0
  218. package/src/store/DirtyTracker.ts +117 -0
  219. package/src/store/EntityMetaStore.ts +143 -0
  220. package/src/store/EntitySnapshotStore.ts +260 -0
  221. package/src/store/ErrorStore.ts +251 -0
  222. package/src/store/RelationStore.ts +696 -0
  223. package/src/store/SnapshotStore.ts +765 -0
  224. package/src/store/SubscriptionManager.ts +241 -0
  225. package/src/store/TouchedStore.ts +42 -0
  226. package/src/store/entityId.ts +43 -0
  227. package/src/store/index.ts +2 -0
  228. package/src/store/snapshots.ts +203 -0
  229. package/src/undo/UndoManager.ts +418 -0
  230. package/src/undo/index.ts +9 -0
  231. package/src/undo/types.ts +84 -0
  232. package/src/utils/aliasGenerator.ts +56 -0
  233. package/src/utils/deepEqual.ts +33 -0
  234. 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'