@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,126 @@
1
+ /**
2
+ * Disposable pattern for cleanup management.
3
+ * Ensures proper cleanup of subscriptions and resources.
4
+ */
5
+
6
+ /**
7
+ * Interface for objects that can be disposed.
8
+ */
9
+ export interface Disposable {
10
+ dispose(): void
11
+ }
12
+
13
+ /**
14
+ * A group of disposables that can be disposed together.
15
+ * Provides safe cleanup even if individual dispose() calls throw.
16
+ */
17
+ export class DisposableGroup implements Disposable {
18
+ private disposables: Disposable[] = []
19
+ private disposed = false
20
+
21
+ /**
22
+ * Adds a disposable to the group.
23
+ * If the group is already disposed, the disposable is disposed immediately.
24
+ */
25
+ add(disposable: Disposable): void {
26
+ if (this.disposed) {
27
+ // Already disposed, dispose immediately
28
+ try {
29
+ disposable.dispose()
30
+ } catch (error) {
31
+ console.error('Error disposing:', error)
32
+ }
33
+ return
34
+ }
35
+ this.disposables.push(disposable)
36
+ }
37
+
38
+ /**
39
+ * Adds a callback as a disposable.
40
+ */
41
+ addCallback(callback: () => void): void {
42
+ this.add({ dispose: callback })
43
+ }
44
+
45
+ /**
46
+ * Adds an unsubscribe function as a disposable.
47
+ */
48
+ addUnsubscribe(unsubscribe: () => void): void {
49
+ this.addCallback(unsubscribe)
50
+ }
51
+
52
+ /**
53
+ * Disposes all items in the group.
54
+ * Each item is disposed even if previous ones throw.
55
+ */
56
+ dispose(): void {
57
+ if (this.disposed) return
58
+ this.disposed = true
59
+
60
+ const errors: Error[] = []
61
+
62
+ for (const disposable of this.disposables) {
63
+ try {
64
+ disposable.dispose()
65
+ } catch (error) {
66
+ errors.push(error instanceof Error ? error : new Error(String(error)))
67
+ }
68
+ }
69
+
70
+ this.disposables = []
71
+
72
+ if (errors.length > 0) {
73
+ console.error('Errors during disposal:', errors)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Checks if the group has been disposed.
79
+ */
80
+ isDisposed(): boolean {
81
+ return this.disposed
82
+ }
83
+
84
+ /**
85
+ * Gets the number of disposables in the group.
86
+ */
87
+ get size(): number {
88
+ return this.disposables.length
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Creates a disposable from a callback function.
94
+ */
95
+ export function createDisposable(callback: () => void): Disposable {
96
+ let disposed = false
97
+ return {
98
+ dispose() {
99
+ if (disposed) return
100
+ disposed = true
101
+ callback()
102
+ },
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Combines multiple disposables into one.
108
+ */
109
+ export function combineDisposables(...disposables: Disposable[]): Disposable {
110
+ const group = new DisposableGroup()
111
+ for (const d of disposables) {
112
+ group.add(d)
113
+ }
114
+ return group
115
+ }
116
+
117
+ /**
118
+ * Type guard to check if something is disposable.
119
+ */
120
+ export function isDisposable(value: unknown): value is Disposable {
121
+ return (
122
+ typeof value === 'object' &&
123
+ value !== null &&
124
+ typeof (value as Disposable).dispose === 'function'
125
+ )
126
+ }
@@ -0,0 +1,169 @@
1
+ import type { BackendAdapter } from '../adapter/types.js'
2
+ import type { SnapshotStore } from '../store/SnapshotStore.js'
3
+ import type { QuerySpec } from '../selection/buildQuery.js'
4
+ import type { EntityWhere, EntityOrderBy } from '../selection/queryTypes.js'
5
+ import { createLoadError, type FieldError } from '../errors/types.js'
6
+
7
+ /**
8
+ * Checks if an error is an AbortError that should be rethrown.
9
+ */
10
+ function isAbortError(error: unknown): boolean {
11
+ return error instanceof DOMException && error.name === 'AbortError'
12
+ }
13
+
14
+ /**
15
+ * Normalizes unknown error to Error instance.
16
+ */
17
+ function normalizeError(error: unknown): Error {
18
+ return error instanceof Error ? error : new Error(String(error))
19
+ }
20
+
21
+ /**
22
+ * Result of entity loading operation
23
+ */
24
+ export type EntityLoadResult<T> =
25
+ | { status: 'success'; data: T }
26
+ | { status: 'error'; error: FieldError }
27
+ | { status: 'not_found' }
28
+
29
+ /**
30
+ * Result of entity list loading operation
31
+ */
32
+ export type EntityListLoadResult<T> =
33
+ | { status: 'success'; data: T[] }
34
+ | { status: 'error'; error: FieldError }
35
+
36
+ /**
37
+ * Options for loading a single entity
38
+ */
39
+ export interface LoadEntityOptions {
40
+ entityType: string
41
+ id: string
42
+ query: QuerySpec
43
+ useCache?: boolean
44
+ signal?: AbortSignal
45
+ }
46
+
47
+ /**
48
+ * Options for loading entity list
49
+ */
50
+ export interface LoadEntityListOptions<TEntity = unknown> {
51
+ entityType: string
52
+ query: QuerySpec
53
+ filter?: EntityWhere<TEntity>
54
+ orderBy?: readonly EntityOrderBy<TEntity>[]
55
+ limit?: number
56
+ offset?: number
57
+ signal?: AbortSignal
58
+ }
59
+
60
+ /**
61
+ * Non-React service for loading entities.
62
+ * Can be used in any JavaScript environment.
63
+ */
64
+ export class EntityLoader {
65
+ constructor(
66
+ private readonly adapter: BackendAdapter,
67
+ private readonly store: SnapshotStore,
68
+ ) {}
69
+
70
+ /**
71
+ * Loads a single entity by ID.
72
+ */
73
+ async loadOne<T>(options: LoadEntityOptions): Promise<EntityLoadResult<T>> {
74
+ const { entityType, id, query, useCache = false, signal } = options
75
+
76
+ // Check cache first
77
+ if (useCache && this.store.hasEntity(entityType, id)) {
78
+ const snapshot = this.store.getEntitySnapshot(entityType, id)
79
+ if (snapshot) {
80
+ return { status: 'success', data: snapshot.data as T }
81
+ }
82
+ }
83
+
84
+ try {
85
+ const results = await this.adapter.query(
86
+ [{ type: 'get', entityType, by: { id }, spec: query }],
87
+ { signal },
88
+ )
89
+
90
+ const result = results[0]
91
+ if (!result || result.type !== 'get' || result.data === null) {
92
+ return { status: 'not_found' }
93
+ }
94
+
95
+ // Store in snapshot store
96
+ this.store.setEntityData(entityType, id, result.data, true)
97
+
98
+ return { status: 'success', data: result.data as T }
99
+ } catch (error) {
100
+ if (isAbortError(error)) {
101
+ throw error
102
+ }
103
+ return { status: 'error', error: createLoadError(normalizeError(error)) }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Loads multiple entities matching filter.
109
+ */
110
+ async loadMany<T, TEntity = unknown>(options: LoadEntityListOptions<TEntity>): Promise<EntityListLoadResult<T>> {
111
+ const { entityType, query, filter, orderBy, limit, offset, signal } = options
112
+
113
+ try {
114
+ const results = await this.adapter.query(
115
+ [{ type: 'list', entityType, filter, orderBy, limit, offset, spec: query }],
116
+ { signal },
117
+ )
118
+
119
+ const result = results[0]
120
+ if (!result || result.type !== 'list') {
121
+ return {
122
+ status: 'error',
123
+ error: createLoadError(new Error('Unexpected query result type')),
124
+ }
125
+ }
126
+
127
+ // Store each entity in snapshot store
128
+ for (const item of result.data) {
129
+ const record = item as Record<string, unknown>
130
+ if (typeof record['id'] === 'string') {
131
+ this.store.setEntityData(entityType, record['id'], record, true)
132
+ }
133
+ }
134
+
135
+ return { status: 'success', data: result.data as T[] }
136
+ } catch (error) {
137
+ if (isAbortError(error)) {
138
+ throw error
139
+ }
140
+ return { status: 'error', error: createLoadError(normalizeError(error)) }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Checks if entity exists in cache.
146
+ */
147
+ hasInCache(entityType: string, id: string): boolean {
148
+ return this.store.hasEntity(entityType, id)
149
+ }
150
+
151
+ /**
152
+ * Gets entity from cache if available.
153
+ */
154
+ getFromCache<T>(entityType: string, id: string): T | null {
155
+ const snapshot = this.store.getEntitySnapshot(entityType, id)
156
+ return snapshot ? (snapshot.data as T) : null
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Creates an EntityLoader instance.
162
+ * Factory function for easier testing and dependency injection.
163
+ */
164
+ export function createEntityLoader(
165
+ adapter: BackendAdapter,
166
+ store: SnapshotStore,
167
+ ): EntityLoader {
168
+ return new EntityLoader(adapter, store)
169
+ }
@@ -0,0 +1,78 @@
1
+ import type { SelectionMeta, FluentFragment, SelectionBuilder } from '../selection/types.js'
2
+ import { createSelectionBuilder, SELECTION_META, buildQueryFromSelection } from '../selection/index.js'
3
+ import type { QuerySpec } from '../selection/buildQuery.js'
4
+
5
+ /**
6
+ * Input types for selection resolution
7
+ */
8
+ export type SelectionInput<TModel, TResult extends object> =
9
+ | FluentDefiner<TModel, TResult>
10
+ | FluentFragment<TModel, TResult>
11
+ | SelectionMeta
12
+
13
+ /**
14
+ * Type for fluent definer function
15
+ */
16
+ export type FluentDefiner<TModel, TResult extends object> = (
17
+ builder: SelectionBuilder<TModel>,
18
+ ) => SelectionBuilder<TModel, TResult, object>
19
+
20
+ /**
21
+ * Resolves various selection input formats to SelectionMeta.
22
+ * Works without React - pure function.
23
+ */
24
+ export function resolveSelectionMeta<TModel, TResult extends object>(
25
+ input: SelectionInput<TModel, TResult>,
26
+ ): SelectionMeta {
27
+ // Already SelectionMeta
28
+ if (isSelectionMeta(input)) {
29
+ return input
30
+ }
31
+
32
+ // FluentFragment
33
+ if (isFluentFragment(input)) {
34
+ return input.__meta
35
+ }
36
+
37
+ // FluentDefiner function
38
+ const builder = createSelectionBuilder<TModel>()
39
+ const resultBuilder = input(builder)
40
+ return resultBuilder[SELECTION_META]
41
+ }
42
+
43
+ /**
44
+ * Type guard for SelectionMeta
45
+ */
46
+ function isSelectionMeta(input: unknown): input is SelectionMeta {
47
+ return (
48
+ typeof input === 'object' &&
49
+ input !== null &&
50
+ 'fields' in input &&
51
+ input.fields instanceof Map
52
+ )
53
+ }
54
+
55
+ /**
56
+ * Type guard for FluentFragment
57
+ */
58
+ function isFluentFragment<TModel, TResult extends object>(
59
+ input: unknown,
60
+ ): input is FluentFragment<TModel, TResult> {
61
+ return (
62
+ typeof input === 'object' &&
63
+ input !== null &&
64
+ '__isFragment' in input &&
65
+ (input as FluentFragment<TModel, TResult>).__isFragment === true
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Builds a QuerySpec from selection input.
71
+ * Convenience function combining resolution and building.
72
+ */
73
+ export function buildQuery<TModel, TResult extends object>(
74
+ input: SelectionInput<TModel, TResult>,
75
+ ): QuerySpec {
76
+ const meta = resolveSelectionMeta(input)
77
+ return buildQueryFromSelection(meta)
78
+ }
@@ -0,0 +1,196 @@
1
+ import type { Action } from './actions.js'
2
+
3
+ /**
4
+ * Keys in the store that are affected by an action.
5
+ * Used for partial snapshot capture/restore.
6
+ */
7
+ export interface StoreAffectedKeys {
8
+ /** Entity keys in format "entityType:id" */
9
+ entityKeys: string[]
10
+ /** Relation keys in format "entityType:id:fieldName" */
11
+ relationKeys: string[]
12
+ /** Has-many keys in format "entityType:id:fieldName" */
13
+ hasManyKeys: string[]
14
+ }
15
+
16
+ /**
17
+ * Action types that should be tracked for undo/redo.
18
+ * These are user-initiated data mutations.
19
+ */
20
+ const TRACKABLE_ACTION_TYPES = new Set<Action['type']>([
21
+ 'SET_FIELD',
22
+ 'SET_ENTITY_DATA',
23
+ 'SET_PLACEHOLDER_DATA',
24
+ 'CONNECT_RELATION',
25
+ 'DISCONNECT_RELATION',
26
+ 'DELETE_RELATION',
27
+ 'RESET_ENTITY',
28
+ 'RESET_RELATION',
29
+ 'ADD_TO_LIST',
30
+ 'REMOVE_FROM_LIST',
31
+ 'MOVE_IN_LIST',
32
+ ])
33
+
34
+ /**
35
+ * Action types that should be skipped for undo/redo.
36
+ * These are operational, commit, or error actions.
37
+ */
38
+ const SKIP_ACTION_TYPES = new Set<Action['type']>([
39
+ 'SET_LOAD_STATE',
40
+ 'SET_PERSISTING',
41
+ 'COMMIT_ENTITY',
42
+ 'COMMIT_RELATION',
43
+ // Error actions are skipped as errors are ephemeral state
44
+ 'ADD_FIELD_ERROR',
45
+ 'CLEAR_FIELD_ERRORS',
46
+ 'ADD_ENTITY_ERROR',
47
+ 'CLEAR_ENTITY_ERRORS',
48
+ 'ADD_RELATION_ERROR',
49
+ 'CLEAR_RELATION_ERRORS',
50
+ 'CLEAR_ALL_SERVER_ERRORS',
51
+ 'CLEAR_ALL_ERRORS',
52
+ ])
53
+
54
+ /**
55
+ * Checks if an action should be tracked for undo/redo.
56
+ * Server data loads (SET_ENTITY_DATA with isServerData=true) are not tracked.
57
+ */
58
+ export function isTrackableAction(action: Action): boolean {
59
+ if (!TRACKABLE_ACTION_TYPES.has(action.type)) {
60
+ return false
61
+ }
62
+ // Server data loads are not user-initiated mutations
63
+ if (action.type === 'SET_ENTITY_DATA' && action.isServerData) {
64
+ return false
65
+ }
66
+ return true
67
+ }
68
+
69
+ /**
70
+ * Checks if an action should be skipped for undo/redo.
71
+ */
72
+ export function isSkippedAction(action: Action): boolean {
73
+ return SKIP_ACTION_TYPES.has(action.type)
74
+ }
75
+
76
+ /**
77
+ * Creates an entity key from type and id.
78
+ */
79
+ export function createEntityKey(entityType: string, entityId: string): string {
80
+ return `${entityType}:${entityId}`
81
+ }
82
+
83
+ /**
84
+ * Creates a relation key from parent type, id and field name.
85
+ */
86
+ export function createRelationKey(
87
+ entityType: string,
88
+ entityId: string,
89
+ fieldName: string,
90
+ ): string {
91
+ return `${entityType}:${entityId}:${fieldName}`
92
+ }
93
+
94
+ /**
95
+ * Extracts the store keys affected by an action.
96
+ * Returns entity keys, relation keys, and has-many keys.
97
+ */
98
+ export function getAffectedKeys(action: Action): StoreAffectedKeys {
99
+ const entityKeys: string[] = []
100
+ const relationKeys: string[] = []
101
+ const hasManyKeys: string[] = []
102
+
103
+ switch (action.type) {
104
+ case 'SET_FIELD':
105
+ case 'SET_ENTITY_DATA':
106
+ case 'RESET_ENTITY':
107
+ case 'COMMIT_ENTITY':
108
+ entityKeys.push(createEntityKey(action.entityType, action.entityId))
109
+ break
110
+
111
+ case 'CONNECT_RELATION':
112
+ case 'DISCONNECT_RELATION':
113
+ case 'DELETE_RELATION':
114
+ case 'RESET_RELATION':
115
+ case 'COMMIT_RELATION':
116
+ case 'SET_PLACEHOLDER_DATA':
117
+ relationKeys.push(
118
+ createRelationKey(action.entityType, action.entityId, action.fieldName),
119
+ )
120
+ break
121
+
122
+ case 'ADD_TO_LIST':
123
+ case 'REMOVE_FROM_LIST':
124
+ case 'MOVE_IN_LIST':
125
+ hasManyKeys.push(
126
+ createRelationKey(action.entityType, action.entityId, action.fieldName),
127
+ )
128
+ break
129
+
130
+ case 'SET_LOAD_STATE':
131
+ case 'SET_PERSISTING':
132
+ case 'ADD_FIELD_ERROR':
133
+ case 'CLEAR_FIELD_ERRORS':
134
+ case 'ADD_ENTITY_ERROR':
135
+ case 'CLEAR_ENTITY_ERRORS':
136
+ case 'ADD_RELATION_ERROR':
137
+ case 'CLEAR_RELATION_ERRORS':
138
+ case 'CLEAR_ALL_SERVER_ERRORS':
139
+ case 'CLEAR_ALL_ERRORS':
140
+ // These don't affect undo-able state
141
+ break
142
+
143
+ default: {
144
+ const _exhaustive: never = action
145
+ throw new Error(`Unknown action type: ${(_exhaustive as Action).type}`)
146
+ }
147
+ }
148
+
149
+ return { entityKeys, relationKeys, hasManyKeys }
150
+ }
151
+
152
+ /**
153
+ * Merges two StoreAffectedKeys objects, deduplicating keys.
154
+ */
155
+ export function mergeAffectedKeys(
156
+ target: StoreAffectedKeys,
157
+ source: StoreAffectedKeys,
158
+ ): void {
159
+ for (const key of source.entityKeys) {
160
+ if (!target.entityKeys.includes(key)) {
161
+ target.entityKeys.push(key)
162
+ }
163
+ }
164
+ for (const key of source.relationKeys) {
165
+ if (!target.relationKeys.includes(key)) {
166
+ target.relationKeys.push(key)
167
+ }
168
+ }
169
+ for (const key of source.hasManyKeys) {
170
+ if (!target.hasManyKeys.includes(key)) {
171
+ target.hasManyKeys.push(key)
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Creates an empty StoreAffectedKeys object.
178
+ */
179
+ export function createEmptyAffectedKeys(): StoreAffectedKeys {
180
+ return {
181
+ entityKeys: [],
182
+ relationKeys: [],
183
+ hasManyKeys: [],
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Checks if StoreAffectedKeys is empty.
189
+ */
190
+ export function isEmptyAffectedKeys(keys: StoreAffectedKeys): boolean {
191
+ return (
192
+ keys.entityKeys.length === 0 &&
193
+ keys.relationKeys.length === 0 &&
194
+ keys.hasManyKeys.length === 0
195
+ )
196
+ }