@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,394 @@
1
+ import type { BackendAdapter, Query, QueryResult, QueryOptions, GetQuery, ListQuery, PersistResult, CreateResult, DeleteResult } from './types.js'
2
+ import { MockQueryEngine } from './MockQueryEngine.js'
3
+
4
+ /**
5
+ * In-memory data store structure
6
+ */
7
+ export type MockDataStore = {
8
+ [entityType: string]: {
9
+ [id: string]: Record<string, unknown>
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Options for MockAdapter
15
+ */
16
+ export interface MockAdapterOptions {
17
+ /** Simulated network delay in milliseconds (default: 100) */
18
+ delay?: number
19
+ /** Whether to log operations to console (default: false) */
20
+ debug?: boolean
21
+ }
22
+
23
+ /**
24
+ * Mock backend adapter for testing and development.
25
+ * Stores data in memory and simulates async operations.
26
+ */
27
+ export class MockAdapter implements BackendAdapter {
28
+ private readonly delay: number
29
+ private readonly debug: boolean
30
+ private readonly queryEngine: MockQueryEngine
31
+
32
+ constructor(
33
+ private store: MockDataStore,
34
+ options: MockAdapterOptions = {},
35
+ ) {
36
+ this.delay = options.delay ?? 100
37
+ this.debug = options.debug ?? false
38
+ this.queryEngine = new MockQueryEngine()
39
+ }
40
+
41
+ /**
42
+ * Simulates network delay with abort support
43
+ */
44
+ private async simulateDelay(signal?: AbortSignal): Promise<void> {
45
+ if (this.delay > 0) {
46
+ await new Promise<void>((resolve, reject) => {
47
+ const timeoutId = setTimeout(resolve, this.delay)
48
+
49
+ if (signal) {
50
+ if (signal.aborted) {
51
+ clearTimeout(timeoutId)
52
+ reject(new DOMException('Aborted', 'AbortError'))
53
+ return
54
+ }
55
+
56
+ signal.addEventListener('abort', () => {
57
+ clearTimeout(timeoutId)
58
+ reject(new DOMException('Aborted', 'AbortError'))
59
+ }, { once: true })
60
+ }
61
+ })
62
+ }
63
+
64
+ // Check if aborted after delay
65
+ if (signal?.aborted) {
66
+ throw new DOMException('Aborted', 'AbortError')
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Logs operations if debug is enabled
72
+ */
73
+ private log(operation: string, ...args: unknown[]): void {
74
+ if (this.debug) {
75
+ console.log(`[MockAdapter] ${operation}:`, ...args)
76
+ }
77
+ }
78
+
79
+ async query(queries: readonly Query[], options?: QueryOptions): Promise<QueryResult[]> {
80
+ this.log('query', { queries })
81
+ await this.simulateDelay(options?.signal)
82
+
83
+ return queries.map(q => {
84
+ if (q.type === 'get') {
85
+ return this.executeGet(q)
86
+ } else {
87
+ return this.executeList(q)
88
+ }
89
+ })
90
+ }
91
+
92
+ private executeGet(query: GetQuery): QueryResult {
93
+ const entityStore = this.store[query.entityType]
94
+ if (!entityStore) {
95
+ return { type: 'get', data: null }
96
+ }
97
+
98
+ // Find entity by unique field(s) in 'by'
99
+ const by = query.by
100
+ const [field, value] = Object.entries(by)[0] ?? []
101
+
102
+ if (!field || value === undefined) {
103
+ return { type: 'get', data: null }
104
+ }
105
+
106
+ let entity: Record<string, unknown> | undefined
107
+ if (field === 'id') {
108
+ // Direct lookup by ID
109
+ entity = entityStore[value as string]
110
+ } else {
111
+ // Search by other unique field
112
+ entity = Object.values(entityStore).find(e => e[field] === value)
113
+ }
114
+
115
+ if (!entity) {
116
+ return { type: 'get', data: null }
117
+ }
118
+
119
+ const result = this.queryEngine.projectFields(entity, query.spec.fields)
120
+ this.log('executeGet result', result)
121
+
122
+ return { type: 'get', data: result }
123
+ }
124
+
125
+ private executeList(query: ListQuery): QueryResult {
126
+ const entityStore = this.store[query.entityType]
127
+ if (!entityStore) {
128
+ return { type: 'list', data: [] }
129
+ }
130
+
131
+ let entities = Object.values(entityStore)
132
+
133
+ // Apply filter using query engine
134
+ if (query.filter) {
135
+ entities = this.queryEngine.filter(entities, query.filter as Record<string, unknown>)
136
+ }
137
+
138
+ // Apply ordering using query engine
139
+ if (query.orderBy && query.orderBy.length > 0) {
140
+ entities = this.queryEngine.orderBy(entities, query.orderBy)
141
+ }
142
+
143
+ // Apply pagination using query engine
144
+ entities = this.queryEngine.paginate(entities, query.offset, query.limit)
145
+
146
+ const results = entities.map(entity => this.queryEngine.projectFields(entity, query.spec.fields))
147
+ this.log('executeList result', results)
148
+
149
+ return { type: 'list', data: results }
150
+ }
151
+
152
+ async persist(
153
+ entityType: string,
154
+ id: string,
155
+ changes: Record<string, unknown>,
156
+ ): Promise<PersistResult> {
157
+ this.log('persist', { entityType, id, changes })
158
+ await this.simulateDelay()
159
+
160
+ const entityStore = this.store[entityType]
161
+ if (!entityStore) {
162
+ return {
163
+ ok: false,
164
+ errorMessage: `Entity type '${entityType}' not found in store`,
165
+ }
166
+ }
167
+
168
+ const entity = entityStore[id]
169
+ if (!entity) {
170
+ return {
171
+ ok: false,
172
+ errorMessage: `Entity '${entityType}:${id}' not found`,
173
+ }
174
+ }
175
+
176
+ // Process changes with Contember-style operation support
177
+ this.applyChanges(entity, changes)
178
+ this.log('persist result', entity)
179
+
180
+ return { ok: true }
181
+ }
182
+
183
+ /**
184
+ * Applies changes to an entity, handling Contember-style relation operations.
185
+ * Supports:
186
+ * - Scalar fields: direct assignment
187
+ * - HasOne: { connect: { id } }, { disconnect: true }, { delete: true }, { create: {...} }, { update: {...} }
188
+ * - HasMany: [{ connect: { id } }, { disconnect: { id } }, { delete: { id } }, { create: {...} }, { update: { by: { id }, data: {...} } }]
189
+ */
190
+ private applyChanges(entity: Record<string, unknown>, changes: Record<string, unknown>): void {
191
+ for (const [key, value] of Object.entries(changes)) {
192
+ if (value === null || value === undefined) {
193
+ entity[key] = value
194
+ continue
195
+ }
196
+
197
+ // Check if it's a Contember-style relation operation
198
+ if (typeof value === 'object' && !Array.isArray(value)) {
199
+ const operation = value as Record<string, unknown>
200
+
201
+ if ('connect' in operation) {
202
+ // HasOne connect: { connect: { id: '...' } }
203
+ const connectData = operation['connect'] as Record<string, unknown>
204
+ const targetId = connectData['id'] as string
205
+ entity[key] = this.resolveRelatedEntity(targetId)
206
+ } else if ('disconnect' in operation && operation['disconnect'] === true) {
207
+ // HasOne disconnect: { disconnect: true }
208
+ entity[key] = null
209
+ } else if ('delete' in operation && operation['delete'] === true) {
210
+ // HasOne delete: { delete: true }
211
+ // Delete the related entity and set to null
212
+ const existingRelation = entity[key] as Record<string, unknown> | null
213
+ if (existingRelation?.['id']) {
214
+ this.deleteRelatedEntity(existingRelation['id'] as string)
215
+ }
216
+ entity[key] = null
217
+ } else if ('create' in operation) {
218
+ // HasOne create: { create: { name: '...', ... } }
219
+ const createData = operation['create'] as Record<string, unknown>
220
+ entity[key] = this.createRelatedEntity(createData)
221
+ } else if ('update' in operation) {
222
+ // HasOne update: { update: { name: '...', ... } }
223
+ const updateData = operation['update'] as Record<string, unknown>
224
+ const existingRelation = entity[key] as Record<string, unknown> | null
225
+ if (existingRelation) {
226
+ this.applyChanges(existingRelation, updateData)
227
+ }
228
+ } else {
229
+ // Regular object value (not a Contember operation)
230
+ entity[key] = value
231
+ }
232
+ } else if (Array.isArray(value)) {
233
+ // HasMany operations: [{ connect: { id } }, { disconnect: { id } }, ...]
234
+ entity[key] = this.applyHasManyOperations(
235
+ entity[key] as Array<Record<string, unknown>> | undefined,
236
+ value as Array<Record<string, unknown>>,
237
+ )
238
+ } else {
239
+ // Scalar field
240
+ entity[key] = value
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Applies hasMany relation operations.
247
+ */
248
+ private applyHasManyOperations(
249
+ currentItems: Array<Record<string, unknown>> | undefined,
250
+ operations: Array<Record<string, unknown>>,
251
+ ): Array<Record<string, unknown>> {
252
+ const items = [...(currentItems ?? [])]
253
+
254
+ for (const operation of operations) {
255
+ if ('connect' in operation) {
256
+ // Connect: { connect: { id: '...' } }
257
+ const connectData = operation['connect'] as Record<string, unknown>
258
+ const targetId = connectData['id'] as string
259
+ // Only add if not already present
260
+ if (!items.some(item => item['id'] === targetId)) {
261
+ const relatedEntity = this.resolveRelatedEntity(targetId)
262
+ if (relatedEntity) {
263
+ items.push(relatedEntity)
264
+ }
265
+ }
266
+ } else if ('disconnect' in operation) {
267
+ // Disconnect: { disconnect: { id: '...' } }
268
+ const disconnectData = operation['disconnect'] as Record<string, unknown>
269
+ const targetId = disconnectData['id'] as string
270
+ const index = items.findIndex(item => item['id'] === targetId)
271
+ if (index >= 0) {
272
+ items.splice(index, 1)
273
+ }
274
+ } else if ('delete' in operation) {
275
+ // Delete: { delete: { id: '...' } }
276
+ const deleteData = operation['delete'] as Record<string, unknown>
277
+ const targetId = deleteData['id'] as string
278
+ const index = items.findIndex(item => item['id'] === targetId)
279
+ if (index >= 0) {
280
+ items.splice(index, 1)
281
+ this.deleteRelatedEntity(targetId)
282
+ }
283
+ } else if ('create' in operation) {
284
+ // Create: { create: { name: '...', ... } }
285
+ const createData = operation['create'] as Record<string, unknown>
286
+ const newEntity = this.createRelatedEntity(createData)
287
+ if (newEntity) {
288
+ items.push(newEntity)
289
+ }
290
+ } else if ('update' in operation) {
291
+ // Update: { update: { by: { id: '...' }, data: { ... } } }
292
+ const updateOp = operation['update'] as Record<string, unknown>
293
+ const by = updateOp['by'] as Record<string, unknown>
294
+ const data = updateOp['data'] as Record<string, unknown>
295
+ const targetId = by['id'] as string
296
+ const item = items.find(item => item['id'] === targetId)
297
+ if (item) {
298
+ this.applyChanges(item, data)
299
+ }
300
+ }
301
+ }
302
+
303
+ return items
304
+ }
305
+
306
+ /**
307
+ * Resolves a related entity by ID from any entity store.
308
+ * Returns a copy of the entity data or { id } if not found.
309
+ */
310
+ private resolveRelatedEntity(id: string): Record<string, unknown> | null {
311
+ for (const entityStore of Object.values(this.store)) {
312
+ if (entityStore[id]) {
313
+ return { ...entityStore[id] }
314
+ }
315
+ }
316
+ // Entity not found - return just the ID
317
+ return { id }
318
+ }
319
+
320
+ /**
321
+ * Creates a related entity with a generated ID.
322
+ */
323
+ private createRelatedEntity(data: Record<string, unknown>): Record<string, unknown> {
324
+ const id = this.generateId()
325
+ return { id, ...data }
326
+ }
327
+
328
+ /**
329
+ * Deletes a related entity from any entity store.
330
+ */
331
+ private deleteRelatedEntity(id: string): void {
332
+ for (const entityStore of Object.values(this.store)) {
333
+ if (entityStore[id]) {
334
+ delete entityStore[id]
335
+ return
336
+ }
337
+ }
338
+ }
339
+
340
+ async create(
341
+ entityType: string,
342
+ data: Record<string, unknown>,
343
+ ): Promise<CreateResult> {
344
+ this.log('create', { entityType, data })
345
+ await this.simulateDelay()
346
+
347
+ // Ensure entity store exists
348
+ if (!this.store[entityType]) {
349
+ this.store[entityType] = {}
350
+ }
351
+
352
+ // Generate ID if not provided
353
+ const id = (data['id'] as string) ?? this.generateId()
354
+ const entity = { ...data, id }
355
+
356
+ this.store[entityType]![id] = entity
357
+ this.log('create result', entity)
358
+
359
+ return { ok: true, data: entity }
360
+ }
361
+
362
+ async delete(entityType: string, id: string): Promise<DeleteResult> {
363
+ this.log('delete', { entityType, id })
364
+ await this.simulateDelay()
365
+
366
+ const entityStore = this.store[entityType]
367
+ if (entityStore) {
368
+ delete entityStore[id]
369
+ }
370
+
371
+ return { ok: true }
372
+ }
373
+
374
+ /**
375
+ * Generates a simple unique ID
376
+ */
377
+ private generateId(): string {
378
+ return `mock_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
379
+ }
380
+
381
+ /**
382
+ * Gets the current store state (for testing/debugging)
383
+ */
384
+ getStore(): MockDataStore {
385
+ return this.store
386
+ }
387
+
388
+ /**
389
+ * Resets the store to initial state
390
+ */
391
+ resetStore(newStore: MockDataStore): void {
392
+ this.store = newStore
393
+ }
394
+ }
@@ -0,0 +1,236 @@
1
+ import type { QueryFieldSpec } from '../selection/buildQuery.js'
2
+
3
+ /**
4
+ * MockQueryEngine provides in-memory query operations for MockAdapter.
5
+ * Extracted for better testability and separation of concerns.
6
+ *
7
+ * Supports:
8
+ * - Filtering with various operators (eq, notEq, in, lt, gt, contains, etc.)
9
+ * - Ordering with multi-field support and null handling
10
+ * - Pagination (offset/limit)
11
+ * - Field projection with nested relation support
12
+ */
13
+ export class MockQueryEngine {
14
+ /**
15
+ * Filters entities based on a filter object.
16
+ */
17
+ filter(
18
+ entities: Record<string, unknown>[],
19
+ filter: Record<string, unknown>,
20
+ ): Record<string, unknown>[] {
21
+ return entities.filter(entity => this.matchesFilter(entity, filter))
22
+ }
23
+
24
+ /**
25
+ * Orders entities based on orderBy specifications.
26
+ */
27
+ orderBy(
28
+ entities: Record<string, unknown>[],
29
+ orderBy: readonly Record<string, unknown>[],
30
+ ): Record<string, unknown>[] {
31
+ return [...entities].sort((a, b) => {
32
+ for (const order of orderBy) {
33
+ for (const [field, direction] of Object.entries(order)) {
34
+ if (field.startsWith('_')) continue // Skip _random, _randomSeeded
35
+ const aVal = a[field]
36
+ const bVal = b[field]
37
+
38
+ let comparison = 0
39
+ if (aVal === bVal) comparison = 0
40
+ else if (aVal === null || aVal === undefined) comparison = 1
41
+ else if (bVal === null || bVal === undefined) comparison = -1
42
+ else if (aVal < bVal) comparison = -1
43
+ else comparison = 1
44
+
45
+ if (direction === 'desc' || direction === 'descNullsLast') {
46
+ comparison = -comparison
47
+ }
48
+
49
+ if (comparison !== 0) return comparison
50
+ }
51
+ }
52
+ return 0
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Paginates entities using offset and limit.
58
+ */
59
+ paginate(
60
+ entities: Record<string, unknown>[],
61
+ offset?: number,
62
+ limit?: number,
63
+ ): Record<string, unknown>[] {
64
+ let result = entities
65
+ if (offset !== undefined) {
66
+ result = result.slice(offset)
67
+ }
68
+ if (limit !== undefined) {
69
+ result = result.slice(0, limit)
70
+ }
71
+ return result
72
+ }
73
+
74
+ /**
75
+ * Projects only the requested fields from source data.
76
+ * Supports nested relations (hasOne and hasMany).
77
+ */
78
+ projectFields(
79
+ source: Record<string, unknown>,
80
+ fields: QueryFieldSpec[],
81
+ basePath: string[] = [],
82
+ ): Record<string, unknown> {
83
+ const result: Record<string, unknown> = {}
84
+
85
+ for (const field of fields) {
86
+ // Get relative path by removing the base path prefix
87
+ const relativePath = field.sourcePath.slice(basePath.length)
88
+ const value = this.getNestedValue(source, relativePath)
89
+
90
+ if (field.nested && Array.isArray(value)) {
91
+ // Has-many relation — apply filter/orderBy/limit/offset, return flat array + metadata
92
+ let items = value as Record<string, unknown>[]
93
+ if (field.filter) {
94
+ items = this.filter(items, field.filter as Record<string, unknown>)
95
+ }
96
+ if (field.orderBy) {
97
+ items = this.orderBy(items, field.orderBy as readonly Record<string, unknown>[])
98
+ }
99
+ const totalCount = items.length
100
+ if (field.limit !== undefined || field.offset !== undefined) {
101
+ items = this.paginate(items, field.offset, field.limit)
102
+ }
103
+ const projected = items.map(item =>
104
+ this.projectFields(item as Record<string, unknown>, field.nested!.fields, []),
105
+ )
106
+ if (field.totalCount) {
107
+ Object.defineProperty(projected, 'totalCount', {
108
+ value: totalCount,
109
+ enumerable: false,
110
+ writable: false,
111
+ })
112
+ }
113
+ result[field.name] = projected
114
+ } else if (field.nested && value && typeof value === 'object') {
115
+ // Project nested object (has-one relation)
116
+ result[field.name] = this.projectFields(
117
+ value as Record<string, unknown>,
118
+ field.nested.fields,
119
+ [],
120
+ )
121
+ } else {
122
+ // Scalar or leaf value
123
+ result[field.name] = value
124
+ }
125
+ }
126
+
127
+ return result
128
+ }
129
+
130
+ /**
131
+ * Checks if an entity matches a filter.
132
+ */
133
+ private matchesFilter(entity: Record<string, unknown>, filter: Record<string, unknown>): boolean {
134
+ for (const [key, condition] of Object.entries(filter)) {
135
+ if (key === 'and' && Array.isArray(condition)) {
136
+ if (!condition.every(c => this.matchesFilter(entity, c as Record<string, unknown>))) {
137
+ return false
138
+ }
139
+ continue
140
+ }
141
+ if (key === 'or' && Array.isArray(condition)) {
142
+ if (!condition.some(c => this.matchesFilter(entity, c as Record<string, unknown>))) {
143
+ return false
144
+ }
145
+ continue
146
+ }
147
+ if (key === 'not' && condition && typeof condition === 'object') {
148
+ if (this.matchesFilter(entity, condition as Record<string, unknown>)) {
149
+ return false
150
+ }
151
+ continue
152
+ }
153
+
154
+ const fieldValue = entity[key]
155
+ if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
156
+ // It's a condition object like { eq: 'value' }
157
+ if (!this.matchesCondition(fieldValue, condition as Record<string, unknown>)) {
158
+ return false
159
+ }
160
+ } else {
161
+ // Direct value comparison (for backward compatibility)
162
+ if (fieldValue !== condition) {
163
+ return false
164
+ }
165
+ }
166
+ }
167
+ return true
168
+ }
169
+
170
+ /**
171
+ * Checks if a value matches a condition object.
172
+ */
173
+ private matchesCondition(value: unknown, condition: Record<string, unknown>): boolean {
174
+ for (const [op, expected] of Object.entries(condition)) {
175
+ switch (op) {
176
+ case 'eq':
177
+ if (value !== expected) return false
178
+ break
179
+ case 'notEq':
180
+ if (value === expected) return false
181
+ break
182
+ case 'in':
183
+ if (!Array.isArray(expected) || !expected.includes(value)) return false
184
+ break
185
+ case 'notIn':
186
+ if (Array.isArray(expected) && expected.includes(value)) return false
187
+ break
188
+ case 'lt':
189
+ if (typeof value !== 'number' || typeof expected !== 'number' || value >= expected) return false
190
+ break
191
+ case 'lte':
192
+ if (typeof value !== 'number' || typeof expected !== 'number' || value > expected) return false
193
+ break
194
+ case 'gt':
195
+ if (typeof value !== 'number' || typeof expected !== 'number' || value <= expected) return false
196
+ break
197
+ case 'gte':
198
+ if (typeof value !== 'number' || typeof expected !== 'number' || value < expected) return false
199
+ break
200
+ case 'isNull':
201
+ if (expected === true && value !== null && value !== undefined) return false
202
+ if (expected === false && (value === null || value === undefined)) return false
203
+ break
204
+ case 'contains':
205
+ if (typeof value !== 'string' || typeof expected !== 'string' || !value.includes(expected)) return false
206
+ break
207
+ case 'startsWith':
208
+ if (typeof value !== 'string' || typeof expected !== 'string' || !value.startsWith(expected)) return false
209
+ break
210
+ case 'endsWith':
211
+ if (typeof value !== 'string' || typeof expected !== 'string' || !value.endsWith(expected)) return false
212
+ break
213
+ case 'containsCI':
214
+ if (typeof value !== 'string' || typeof expected !== 'string' || !value.toLowerCase().includes(expected.toLowerCase())) return false
215
+ break
216
+ }
217
+ }
218
+ return true
219
+ }
220
+
221
+ /**
222
+ * Gets a nested value from an object using a path.
223
+ */
224
+ private getNestedValue(obj: Record<string, unknown>, path: string[]): unknown {
225
+ let current: unknown = obj
226
+
227
+ for (const key of path) {
228
+ if (current === null || current === undefined || typeof current !== 'object') {
229
+ return undefined
230
+ }
231
+ current = (current as Record<string, unknown>)[key]
232
+ }
233
+
234
+ return current
235
+ }
236
+ }
@@ -0,0 +1,21 @@
1
+ export type {
2
+ BackendAdapter,
3
+ QueryOptions,
4
+ Query,
5
+ GetQuery,
6
+ ListQuery,
7
+ QueryResult,
8
+ GetQueryResult,
9
+ ListQueryResult,
10
+ PersistResult,
11
+ CreateResult,
12
+ DeleteResult,
13
+ EntityUniqueWhere,
14
+ UniqueWhereValue,
15
+ TransactionMutation,
16
+ TransactionMutationResult,
17
+ TransactionResult,
18
+ } from './types.js'
19
+ export { MockAdapter, type MockDataStore, type MockAdapterOptions } from './MockAdapter.js'
20
+ export { MockQueryEngine } from './MockQueryEngine.js'
21
+ export { ContemberAdapter, type ContemberAdapterOptions } from './ContemberAdapter.js'