@contember/bindx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/ContemberAdapter.d.ts +40 -0
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
- package/dist/adapter/MockAdapter.d.ts +81 -0
- package/dist/adapter/MockAdapter.d.ts.map +1 -0
- package/dist/adapter/MockQueryEngine.d.ts +43 -0
- package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +184 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/brand/ComponentBrand.d.ts +25 -0
- package/dist/brand/ComponentBrand.d.ts.map +1 -0
- package/dist/contember/index.d.ts +10 -0
- package/dist/contember/index.d.ts.map +1 -0
- package/dist/core/ActionDispatcher.d.ts +64 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -0
- package/dist/core/Disposable.d.ts +57 -0
- package/dist/core/Disposable.d.ts.map +1 -0
- package/dist/core/EntityLoader.d.ts +80 -0
- package/dist/core/EntityLoader.d.ts.map +1 -0
- package/dist/core/PersistenceManager.d.ts +126 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/SelectionResolver.d.ts +21 -0
- package/dist/core/SelectionResolver.d.ts.map +1 -0
- package/dist/core/actionClassification.d.ts +48 -0
- package/dist/core/actionClassification.d.ts.map +1 -0
- package/dist/core/actions.d.ts +301 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/dataview/filterHandlers.d.ts +18 -0
- package/dist/dataview/filterHandlers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +3 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/types.d.ts +164 -0
- package/dist/dataview/types.d.ts.map +1 -0
- package/dist/errors/UnfetchedFieldError.d.ts +11 -0
- package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
- package/dist/errors/errorPathResolver.d.ts +52 -0
- package/dist/errors/errorPathResolver.d.ts.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/pathMapper.d.ts +42 -0
- package/dist/errors/pathMapper.d.ts.map +1 -0
- package/dist/errors/types.d.ts +131 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/events/EventEmitter.d.ts +97 -0
- package/dist/events/EventEmitter.d.ts.map +1 -0
- package/dist/events/eventFactory.d.ts +40 -0
- package/dist/events/eventFactory.d.ts.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +264 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/handles/BaseHandle.d.ts +64 -0
- package/dist/handles/BaseHandle.d.ts.map +1 -0
- package/dist/handles/EntityHandle.d.ts +208 -0
- package/dist/handles/EntityHandle.d.ts.map +1 -0
- package/dist/handles/FieldHandle.d.ts +111 -0
- package/dist/handles/FieldHandle.d.ts.map +1 -0
- package/dist/handles/HasManyListHandle.d.ts +166 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -0
- package/dist/handles/HasOneHandle.d.ts +168 -0
- package/dist/handles/HasOneHandle.d.ts.map +1 -0
- package/dist/handles/PlaceholderHandle.d.ts +117 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
- package/dist/handles/index.d.ts +8 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/proxyFactory.d.ts +66 -0
- package/dist/handles/proxyFactory.d.ts.map +1 -0
- package/dist/handles/types.d.ts +430 -0
- package/dist/handles/types.d.ts.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/persistence/BatchPersister.d.ts +142 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -0
- package/dist/persistence/ChangeRegistry.d.ts +87 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
- package/dist/persistence/MutationCollector.d.ts +96 -0
- package/dist/persistence/MutationCollector.d.ts.map +1 -0
- package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
- package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +189 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
- package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
- package/dist/roles/index.d.ts +11 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/types.d.ts +242 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/schema/ContemberSchema.d.ts +98 -0
- package/dist/schema/ContemberSchema.d.ts.map +1 -0
- package/dist/schema/SchemaLoader.d.ts +28 -0
- package/dist/schema/SchemaLoader.d.ts.map +1 -0
- package/dist/schema/SchemaRegistry.d.ts +95 -0
- package/dist/schema/SchemaRegistry.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/roles.d.ts +62 -0
- package/dist/schema/roles.d.ts.map +1 -0
- package/dist/schema/types.d.ts +179 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/selection/SelectionMetaCollector.d.ts +32 -0
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
- package/dist/selection/SelectionScope.d.ts +77 -0
- package/dist/selection/SelectionScope.d.ts.map +1 -0
- package/dist/selection/buildQuery.d.ts +45 -0
- package/dist/selection/buildQuery.d.ts.map +1 -0
- package/dist/selection/createFragment.d.ts +29 -0
- package/dist/selection/createFragment.d.ts.map +1 -0
- package/dist/selection/createSelectionBuilder.d.ts +18 -0
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/queryTypes.d.ts +84 -0
- package/dist/selection/queryTypes.d.ts.map +1 -0
- package/dist/selection/types.d.ts +238 -0
- package/dist/selection/types.d.ts.map +1 -0
- package/dist/store/DirtyTracker.d.ts +26 -0
- package/dist/store/DirtyTracker.d.ts.map +1 -0
- package/dist/store/EntityMetaStore.d.ts +52 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -0
- package/dist/store/EntitySnapshotStore.d.ts +63 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
- package/dist/store/ErrorStore.d.ts +82 -0
- package/dist/store/ErrorStore.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +175 -0
- package/dist/store/RelationStore.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +143 -0
- package/dist/store/SnapshotStore.d.ts.map +1 -0
- package/dist/store/SubscriptionManager.d.ts +90 -0
- package/dist/store/SubscriptionManager.d.ts.map +1 -0
- package/dist/store/TouchedStore.d.ts +24 -0
- package/dist/store/TouchedStore.d.ts.map +1 -0
- package/dist/store/entityId.d.ts +29 -0
- package/dist/store/entityId.d.ts.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/snapshots.d.ts +144 -0
- package/dist/store/snapshots.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +120 -0
- package/dist/undo/UndoManager.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/types.d.ts +74 -0
- package/dist/undo/types.d.ts.map +1 -0
- package/dist/utils/aliasGenerator.d.ts +25 -0
- package/dist/utils/aliasGenerator.d.ts.map +1 -0
- package/dist/utils/deepEqual.d.ts +6 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/adapter/ContemberAdapter.ts +239 -0
- package/src/adapter/MockAdapter.ts +394 -0
- package/src/adapter/MockQueryEngine.ts +236 -0
- package/src/adapter/index.ts +21 -0
- package/src/adapter/types.ts +199 -0
- package/src/brand/ComponentBrand.ts +28 -0
- package/src/contember/index.ts +17 -0
- package/src/core/ActionDispatcher.ts +450 -0
- package/src/core/Disposable.ts +126 -0
- package/src/core/EntityLoader.ts +169 -0
- package/src/core/SelectionResolver.ts +78 -0
- package/src/core/actionClassification.ts +196 -0
- package/src/core/actions.ts +491 -0
- package/src/core/index.ts +20 -0
- package/src/dataview/filterHandlers.ts +443 -0
- package/src/dataview/index.ts +36 -0
- package/src/dataview/types.ts +220 -0
- package/src/errors/UnfetchedFieldError.ts +18 -0
- package/src/errors/errorPathResolver.ts +239 -0
- package/src/errors/index.ts +40 -0
- package/src/errors/pathMapper.ts +40 -0
- package/src/errors/types.ts +223 -0
- package/src/events/EventEmitter.ts +447 -0
- package/src/events/eventFactory.ts +430 -0
- package/src/events/index.ts +89 -0
- package/src/events/types.ts +374 -0
- package/src/handles/BaseHandle.ts +100 -0
- package/src/handles/EntityHandle.ts +538 -0
- package/src/handles/FieldHandle.ts +268 -0
- package/src/handles/HasManyListHandle.ts +500 -0
- package/src/handles/HasOneHandle.ts +500 -0
- package/src/handles/PlaceholderHandle.ts +321 -0
- package/src/handles/index.ts +37 -0
- package/src/handles/proxyFactory.ts +218 -0
- package/src/handles/types.ts +743 -0
- package/src/index.ts +384 -0
- package/src/internal.ts +19 -0
- package/src/persistence/BatchPersister.ts +911 -0
- package/src/persistence/ChangeRegistry.ts +150 -0
- package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
- package/src/persistence/MutationCollector.ts +567 -0
- package/src/persistence/MutationSchemaProvider.ts +33 -0
- package/src/persistence/index.ts +23 -0
- package/src/persistence/types.ts +220 -0
- package/src/schema/ContemberSchema.ts +132 -0
- package/src/schema/SchemaLoader.ts +93 -0
- package/src/schema/SchemaRegistry.ts +286 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/roles.ts +83 -0
- package/src/schema/types.ts +238 -0
- package/src/selection/SelectionMetaCollector.ts +122 -0
- package/src/selection/SelectionScope.ts +208 -0
- package/src/selection/buildQuery.ts +134 -0
- package/src/selection/createFragment.ts +50 -0
- package/src/selection/createSelectionBuilder.ts +283 -0
- package/src/selection/index.ts +31 -0
- package/src/selection/queryTypes.ts +135 -0
- package/src/selection/types.ts +320 -0
- package/src/store/DirtyTracker.ts +117 -0
- package/src/store/EntityMetaStore.ts +143 -0
- package/src/store/EntitySnapshotStore.ts +260 -0
- package/src/store/ErrorStore.ts +251 -0
- package/src/store/RelationStore.ts +696 -0
- package/src/store/SnapshotStore.ts +765 -0
- package/src/store/SubscriptionManager.ts +241 -0
- package/src/store/TouchedStore.ts +42 -0
- package/src/store/entityId.ts +43 -0
- package/src/store/index.ts +2 -0
- package/src/store/snapshots.ts +203 -0
- package/src/undo/UndoManager.ts +418 -0
- package/src/undo/index.ts +9 -0
- package/src/undo/types.ts +84 -0
- package/src/utils/aliasGenerator.ts +56 -0
- package/src/utils/deepEqual.ts +33 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,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
|
+
}
|