@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,320 @@
|
|
|
1
|
+
import type { ComponentBrand, AnyBrand } from '../brand/ComponentBrand.js'
|
|
2
|
+
import type { EntityWhere, EntityOrderBy } from './queryTypes.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Symbol used to store selection metadata on builder objects
|
|
6
|
+
*/
|
|
7
|
+
export const SELECTION_META = Symbol('SELECTION_META')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for has-many relation selection with typed filter/orderBy
|
|
11
|
+
*
|
|
12
|
+
* @typeParam TItem - The related entity type (array item type)
|
|
13
|
+
* @typeParam TAlias - Optional alias for the field
|
|
14
|
+
*/
|
|
15
|
+
export interface HasManyOptions<
|
|
16
|
+
TItem = unknown,
|
|
17
|
+
TAlias extends string = string,
|
|
18
|
+
> {
|
|
19
|
+
as?: TAlias
|
|
20
|
+
filter?: EntityWhere<TItem>
|
|
21
|
+
orderBy?: readonly EntityOrderBy<TItem>[]
|
|
22
|
+
limit?: number
|
|
23
|
+
offset?: number
|
|
24
|
+
totalCount?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Metadata for a selected field (runtime)
|
|
29
|
+
*/
|
|
30
|
+
export interface SelectionFieldMeta {
|
|
31
|
+
/** Original field name in the entity */
|
|
32
|
+
fieldName: string
|
|
33
|
+
/** Alias (output key) - defaults to fieldName */
|
|
34
|
+
alias: string
|
|
35
|
+
/** Full path to this field (for JSX components) */
|
|
36
|
+
path: string[]
|
|
37
|
+
/** Whether this is a relation (has nested fields) */
|
|
38
|
+
isRelation: boolean
|
|
39
|
+
/** Whether this is an array/has-many relation */
|
|
40
|
+
isArray: boolean
|
|
41
|
+
/** For relations: nested selection metadata */
|
|
42
|
+
nested?: SelectionMeta
|
|
43
|
+
/** For has-many: additional parameters */
|
|
44
|
+
hasManyParams?: {
|
|
45
|
+
filter?: unknown
|
|
46
|
+
orderBy?: unknown
|
|
47
|
+
limit?: number
|
|
48
|
+
offset?: number
|
|
49
|
+
totalCount?: boolean
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Runtime metadata for a complete selection
|
|
55
|
+
*/
|
|
56
|
+
export interface SelectionMeta {
|
|
57
|
+
fields: Map<string, SelectionFieldMeta>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract item type from array
|
|
62
|
+
*/
|
|
63
|
+
type ArrayItemType<T> = T extends Array<infer U> ? U : never
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A fragment defined with the fluent builder
|
|
67
|
+
*
|
|
68
|
+
* @typeParam TModel - The entity model type
|
|
69
|
+
* @typeParam TResult - The selected fields result type
|
|
70
|
+
* @typeParam TBrand - Component brand type for validation (defaults to AnyBrand)
|
|
71
|
+
*/
|
|
72
|
+
export interface FluentFragment<
|
|
73
|
+
TModel,
|
|
74
|
+
TResult,
|
|
75
|
+
TBrand extends AnyBrand = AnyBrand,
|
|
76
|
+
> {
|
|
77
|
+
/** Runtime metadata */
|
|
78
|
+
readonly __meta: SelectionMeta
|
|
79
|
+
/** Type brand for result type */
|
|
80
|
+
readonly __resultType: TResult
|
|
81
|
+
/** Type brand for model type */
|
|
82
|
+
readonly __modelType: TModel
|
|
83
|
+
/** Marker for type checking */
|
|
84
|
+
readonly __isFragment: true
|
|
85
|
+
/** Component brand for type-level tracking */
|
|
86
|
+
readonly __brand?: TBrand
|
|
87
|
+
/** Runtime brand symbols for validation */
|
|
88
|
+
readonly __brands?: Set<symbol>
|
|
89
|
+
/** Runtime roles for validation */
|
|
90
|
+
readonly __roles?: readonly string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Scalar method on selection builder - selects a scalar field
|
|
95
|
+
*/
|
|
96
|
+
export interface ScalarMethod<TEntity, K extends keyof TEntity, TSelected extends object, THasManyParams extends object> {
|
|
97
|
+
/** Select field with original name */
|
|
98
|
+
(): SelectionBuilder<TEntity, TSelected & Pick<TEntity, K>, THasManyParams>
|
|
99
|
+
/** Select field with alias */
|
|
100
|
+
<TAlias extends string>(options: { as: TAlias }): SelectionBuilder<
|
|
101
|
+
TEntity,
|
|
102
|
+
TSelected & { [P in TAlias]: TEntity[K] },
|
|
103
|
+
THasManyParams
|
|
104
|
+
>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Has-one method on selection builder - selects a relation to single entity
|
|
109
|
+
*/
|
|
110
|
+
export interface HasOneMethod<
|
|
111
|
+
TEntity,
|
|
112
|
+
K extends keyof TEntity,
|
|
113
|
+
TRelated,
|
|
114
|
+
TSelected extends object,
|
|
115
|
+
THasManyParams extends object,
|
|
116
|
+
> {
|
|
117
|
+
/** Select with inline callback */
|
|
118
|
+
<TNestedSelected extends object>(
|
|
119
|
+
selector: (builder: SelectionBuilder<TRelated>) => SelectionBuilder<TRelated, TNestedSelected, object>,
|
|
120
|
+
): SelectionBuilder<TEntity, TSelected & { [P in K & string]: TNestedSelected }, THasManyParams>
|
|
121
|
+
|
|
122
|
+
/** Select with fragment */
|
|
123
|
+
<TFragmentResult extends object>(
|
|
124
|
+
fragment: FluentFragment<TRelated, TFragmentResult>,
|
|
125
|
+
): SelectionBuilder<TEntity, TSelected & { [P in K & string]: TFragmentResult }, THasManyParams>
|
|
126
|
+
|
|
127
|
+
/** Select with multiple fragments (merged) - result type is intersection of all fragment result types */
|
|
128
|
+
<T1 extends object, T2 extends object>(
|
|
129
|
+
fragment1: FluentFragment<TRelated, T1>,
|
|
130
|
+
fragment2: FluentFragment<TRelated, T2>,
|
|
131
|
+
): SelectionBuilder<TEntity, TSelected & { [P in K & string]: T1 & T2 }, THasManyParams>
|
|
132
|
+
|
|
133
|
+
/** Select with three fragments (merged) */
|
|
134
|
+
<T1 extends object, T2 extends object, T3 extends object>(
|
|
135
|
+
fragment1: FluentFragment<TRelated, T1>,
|
|
136
|
+
fragment2: FluentFragment<TRelated, T2>,
|
|
137
|
+
fragment3: FluentFragment<TRelated, T3>,
|
|
138
|
+
): SelectionBuilder<TEntity, TSelected & { [P in K & string]: T1 & T2 & T3 }, THasManyParams>
|
|
139
|
+
|
|
140
|
+
/** Select with alias and callback */
|
|
141
|
+
<TAlias extends string, TNestedSelected extends object>(
|
|
142
|
+
options: { as: TAlias },
|
|
143
|
+
selector: (builder: SelectionBuilder<TRelated>) => SelectionBuilder<TRelated, TNestedSelected, object>,
|
|
144
|
+
): SelectionBuilder<TEntity, TSelected & { [P in TAlias]: TNestedSelected }, THasManyParams>
|
|
145
|
+
|
|
146
|
+
/** Select with alias and fragment */
|
|
147
|
+
<TAlias extends string, TFragmentResult extends object>(
|
|
148
|
+
options: { as: TAlias },
|
|
149
|
+
fragment: FluentFragment<TRelated, TFragmentResult>,
|
|
150
|
+
): SelectionBuilder<TEntity, TSelected & { [P in TAlias]: TFragmentResult }, THasManyParams>
|
|
151
|
+
|
|
152
|
+
/** Select with alias and two fragments (merged) */
|
|
153
|
+
<TAlias extends string, T1 extends object, T2 extends object>(
|
|
154
|
+
options: { as: TAlias },
|
|
155
|
+
fragment1: FluentFragment<TRelated, T1>,
|
|
156
|
+
fragment2: FluentFragment<TRelated, T2>,
|
|
157
|
+
): SelectionBuilder<TEntity, TSelected & { [P in TAlias]: T1 & T2 }, THasManyParams>
|
|
158
|
+
|
|
159
|
+
/** Select with alias and three fragments (merged) */
|
|
160
|
+
<TAlias extends string, T1 extends object, T2 extends object, T3 extends object>(
|
|
161
|
+
options: { as: TAlias },
|
|
162
|
+
fragment1: FluentFragment<TRelated, T1>,
|
|
163
|
+
fragment2: FluentFragment<TRelated, T2>,
|
|
164
|
+
fragment3: FluentFragment<TRelated, T3>,
|
|
165
|
+
): SelectionBuilder<TEntity, TSelected & { [P in TAlias]: T1 & T2 & T3 }, THasManyParams>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve alias from options or fall back to field name
|
|
170
|
+
*/
|
|
171
|
+
type ResolveAlias<K, TOptions> = TOptions extends { as: infer A extends string } ? A : K & string
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Has-many method on selection builder - selects a relation to multiple entities
|
|
175
|
+
*/
|
|
176
|
+
export interface HasManyMethod<
|
|
177
|
+
TEntity,
|
|
178
|
+
K extends keyof TEntity,
|
|
179
|
+
TItem,
|
|
180
|
+
TSelected extends object,
|
|
181
|
+
THasManyParams extends object,
|
|
182
|
+
> {
|
|
183
|
+
/** Select with inline callback (no options) */
|
|
184
|
+
<TNestedSelected extends object>(
|
|
185
|
+
selector: (builder: SelectionBuilder<TItem>) => SelectionBuilder<TItem, TNestedSelected, object>,
|
|
186
|
+
): SelectionBuilder<
|
|
187
|
+
TEntity,
|
|
188
|
+
TSelected & { [P in K & string]: TNestedSelected[] },
|
|
189
|
+
THasManyParams & { [P in K & string]: object }
|
|
190
|
+
>
|
|
191
|
+
|
|
192
|
+
/** Select with fragment (no options) */
|
|
193
|
+
<TFragmentResult extends object>(
|
|
194
|
+
fragment: FluentFragment<TItem, TFragmentResult>,
|
|
195
|
+
): SelectionBuilder<
|
|
196
|
+
TEntity,
|
|
197
|
+
TSelected & { [P in K & string]: TFragmentResult[] },
|
|
198
|
+
THasManyParams & { [P in K & string]: object }
|
|
199
|
+
>
|
|
200
|
+
|
|
201
|
+
/** Select with two fragments (merged, no options) */
|
|
202
|
+
<T1 extends object, T2 extends object>(
|
|
203
|
+
fragment1: FluentFragment<TItem, T1>,
|
|
204
|
+
fragment2: FluentFragment<TItem, T2>,
|
|
205
|
+
): SelectionBuilder<
|
|
206
|
+
TEntity,
|
|
207
|
+
TSelected & { [P in K & string]: (T1 & T2)[] },
|
|
208
|
+
THasManyParams & { [P in K & string]: object }
|
|
209
|
+
>
|
|
210
|
+
|
|
211
|
+
/** Select with three fragments (merged, no options) */
|
|
212
|
+
<T1 extends object, T2 extends object, T3 extends object>(
|
|
213
|
+
fragment1: FluentFragment<TItem, T1>,
|
|
214
|
+
fragment2: FluentFragment<TItem, T2>,
|
|
215
|
+
fragment3: FluentFragment<TItem, T3>,
|
|
216
|
+
): SelectionBuilder<
|
|
217
|
+
TEntity,
|
|
218
|
+
TSelected & { [P in K & string]: (T1 & T2 & T3)[] },
|
|
219
|
+
THasManyParams & { [P in K & string]: object }
|
|
220
|
+
>
|
|
221
|
+
|
|
222
|
+
/** Select with options and callback */
|
|
223
|
+
<TOptions extends HasManyOptions<TItem>, TNestedSelected extends object>(
|
|
224
|
+
options: TOptions,
|
|
225
|
+
selector: (builder: SelectionBuilder<TItem>) => SelectionBuilder<TItem, TNestedSelected, object>,
|
|
226
|
+
): SelectionBuilder<
|
|
227
|
+
TEntity,
|
|
228
|
+
TSelected & { [P in ResolveAlias<K, TOptions>]: TNestedSelected[] },
|
|
229
|
+
THasManyParams & { [P in ResolveAlias<K, TOptions>]: TOptions }
|
|
230
|
+
>
|
|
231
|
+
|
|
232
|
+
/** Select with options and fragment */
|
|
233
|
+
<TOptions extends HasManyOptions<TItem>, TFragmentResult extends object>(
|
|
234
|
+
options: TOptions,
|
|
235
|
+
fragment: FluentFragment<TItem, TFragmentResult>,
|
|
236
|
+
): SelectionBuilder<
|
|
237
|
+
TEntity,
|
|
238
|
+
TSelected & { [P in ResolveAlias<K, TOptions>]: TFragmentResult[] },
|
|
239
|
+
THasManyParams & { [P in ResolveAlias<K, TOptions>]: TOptions }
|
|
240
|
+
>
|
|
241
|
+
|
|
242
|
+
/** Select with options and two fragments (merged) */
|
|
243
|
+
<TOptions extends HasManyOptions<TItem>, T1 extends object, T2 extends object>(
|
|
244
|
+
options: TOptions,
|
|
245
|
+
fragment1: FluentFragment<TItem, T1>,
|
|
246
|
+
fragment2: FluentFragment<TItem, T2>,
|
|
247
|
+
): SelectionBuilder<
|
|
248
|
+
TEntity,
|
|
249
|
+
TSelected & { [P in ResolveAlias<K, TOptions>]: (T1 & T2)[] },
|
|
250
|
+
THasManyParams & { [P in ResolveAlias<K, TOptions>]: TOptions }
|
|
251
|
+
>
|
|
252
|
+
|
|
253
|
+
/** Select with options and three fragments (merged) */
|
|
254
|
+
<TOptions extends HasManyOptions<TItem>, T1 extends object, T2 extends object, T3 extends object>(
|
|
255
|
+
options: TOptions,
|
|
256
|
+
fragment1: FluentFragment<TItem, T1>,
|
|
257
|
+
fragment2: FluentFragment<TItem, T2>,
|
|
258
|
+
fragment3: FluentFragment<TItem, T3>,
|
|
259
|
+
): SelectionBuilder<
|
|
260
|
+
TEntity,
|
|
261
|
+
TSelected & { [P in ResolveAlias<K, TOptions>]: (T1 & T2 & T3)[] },
|
|
262
|
+
THasManyParams & { [P in ResolveAlias<K, TOptions>]: TOptions }
|
|
263
|
+
>
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Maps entity fields to their corresponding builder methods
|
|
268
|
+
*/
|
|
269
|
+
type SelectionBuilderMethods<TEntity, TSelected extends object, THasManyParams extends object> = {
|
|
270
|
+
[K in keyof TEntity]-?: TEntity[K] extends Array<infer U>
|
|
271
|
+
? HasManyMethod<TEntity, K, U, TSelected, THasManyParams>
|
|
272
|
+
: NonNullable<TEntity[K]> extends object
|
|
273
|
+
? HasOneMethod<TEntity, K, NonNullable<TEntity[K]>, TSelected, THasManyParams>
|
|
274
|
+
: ScalarMethod<TEntity, K, TSelected, THasManyParams>
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The main fluent selection builder interface.
|
|
279
|
+
*
|
|
280
|
+
* @typeParam TEntity - The entity type being selected from
|
|
281
|
+
* @typeParam TSelected - Accumulated type of selected fields (grows with each selection)
|
|
282
|
+
* @typeParam THasManyParams - Record tracking has-many parameters for alias consistency
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* // Select scalar fields
|
|
287
|
+
* e.id().title().content()
|
|
288
|
+
* // Type: { id: string, title: string, content: string }
|
|
289
|
+
*
|
|
290
|
+
* // Select with alias
|
|
291
|
+
* e.title({ as: 'headline' })
|
|
292
|
+
* // Type: { headline: string }
|
|
293
|
+
*
|
|
294
|
+
* // Select has-one relation
|
|
295
|
+
* e.author(a => a.name().email())
|
|
296
|
+
* // Type: { author: { name: string, email: string } }
|
|
297
|
+
*
|
|
298
|
+
* // Select has-many with options
|
|
299
|
+
* e.tags({ filter: { active: true } }, t => t.name())
|
|
300
|
+
* // Type: { tags: { name: string }[] }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
export type SelectionBuilder<TEntity, TSelected extends object = object, THasManyParams extends object = object> =
|
|
304
|
+
SelectionBuilderMethods<TEntity, TSelected, THasManyParams> & {
|
|
305
|
+
[SELECTION_META]: SelectionMeta
|
|
306
|
+
/** Type brand for the selected result */
|
|
307
|
+
readonly __selected: TSelected
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Extracts the selected type from a SelectionBuilder
|
|
312
|
+
*/
|
|
313
|
+
export type InferSelection<T> = T extends SelectionBuilder<infer _E, infer S, infer _P> ? S : never
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Type for the fluent definer function used in useEntity/useEntityList
|
|
317
|
+
*/
|
|
318
|
+
export type FluentDefiner<TModel, TResult extends object> = (
|
|
319
|
+
builder: SelectionBuilder<TModel>,
|
|
320
|
+
) => SelectionBuilder<TModel, TResult, object>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { EntitySnapshotStore } from './EntitySnapshotStore.js'
|
|
2
|
+
import type { EntityMetaStore } from './EntityMetaStore.js'
|
|
3
|
+
import type { RelationStore } from './RelationStore.js'
|
|
4
|
+
import { deepEqual } from '../utils/deepEqual.js'
|
|
5
|
+
|
|
6
|
+
interface DirtyEntity {
|
|
7
|
+
entityType: string
|
|
8
|
+
entityId: string
|
|
9
|
+
changeType: 'create' | 'update' | 'delete'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tracks dirty state across entities by comparing current data to server data.
|
|
14
|
+
*
|
|
15
|
+
* Receives shared references to SnapshotStore internals so it can read
|
|
16
|
+
* entity snapshots, metadata, and relation state without duplicating storage.
|
|
17
|
+
*/
|
|
18
|
+
export class DirtyTracker {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly entitySnapshots: EntitySnapshotStore,
|
|
21
|
+
private readonly meta: EntityMetaStore,
|
|
22
|
+
private readonly relations: RelationStore,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
getAllDirtyEntities(): DirtyEntity[] {
|
|
26
|
+
const dirtyEntities: DirtyEntity[] = []
|
|
27
|
+
|
|
28
|
+
for (const key of this.entitySnapshots.keys()) {
|
|
29
|
+
const [entityType, ...idParts] = key.split(':')
|
|
30
|
+
const entityId = idParts.join(':')
|
|
31
|
+
|
|
32
|
+
if (!entityType || !entityId) continue
|
|
33
|
+
|
|
34
|
+
const entityKey = `${entityType}:${entityId}`
|
|
35
|
+
|
|
36
|
+
if (this.meta.isScheduledForDeletion(entityKey)) {
|
|
37
|
+
if (this.meta.existsOnServer(entityKey)) {
|
|
38
|
+
dirtyEntities.push({ entityType, entityId, changeType: 'delete' })
|
|
39
|
+
}
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!this.meta.existsOnServer(entityKey)) {
|
|
44
|
+
dirtyEntities.push({ entityType, entityId, changeType: 'create' })
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.isEntityDirty(entityType, entityId)) {
|
|
49
|
+
dirtyEntities.push({ entityType, entityId, changeType: 'update' })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return dirtyEntities
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getDirtyFields(entityType: string, entityId: string): string[] {
|
|
57
|
+
const key = `${entityType}:${entityId}`
|
|
58
|
+
const snapshot = this.entitySnapshots.get(key)
|
|
59
|
+
|
|
60
|
+
if (!snapshot) return []
|
|
61
|
+
|
|
62
|
+
const data = snapshot.data as Record<string, unknown>
|
|
63
|
+
const serverData = snapshot.serverData as Record<string, unknown>
|
|
64
|
+
|
|
65
|
+
const dirtyFields: string[] = []
|
|
66
|
+
|
|
67
|
+
for (const fieldName of Object.keys(data)) {
|
|
68
|
+
if (fieldName === 'id') continue
|
|
69
|
+
|
|
70
|
+
const currentValue = data[fieldName]
|
|
71
|
+
const serverValue = serverData[fieldName]
|
|
72
|
+
|
|
73
|
+
if (isRelationValue(currentValue) || isRelationValue(serverValue)) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!deepEqual(currentValue, serverValue)) {
|
|
78
|
+
dirtyFields.push(fieldName)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return dirtyFields
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getDirtyRelations(entityType: string, entityId: string): string[] {
|
|
86
|
+
const keyPrefix = `${entityType}:${entityId}:`
|
|
87
|
+
return this.relations.getDirtyRelations(keyPrefix)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private isEntityDirty(entityType: string, entityId: string): boolean {
|
|
91
|
+
const dirtyFields = this.getDirtyFields(entityType, entityId)
|
|
92
|
+
if (dirtyFields.length > 0) return true
|
|
93
|
+
|
|
94
|
+
const keyPrefix = `${entityType}:${entityId}:`
|
|
95
|
+
const dirtyRelations = this.relations.getDirtyRelations(keyPrefix)
|
|
96
|
+
if (dirtyRelations.length > 0) return true
|
|
97
|
+
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks if a value represents a relation (object with id or array of objects).
|
|
104
|
+
*/
|
|
105
|
+
function isRelationValue(value: unknown): boolean {
|
|
106
|
+
if (value === null || value === undefined) return false
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
return value.length > 0 && typeof value[0] === 'object' && value[0] !== null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof value === 'object') {
|
|
113
|
+
return 'id' in (value as object)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { LoadStatus } from './snapshots.js'
|
|
2
|
+
import { isPersistedId, isPlaceholderId } from './entityId.js'
|
|
3
|
+
import type { FieldError } from '../errors/types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Entity load state tracking.
|
|
7
|
+
*/
|
|
8
|
+
export interface EntityLoadState {
|
|
9
|
+
status: LoadStatus
|
|
10
|
+
error?: FieldError
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Entity metadata for mutation generation.
|
|
15
|
+
*/
|
|
16
|
+
export interface EntityMeta {
|
|
17
|
+
/** Whether the entity exists on the server */
|
|
18
|
+
existsOnServer: boolean
|
|
19
|
+
/** Whether the entity is scheduled for deletion */
|
|
20
|
+
isScheduledForDeletion: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages entity metadata, load states, persisting status, and temp ID mapping.
|
|
25
|
+
*
|
|
26
|
+
* Keys are pre-computed composite strings (e.g., "entityType:id").
|
|
27
|
+
* Follows the same pattern as ErrorStore and RelationStore.
|
|
28
|
+
*/
|
|
29
|
+
export class EntityMetaStore {
|
|
30
|
+
/** Load states keyed by "entityType:id" */
|
|
31
|
+
private readonly loadStates = new Map<string, EntityLoadState>()
|
|
32
|
+
|
|
33
|
+
/** Entity metadata keyed by "entityType:id" */
|
|
34
|
+
private readonly entityMetas = new Map<string, EntityMeta>()
|
|
35
|
+
|
|
36
|
+
/** Persisting status keyed by "entityType:id" */
|
|
37
|
+
private readonly persistingEntities = new Set<string>()
|
|
38
|
+
|
|
39
|
+
/** Mapping from temp ID to persisted ID (keyed by "entityType:tempId") */
|
|
40
|
+
private readonly tempToPersistedId = new Map<string, string>()
|
|
41
|
+
|
|
42
|
+
// ==================== Load State ====================
|
|
43
|
+
|
|
44
|
+
getLoadState(key: string): EntityLoadState | undefined {
|
|
45
|
+
return this.loadStates.get(key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setLoadState(key: string, status: LoadStatus, error?: FieldError): void {
|
|
49
|
+
this.loadStates.set(key, { status, error })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clearLoadState(key: string): void {
|
|
53
|
+
this.loadStates.delete(key)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ==================== Entity Meta ====================
|
|
57
|
+
|
|
58
|
+
getEntityMeta(key: string): EntityMeta | undefined {
|
|
59
|
+
return this.entityMetas.get(key)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setExistsOnServer(key: string, existsOnServer: boolean): void {
|
|
63
|
+
const existing = this.entityMetas.get(key) ?? { existsOnServer: false, isScheduledForDeletion: false }
|
|
64
|
+
this.entityMetas.set(key, { ...existing, existsOnServer })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
existsOnServer(key: string): boolean {
|
|
68
|
+
return this.entityMetas.get(key)?.existsOnServer ?? false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
scheduleForDeletion(key: string): void {
|
|
72
|
+
const existing = this.entityMetas.get(key) ?? { existsOnServer: false, isScheduledForDeletion: false }
|
|
73
|
+
this.entityMetas.set(key, { ...existing, isScheduledForDeletion: true })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
unscheduleForDeletion(key: string): void {
|
|
77
|
+
const existing = this.entityMetas.get(key) ?? { existsOnServer: false, isScheduledForDeletion: false }
|
|
78
|
+
this.entityMetas.set(key, { ...existing, isScheduledForDeletion: false })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isScheduledForDeletion(key: string): boolean {
|
|
82
|
+
return this.entityMetas.get(key)?.isScheduledForDeletion ?? false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ==================== Persisting State ====================
|
|
86
|
+
|
|
87
|
+
isPersisting(key: string): boolean {
|
|
88
|
+
return this.persistingEntities.has(key)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setPersisting(key: string, isPersisting: boolean): void {
|
|
92
|
+
if (isPersisting) {
|
|
93
|
+
this.persistingEntities.add(key)
|
|
94
|
+
} else {
|
|
95
|
+
this.persistingEntities.delete(key)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ==================== Temp ID Mapping ====================
|
|
100
|
+
|
|
101
|
+
mapTempIdToPersistedId(key: string, persistedId: string): void {
|
|
102
|
+
this.tempToPersistedId.set(key, persistedId)
|
|
103
|
+
this.setExistsOnServer(key, true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getPersistedId(key: string, id: string): string | null {
|
|
107
|
+
if (isPlaceholderId(id)) return null
|
|
108
|
+
if (isPersistedId(id)) return id
|
|
109
|
+
return this.tempToPersistedId.get(key) ?? null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isNewEntity(key: string, id: string): boolean {
|
|
113
|
+
if (isPlaceholderId(id)) return true
|
|
114
|
+
if (isPersistedId(id)) return false
|
|
115
|
+
return !this.tempToPersistedId.has(key)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ==================== Bulk Operations ====================
|
|
119
|
+
|
|
120
|
+
exportMetas(keys: string[]): Map<string, EntityMeta> {
|
|
121
|
+
const result = new Map<string, EntityMeta>()
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
const meta = this.entityMetas.get(key)
|
|
124
|
+
if (meta) {
|
|
125
|
+
result.set(key, { ...meta })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
importMetas(metas: Map<string, EntityMeta>): void {
|
|
132
|
+
for (const [key, meta] of metas) {
|
|
133
|
+
this.entityMetas.set(key, { ...meta })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
clear(): void {
|
|
138
|
+
this.loadStates.clear()
|
|
139
|
+
this.entityMetas.clear()
|
|
140
|
+
this.persistingEntities.clear()
|
|
141
|
+
this.tempToPersistedId.clear()
|
|
142
|
+
}
|
|
143
|
+
}
|