@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,199 @@
|
|
|
1
|
+
import type { QuerySpec } from '../selection/buildQuery.js'
|
|
2
|
+
import type { EntityWhere, EntityOrderBy } from '../selection/queryTypes.js'
|
|
3
|
+
import type { ContemberMutationResult } from '../errors/pathMapper.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for query operations
|
|
7
|
+
*/
|
|
8
|
+
export interface QueryOptions {
|
|
9
|
+
/**
|
|
10
|
+
* AbortSignal for cancelling the request
|
|
11
|
+
*/
|
|
12
|
+
readonly signal?: AbortSignal
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Recursive value type for unique where fields (string or number identifiers).
|
|
17
|
+
* Structurally compatible with Input.UniqueWhere from @contember/schema.
|
|
18
|
+
*/
|
|
19
|
+
export type UniqueWhereValue = string | number | { readonly [field: string]: UniqueWhereValue }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Unique field lookup for single entity queries.
|
|
23
|
+
* Supports id and any other unique field.
|
|
24
|
+
*/
|
|
25
|
+
export type EntityUniqueWhere = { readonly [field: string]: UniqueWhereValue }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Query for a single entity by unique field(s)
|
|
29
|
+
*/
|
|
30
|
+
export interface GetQuery {
|
|
31
|
+
readonly type: 'get'
|
|
32
|
+
readonly entityType: string
|
|
33
|
+
readonly by: EntityUniqueWhere
|
|
34
|
+
readonly spec: QuerySpec
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Query for a list of entities
|
|
39
|
+
*/
|
|
40
|
+
export interface ListQuery<TEntity = unknown> {
|
|
41
|
+
readonly type: 'list'
|
|
42
|
+
readonly entityType: string
|
|
43
|
+
readonly filter?: EntityWhere<TEntity>
|
|
44
|
+
readonly orderBy?: readonly EntityOrderBy<TEntity>[]
|
|
45
|
+
readonly limit?: number
|
|
46
|
+
readonly offset?: number
|
|
47
|
+
readonly spec: QuerySpec
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Union of all query types
|
|
52
|
+
*/
|
|
53
|
+
export type Query<TEntity = unknown> = GetQuery | ListQuery<TEntity>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result for a single get query
|
|
57
|
+
*/
|
|
58
|
+
export interface GetQueryResult {
|
|
59
|
+
readonly type: 'get'
|
|
60
|
+
readonly data: Record<string, unknown> | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Result for a list query
|
|
65
|
+
*/
|
|
66
|
+
export interface ListQueryResult {
|
|
67
|
+
readonly type: 'list'
|
|
68
|
+
readonly data: readonly Record<string, unknown>[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Union of query results
|
|
73
|
+
*/
|
|
74
|
+
export type QueryResult = GetQueryResult | ListQueryResult
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Result of a persist operation.
|
|
78
|
+
* Contains detailed error information for error mapping.
|
|
79
|
+
*/
|
|
80
|
+
export interface PersistResult {
|
|
81
|
+
readonly ok: boolean
|
|
82
|
+
readonly errorMessage?: string
|
|
83
|
+
/** Detailed mutation result for error mapping (Contember-specific) */
|
|
84
|
+
readonly mutationResult?: ContemberMutationResult
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Result of a create operation.
|
|
89
|
+
* Contains the created entity data or error information.
|
|
90
|
+
*/
|
|
91
|
+
export interface CreateResult {
|
|
92
|
+
readonly ok: boolean
|
|
93
|
+
readonly errorMessage?: string
|
|
94
|
+
/** The created entity data (when ok is true) */
|
|
95
|
+
readonly data?: Record<string, unknown>
|
|
96
|
+
/** Detailed mutation result for error mapping (Contember-specific) */
|
|
97
|
+
readonly mutationResult?: ContemberMutationResult
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Result of a delete operation.
|
|
102
|
+
*/
|
|
103
|
+
export interface DeleteResult {
|
|
104
|
+
readonly ok: boolean
|
|
105
|
+
readonly errorMessage?: string
|
|
106
|
+
/** Detailed mutation result for error mapping (Contember-specific) */
|
|
107
|
+
readonly mutationResult?: ContemberMutationResult
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A single mutation in a transaction.
|
|
112
|
+
*/
|
|
113
|
+
export interface TransactionMutation {
|
|
114
|
+
readonly entityType: string
|
|
115
|
+
readonly entityId: string
|
|
116
|
+
readonly operation: 'create' | 'update' | 'delete'
|
|
117
|
+
readonly data?: Record<string, unknown>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Result of a single mutation in a transaction.
|
|
122
|
+
*/
|
|
123
|
+
export interface TransactionMutationResult {
|
|
124
|
+
readonly entityType: string
|
|
125
|
+
readonly entityId: string
|
|
126
|
+
readonly ok: boolean
|
|
127
|
+
/** Server-assigned ID for creates */
|
|
128
|
+
readonly persistedId?: string
|
|
129
|
+
readonly errorMessage?: string
|
|
130
|
+
readonly mutationResult?: ContemberMutationResult
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Result of a transaction persist operation.
|
|
135
|
+
* All-or-nothing: if any mutation fails, all are rolled back.
|
|
136
|
+
*/
|
|
137
|
+
export interface TransactionResult {
|
|
138
|
+
/** Whether all mutations succeeded */
|
|
139
|
+
readonly ok: boolean
|
|
140
|
+
/** Results per mutation, in same order as input */
|
|
141
|
+
readonly results: readonly TransactionMutationResult[]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Interface for backend adapters.
|
|
146
|
+
* Implement this to connect bindx to your data source.
|
|
147
|
+
*/
|
|
148
|
+
export interface BackendAdapter {
|
|
149
|
+
/**
|
|
150
|
+
* Execute one or more queries in a single request.
|
|
151
|
+
* When multiple queries are provided, the adapter should batch them
|
|
152
|
+
* into a single network request where possible.
|
|
153
|
+
*
|
|
154
|
+
* @param queries - Array of queries to execute
|
|
155
|
+
* @param options - Optional query options including AbortSignal
|
|
156
|
+
* @returns Array of results in the same order as queries
|
|
157
|
+
*/
|
|
158
|
+
query(queries: readonly Query[], options?: QueryOptions): Promise<QueryResult[]>
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Persists changes to an entity.
|
|
162
|
+
* Returns a result object with success/failure status and error details.
|
|
163
|
+
*
|
|
164
|
+
* @param entityType - The type/name of the entity
|
|
165
|
+
* @param id - The entity's unique identifier
|
|
166
|
+
* @param changes - The changed fields to persist
|
|
167
|
+
* @returns Result with ok status and optional error details
|
|
168
|
+
*/
|
|
169
|
+
persist(entityType: string, id: string, changes: Record<string, unknown>): Promise<PersistResult>
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a new entity.
|
|
173
|
+
* Returns a result object with the created entity or error details.
|
|
174
|
+
*
|
|
175
|
+
* @param entityType - The type/name of the entity
|
|
176
|
+
* @param data - The initial data for the entity
|
|
177
|
+
* @returns Result with ok status, created entity data, and optional error details
|
|
178
|
+
*/
|
|
179
|
+
create?(entityType: string, data: Record<string, unknown>): Promise<CreateResult>
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Deletes an entity.
|
|
183
|
+
* Returns a result object with success/failure status.
|
|
184
|
+
*
|
|
185
|
+
* @param entityType - The type/name of the entity
|
|
186
|
+
* @param id - The entity's unique identifier
|
|
187
|
+
* @returns Result with ok status and optional error details
|
|
188
|
+
*/
|
|
189
|
+
delete?(entityType: string, id: string): Promise<DeleteResult>
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Persists multiple mutations in a single transaction.
|
|
193
|
+
* All-or-nothing: if any mutation fails, all are rolled back.
|
|
194
|
+
*
|
|
195
|
+
* @param mutations - Array of mutations to execute
|
|
196
|
+
* @returns Result with success status and per-mutation results
|
|
197
|
+
*/
|
|
198
|
+
persistTransaction?(mutations: readonly TransactionMutation[]): Promise<TransactionResult>
|
|
199
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentBrand provides unique identity for each component created with createComponent.
|
|
3
|
+
* Each instance has a unique runtime Symbol for brand validation.
|
|
4
|
+
* The class structure itself provides nominal typing in TypeScript.
|
|
5
|
+
*/
|
|
6
|
+
export class ComponentBrand {
|
|
7
|
+
/**
|
|
8
|
+
* Private property to ensure nominal typing.
|
|
9
|
+
* Each class instance is considered a unique type.
|
|
10
|
+
*/
|
|
11
|
+
private readonly __nominal = true
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runtime symbol for brand validation.
|
|
15
|
+
* Used to check that EntityRef includes required component brands.
|
|
16
|
+
*/
|
|
17
|
+
readonly brandSymbol: symbol
|
|
18
|
+
|
|
19
|
+
constructor(public readonly name: string) {
|
|
20
|
+
this.brandSymbol = Symbol(`component_${name}_${Math.random().toString(36).slice(2)}`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Type alias for any ComponentBrand.
|
|
26
|
+
* Used as default/base type in generic constraints.
|
|
27
|
+
*/
|
|
28
|
+
export type AnyBrand = ComponentBrand
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contember-specific utilities for bindx
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export unified MutationCollector from persistence
|
|
8
|
+
export { MutationCollector, type EntityMutationResult } from '../persistence/MutationCollector.js'
|
|
9
|
+
export { ContemberSchemaMutationAdapter } from '../persistence/ContemberSchemaMutationAdapter.js'
|
|
10
|
+
export type { MutationSchemaProvider } from '../persistence/MutationSchemaProvider.js'
|
|
11
|
+
|
|
12
|
+
// Re-export commonly used types from @contember/client-content
|
|
13
|
+
export type {
|
|
14
|
+
SchemaNames,
|
|
15
|
+
SchemaEntityNames,
|
|
16
|
+
ContentClientInput,
|
|
17
|
+
} from '@contember/client-content'
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import type { SnapshotStore } from '../store/SnapshotStore.js'
|
|
2
|
+
import type { Action } from './actions.js'
|
|
3
|
+
import { EventEmitter } from '../events/EventEmitter.js'
|
|
4
|
+
import {
|
|
5
|
+
createBeforeEvent,
|
|
6
|
+
createAfterEvent,
|
|
7
|
+
captureStateBeforeAction,
|
|
8
|
+
} from '../events/eventFactory.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ActionDispatcher centralizes all state mutations.
|
|
12
|
+
* All changes to the store go through this dispatcher.
|
|
13
|
+
*
|
|
14
|
+
* Benefits:
|
|
15
|
+
* - Single point of control for all mutations
|
|
16
|
+
* - Easy to add logging, debugging, middleware
|
|
17
|
+
* - Ensures consistent state updates
|
|
18
|
+
* - Enables action replay/undo if needed
|
|
19
|
+
* - Event system for subscriptions and interceptors
|
|
20
|
+
*/
|
|
21
|
+
export class ActionDispatcher {
|
|
22
|
+
private readonly middlewares: ActionMiddleware[] = []
|
|
23
|
+
private readonly eventEmitter: EventEmitter
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly store: SnapshotStore,
|
|
27
|
+
eventEmitter?: EventEmitter,
|
|
28
|
+
) {
|
|
29
|
+
this.eventEmitter = eventEmitter ?? new EventEmitter()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the event emitter for external access.
|
|
34
|
+
*/
|
|
35
|
+
getEventEmitter(): EventEmitter {
|
|
36
|
+
return this.eventEmitter
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dispatches an action to update the store.
|
|
41
|
+
* Runs synchronous interceptors (before-events) and emits after-events.
|
|
42
|
+
* Async interceptors are skipped with a warning — use dispatchAsync() for those.
|
|
43
|
+
*/
|
|
44
|
+
dispatch(action: Action): void {
|
|
45
|
+
// Run through middlewares first
|
|
46
|
+
for (const middleware of this.middlewares) {
|
|
47
|
+
const result = middleware(action, this.store)
|
|
48
|
+
if (result === false) {
|
|
49
|
+
// Middleware can cancel the action
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create and run before event through interceptors (synchronously)
|
|
55
|
+
const beforeEvent = createBeforeEvent(action, this.store)
|
|
56
|
+
if (beforeEvent) {
|
|
57
|
+
const result = this.eventEmitter.runInterceptorsSync(beforeEvent)
|
|
58
|
+
if (result === null) {
|
|
59
|
+
// Interceptor cancelled the action
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
// If event was modified, update action accordingly
|
|
63
|
+
if (result !== beforeEvent) {
|
|
64
|
+
action = this.updateActionFromEvent(action, result)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Capture state before execution (for after events)
|
|
69
|
+
const stateBefore = captureStateBeforeAction(action, this.store)
|
|
70
|
+
|
|
71
|
+
// Execute the action
|
|
72
|
+
this.execute(action)
|
|
73
|
+
|
|
74
|
+
// Emit after event
|
|
75
|
+
const afterEvent = createAfterEvent(action, stateBefore, this.store)
|
|
76
|
+
if (afterEvent) {
|
|
77
|
+
this.eventEmitter.emit(afterEvent)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Dispatches an action with full interceptor support.
|
|
83
|
+
* Returns true if the action was executed, false if it was cancelled by an interceptor.
|
|
84
|
+
*/
|
|
85
|
+
async dispatchAsync(action: Action): Promise<boolean> {
|
|
86
|
+
// Run through middlewares first
|
|
87
|
+
for (const middleware of this.middlewares) {
|
|
88
|
+
const result = middleware(action, this.store)
|
|
89
|
+
if (result === false) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create and run before event through interceptors
|
|
95
|
+
const beforeEvent = createBeforeEvent(action, this.store)
|
|
96
|
+
if (beforeEvent) {
|
|
97
|
+
const result = await this.eventEmitter.runInterceptors(beforeEvent)
|
|
98
|
+
if (result === null) {
|
|
99
|
+
// Interceptor cancelled the action
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
// If event was modified, update action accordingly
|
|
103
|
+
if (result !== beforeEvent) {
|
|
104
|
+
action = this.updateActionFromEvent(action, result)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Capture state before execution (for after events)
|
|
109
|
+
const stateBefore = captureStateBeforeAction(action, this.store)
|
|
110
|
+
|
|
111
|
+
// Execute the action
|
|
112
|
+
this.execute(action)
|
|
113
|
+
|
|
114
|
+
// Emit after event
|
|
115
|
+
const afterEvent = createAfterEvent(action, stateBefore, this.store)
|
|
116
|
+
if (afterEvent) {
|
|
117
|
+
this.eventEmitter.emit(afterEvent)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Updates an action based on a modified before event.
|
|
125
|
+
* Used when an interceptor modifies the event.
|
|
126
|
+
*/
|
|
127
|
+
private updateActionFromEvent(action: Action, event: ReturnType<typeof createBeforeEvent>): Action {
|
|
128
|
+
if (!event) return action
|
|
129
|
+
|
|
130
|
+
switch (action.type) {
|
|
131
|
+
case 'SET_FIELD':
|
|
132
|
+
if (event.type === 'field:changing') {
|
|
133
|
+
return {
|
|
134
|
+
...action,
|
|
135
|
+
value: event.newValue,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
case 'CONNECT_RELATION':
|
|
141
|
+
if (event.type === 'relation:connecting') {
|
|
142
|
+
return {
|
|
143
|
+
...action,
|
|
144
|
+
targetId: event.targetId,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
// Other action types don't have modifiable properties in their before events
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return action
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Adds a middleware to the dispatcher.
|
|
157
|
+
* Middlewares are called in order before action execution.
|
|
158
|
+
*/
|
|
159
|
+
addMiddleware(middleware: ActionMiddleware): void {
|
|
160
|
+
this.middlewares.push(middleware)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Removes a middleware from the dispatcher.
|
|
165
|
+
*/
|
|
166
|
+
removeMiddleware(middleware: ActionMiddleware): void {
|
|
167
|
+
const index = this.middlewares.indexOf(middleware)
|
|
168
|
+
if (index !== -1) {
|
|
169
|
+
this.middlewares.splice(index, 1)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Executes an action, updating the store.
|
|
175
|
+
*/
|
|
176
|
+
private execute(action: Action): void {
|
|
177
|
+
switch (action.type) {
|
|
178
|
+
case 'SET_FIELD':
|
|
179
|
+
this.store.setFieldValue(
|
|
180
|
+
action.entityType,
|
|
181
|
+
action.entityId,
|
|
182
|
+
action.fieldPath,
|
|
183
|
+
action.value,
|
|
184
|
+
)
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
case 'RESET_ENTITY':
|
|
188
|
+
this.store.resetEntity(action.entityType, action.entityId)
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
case 'COMMIT_ENTITY':
|
|
192
|
+
this.store.commitEntity(action.entityType, action.entityId)
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
case 'SET_ENTITY_DATA':
|
|
196
|
+
this.store.setEntityData(
|
|
197
|
+
action.entityType,
|
|
198
|
+
action.entityId,
|
|
199
|
+
action.data,
|
|
200
|
+
action.isServerData,
|
|
201
|
+
)
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
case 'CONNECT_RELATION':
|
|
205
|
+
this.store.setRelation(
|
|
206
|
+
action.entityType,
|
|
207
|
+
action.entityId,
|
|
208
|
+
action.fieldName,
|
|
209
|
+
{
|
|
210
|
+
currentId: action.targetId,
|
|
211
|
+
state: 'connected',
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
case 'DISCONNECT_RELATION':
|
|
217
|
+
this.store.setRelation(
|
|
218
|
+
action.entityType,
|
|
219
|
+
action.entityId,
|
|
220
|
+
action.fieldName,
|
|
221
|
+
{
|
|
222
|
+
currentId: null,
|
|
223
|
+
state: 'disconnected',
|
|
224
|
+
placeholderData: {},
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
case 'DELETE_RELATION':
|
|
230
|
+
this.store.setRelation(
|
|
231
|
+
action.entityType,
|
|
232
|
+
action.entityId,
|
|
233
|
+
action.fieldName,
|
|
234
|
+
{
|
|
235
|
+
state: 'deleted',
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
case 'RESET_RELATION':
|
|
241
|
+
this.store.resetRelation(
|
|
242
|
+
action.entityType,
|
|
243
|
+
action.entityId,
|
|
244
|
+
action.fieldName,
|
|
245
|
+
)
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
case 'COMMIT_RELATION':
|
|
249
|
+
this.store.commitRelation(
|
|
250
|
+
action.entityType,
|
|
251
|
+
action.entityId,
|
|
252
|
+
action.fieldName,
|
|
253
|
+
)
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
case 'SET_PLACEHOLDER_DATA': {
|
|
257
|
+
const relation = this.store.getRelation(
|
|
258
|
+
action.entityType,
|
|
259
|
+
action.entityId,
|
|
260
|
+
action.fieldName,
|
|
261
|
+
)
|
|
262
|
+
const currentPlaceholderData = relation?.placeholderData ?? {}
|
|
263
|
+
const newPlaceholderData = setNestedValue(
|
|
264
|
+
{ ...currentPlaceholderData },
|
|
265
|
+
action.fieldPath,
|
|
266
|
+
action.value,
|
|
267
|
+
)
|
|
268
|
+
this.store.setRelation(
|
|
269
|
+
action.entityType,
|
|
270
|
+
action.entityId,
|
|
271
|
+
action.fieldName,
|
|
272
|
+
{ placeholderData: newPlaceholderData },
|
|
273
|
+
)
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'ADD_TO_LIST': {
|
|
278
|
+
// Create entity if itemId not provided
|
|
279
|
+
const itemId = action.itemId ?? this.store.createEntity(action.targetType, action.itemData)
|
|
280
|
+
// Add to has-many relation
|
|
281
|
+
this.store.addToHasMany(
|
|
282
|
+
action.entityType,
|
|
283
|
+
action.entityId,
|
|
284
|
+
action.fieldName,
|
|
285
|
+
itemId,
|
|
286
|
+
)
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'REMOVE_FROM_LIST':
|
|
291
|
+
this.store.removeFromHasMany(
|
|
292
|
+
action.entityType,
|
|
293
|
+
action.entityId,
|
|
294
|
+
action.fieldName,
|
|
295
|
+
action.itemKey,
|
|
296
|
+
)
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
case 'MOVE_IN_LIST':
|
|
300
|
+
this.store.moveInHasMany(
|
|
301
|
+
action.entityType,
|
|
302
|
+
action.entityId,
|
|
303
|
+
action.fieldName,
|
|
304
|
+
action.fromIndex,
|
|
305
|
+
action.toIndex,
|
|
306
|
+
)
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
case 'SET_LOAD_STATE':
|
|
310
|
+
this.store.setLoadState(
|
|
311
|
+
action.entityType,
|
|
312
|
+
action.entityId,
|
|
313
|
+
action.status,
|
|
314
|
+
action.error,
|
|
315
|
+
)
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
case 'SET_PERSISTING':
|
|
319
|
+
this.store.setPersisting(
|
|
320
|
+
action.entityType,
|
|
321
|
+
action.entityId,
|
|
322
|
+
action.isPersisting,
|
|
323
|
+
)
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
case 'ADD_FIELD_ERROR':
|
|
327
|
+
this.store.addFieldError(
|
|
328
|
+
action.entityType,
|
|
329
|
+
action.entityId,
|
|
330
|
+
action.fieldName,
|
|
331
|
+
action.error,
|
|
332
|
+
)
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
case 'CLEAR_FIELD_ERRORS':
|
|
336
|
+
this.store.clearFieldErrors(
|
|
337
|
+
action.entityType,
|
|
338
|
+
action.entityId,
|
|
339
|
+
action.fieldName,
|
|
340
|
+
action.source,
|
|
341
|
+
)
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
case 'ADD_ENTITY_ERROR':
|
|
345
|
+
this.store.addEntityError(
|
|
346
|
+
action.entityType,
|
|
347
|
+
action.entityId,
|
|
348
|
+
action.error,
|
|
349
|
+
)
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
case 'CLEAR_ENTITY_ERRORS':
|
|
353
|
+
this.store.clearEntityErrors(
|
|
354
|
+
action.entityType,
|
|
355
|
+
action.entityId,
|
|
356
|
+
action.source,
|
|
357
|
+
)
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
case 'ADD_RELATION_ERROR':
|
|
361
|
+
this.store.addRelationError(
|
|
362
|
+
action.entityType,
|
|
363
|
+
action.entityId,
|
|
364
|
+
action.relationName,
|
|
365
|
+
action.error,
|
|
366
|
+
)
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
case 'CLEAR_RELATION_ERRORS':
|
|
370
|
+
this.store.clearRelationErrors(
|
|
371
|
+
action.entityType,
|
|
372
|
+
action.entityId,
|
|
373
|
+
action.relationName,
|
|
374
|
+
action.source,
|
|
375
|
+
)
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
case 'CLEAR_ALL_SERVER_ERRORS':
|
|
379
|
+
this.store.clearAllServerErrors(
|
|
380
|
+
action.entityType,
|
|
381
|
+
action.entityId,
|
|
382
|
+
)
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
case 'CLEAR_ALL_ERRORS':
|
|
386
|
+
this.store.clearAllErrors(
|
|
387
|
+
action.entityType,
|
|
388
|
+
action.entityId,
|
|
389
|
+
)
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
default:
|
|
393
|
+
// Exhaustive check
|
|
394
|
+
const _exhaustive: never = action
|
|
395
|
+
throw new Error(`Unknown action type: ${(_exhaustive as Action).type}`)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Middleware function type.
|
|
402
|
+
* Can inspect/modify actions before execution.
|
|
403
|
+
* Return false to cancel the action.
|
|
404
|
+
*/
|
|
405
|
+
export type ActionMiddleware = (action: Action, store: SnapshotStore) => boolean | void
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Creates a logging middleware for debugging.
|
|
409
|
+
*/
|
|
410
|
+
export function createLoggingMiddleware(
|
|
411
|
+
prefix: string = '[Bindx]',
|
|
412
|
+
): ActionMiddleware {
|
|
413
|
+
return (action) => {
|
|
414
|
+
console.log(`${prefix} ${action.type}`, action)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ==================== Helper Functions ====================
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Sets a nested value in an object, returning a new object.
|
|
422
|
+
*/
|
|
423
|
+
function setNestedValue(
|
|
424
|
+
obj: Record<string, unknown>,
|
|
425
|
+
path: string[],
|
|
426
|
+
value: unknown,
|
|
427
|
+
): Record<string, unknown> {
|
|
428
|
+
if (path.length === 0) return obj
|
|
429
|
+
|
|
430
|
+
const result = { ...obj }
|
|
431
|
+
let current: Record<string, unknown> = result
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
434
|
+
const key = path[i]!
|
|
435
|
+
const nextValue = current[key]
|
|
436
|
+
|
|
437
|
+
if (typeof nextValue === 'object' && nextValue !== null) {
|
|
438
|
+
current[key] = { ...(nextValue as Record<string, unknown>) }
|
|
439
|
+
} else {
|
|
440
|
+
current[key] = {}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
current = current[key] as Record<string, unknown>
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const lastKey = path[path.length - 1]!
|
|
447
|
+
current[lastKey] = value
|
|
448
|
+
|
|
449
|
+
return result
|
|
450
|
+
}
|