@contember/bindx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +40 -0
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
  3. package/dist/adapter/MockAdapter.d.ts +81 -0
  4. package/dist/adapter/MockAdapter.d.ts.map +1 -0
  5. package/dist/adapter/MockQueryEngine.d.ts +43 -0
  6. package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
  7. package/dist/adapter/index.d.ts +5 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/types.d.ts +184 -0
  10. package/dist/adapter/types.d.ts.map +1 -0
  11. package/dist/brand/ComponentBrand.d.ts +25 -0
  12. package/dist/brand/ComponentBrand.d.ts.map +1 -0
  13. package/dist/contember/index.d.ts +10 -0
  14. package/dist/contember/index.d.ts.map +1 -0
  15. package/dist/core/ActionDispatcher.d.ts +64 -0
  16. package/dist/core/ActionDispatcher.d.ts.map +1 -0
  17. package/dist/core/Disposable.d.ts +57 -0
  18. package/dist/core/Disposable.d.ts.map +1 -0
  19. package/dist/core/EntityLoader.d.ts +80 -0
  20. package/dist/core/EntityLoader.d.ts.map +1 -0
  21. package/dist/core/PersistenceManager.d.ts +126 -0
  22. package/dist/core/PersistenceManager.d.ts.map +1 -0
  23. package/dist/core/SelectionResolver.d.ts +21 -0
  24. package/dist/core/SelectionResolver.d.ts.map +1 -0
  25. package/dist/core/actionClassification.d.ts +48 -0
  26. package/dist/core/actionClassification.d.ts.map +1 -0
  27. package/dist/core/actions.d.ts +301 -0
  28. package/dist/core/actions.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/dataview/filterHandlers.d.ts +18 -0
  32. package/dist/dataview/filterHandlers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +3 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/types.d.ts +164 -0
  36. package/dist/dataview/types.d.ts.map +1 -0
  37. package/dist/errors/UnfetchedFieldError.d.ts +11 -0
  38. package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
  39. package/dist/errors/errorPathResolver.d.ts +52 -0
  40. package/dist/errors/errorPathResolver.d.ts.map +1 -0
  41. package/dist/errors/index.d.ts +5 -0
  42. package/dist/errors/index.d.ts.map +1 -0
  43. package/dist/errors/pathMapper.d.ts +42 -0
  44. package/dist/errors/pathMapper.d.ts.map +1 -0
  45. package/dist/errors/types.d.ts +131 -0
  46. package/dist/errors/types.d.ts.map +1 -0
  47. package/dist/events/EventEmitter.d.ts +97 -0
  48. package/dist/events/EventEmitter.d.ts.map +1 -0
  49. package/dist/events/eventFactory.d.ts +40 -0
  50. package/dist/events/eventFactory.d.ts.map +1 -0
  51. package/dist/events/index.d.ts +15 -0
  52. package/dist/events/index.d.ts.map +1 -0
  53. package/dist/events/types.d.ts +264 -0
  54. package/dist/events/types.d.ts.map +1 -0
  55. package/dist/handles/BaseHandle.d.ts +64 -0
  56. package/dist/handles/BaseHandle.d.ts.map +1 -0
  57. package/dist/handles/EntityHandle.d.ts +208 -0
  58. package/dist/handles/EntityHandle.d.ts.map +1 -0
  59. package/dist/handles/FieldHandle.d.ts +111 -0
  60. package/dist/handles/FieldHandle.d.ts.map +1 -0
  61. package/dist/handles/HasManyListHandle.d.ts +166 -0
  62. package/dist/handles/HasManyListHandle.d.ts.map +1 -0
  63. package/dist/handles/HasOneHandle.d.ts +168 -0
  64. package/dist/handles/HasOneHandle.d.ts.map +1 -0
  65. package/dist/handles/PlaceholderHandle.d.ts +117 -0
  66. package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
  67. package/dist/handles/index.d.ts +8 -0
  68. package/dist/handles/index.d.ts.map +1 -0
  69. package/dist/handles/proxyFactory.d.ts +66 -0
  70. package/dist/handles/proxyFactory.d.ts.map +1 -0
  71. package/dist/handles/types.d.ts +430 -0
  72. package/dist/handles/types.d.ts.map +1 -0
  73. package/dist/index.d.ts +53 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/internal.d.ts +11 -0
  76. package/dist/internal.d.ts.map +1 -0
  77. package/dist/persistence/BatchPersister.d.ts +142 -0
  78. package/dist/persistence/BatchPersister.d.ts.map +1 -0
  79. package/dist/persistence/ChangeRegistry.d.ts +87 -0
  80. package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
  81. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
  82. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
  83. package/dist/persistence/MutationCollector.d.ts +96 -0
  84. package/dist/persistence/MutationCollector.d.ts.map +1 -0
  85. package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
  86. package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
  87. package/dist/persistence/index.d.ts +7 -0
  88. package/dist/persistence/index.d.ts.map +1 -0
  89. package/dist/persistence/types.d.ts +189 -0
  90. package/dist/persistence/types.d.ts.map +1 -0
  91. package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
  92. package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
  93. package/dist/roles/index.d.ts +11 -0
  94. package/dist/roles/index.d.ts.map +1 -0
  95. package/dist/roles/types.d.ts +242 -0
  96. package/dist/roles/types.d.ts.map +1 -0
  97. package/dist/schema/ContemberSchema.d.ts +98 -0
  98. package/dist/schema/ContemberSchema.d.ts.map +1 -0
  99. package/dist/schema/SchemaLoader.d.ts +28 -0
  100. package/dist/schema/SchemaLoader.d.ts.map +1 -0
  101. package/dist/schema/SchemaRegistry.d.ts +95 -0
  102. package/dist/schema/SchemaRegistry.d.ts.map +1 -0
  103. package/dist/schema/index.d.ts +6 -0
  104. package/dist/schema/index.d.ts.map +1 -0
  105. package/dist/schema/roles.d.ts +62 -0
  106. package/dist/schema/roles.d.ts.map +1 -0
  107. package/dist/schema/types.d.ts +179 -0
  108. package/dist/schema/types.d.ts.map +1 -0
  109. package/dist/selection/SelectionMetaCollector.d.ts +32 -0
  110. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
  111. package/dist/selection/SelectionScope.d.ts +77 -0
  112. package/dist/selection/SelectionScope.d.ts.map +1 -0
  113. package/dist/selection/buildQuery.d.ts +45 -0
  114. package/dist/selection/buildQuery.d.ts.map +1 -0
  115. package/dist/selection/createFragment.d.ts +29 -0
  116. package/dist/selection/createFragment.d.ts.map +1 -0
  117. package/dist/selection/createSelectionBuilder.d.ts +18 -0
  118. package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
  119. package/dist/selection/index.d.ts +8 -0
  120. package/dist/selection/index.d.ts.map +1 -0
  121. package/dist/selection/queryTypes.d.ts +84 -0
  122. package/dist/selection/queryTypes.d.ts.map +1 -0
  123. package/dist/selection/types.d.ts +238 -0
  124. package/dist/selection/types.d.ts.map +1 -0
  125. package/dist/store/DirtyTracker.d.ts +26 -0
  126. package/dist/store/DirtyTracker.d.ts.map +1 -0
  127. package/dist/store/EntityMetaStore.d.ts +52 -0
  128. package/dist/store/EntityMetaStore.d.ts.map +1 -0
  129. package/dist/store/EntitySnapshotStore.d.ts +63 -0
  130. package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
  131. package/dist/store/ErrorStore.d.ts +82 -0
  132. package/dist/store/ErrorStore.d.ts.map +1 -0
  133. package/dist/store/RelationStore.d.ts +175 -0
  134. package/dist/store/RelationStore.d.ts.map +1 -0
  135. package/dist/store/SnapshotStore.d.ts +143 -0
  136. package/dist/store/SnapshotStore.d.ts.map +1 -0
  137. package/dist/store/SubscriptionManager.d.ts +90 -0
  138. package/dist/store/SubscriptionManager.d.ts.map +1 -0
  139. package/dist/store/TouchedStore.d.ts +24 -0
  140. package/dist/store/TouchedStore.d.ts.map +1 -0
  141. package/dist/store/entityId.d.ts +29 -0
  142. package/dist/store/entityId.d.ts.map +1 -0
  143. package/dist/store/index.d.ts +1 -0
  144. package/dist/store/index.d.ts.map +1 -0
  145. package/dist/store/snapshots.d.ts +144 -0
  146. package/dist/store/snapshots.d.ts.map +1 -0
  147. package/dist/undo/UndoManager.d.ts +120 -0
  148. package/dist/undo/UndoManager.d.ts.map +1 -0
  149. package/dist/undo/index.d.ts +3 -0
  150. package/dist/undo/index.d.ts.map +1 -0
  151. package/dist/undo/types.d.ts +74 -0
  152. package/dist/undo/types.d.ts.map +1 -0
  153. package/dist/utils/aliasGenerator.d.ts +25 -0
  154. package/dist/utils/aliasGenerator.d.ts.map +1 -0
  155. package/dist/utils/deepEqual.d.ts +6 -0
  156. package/dist/utils/deepEqual.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +3 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/package.json +29 -0
  160. package/src/adapter/ContemberAdapter.ts +239 -0
  161. package/src/adapter/MockAdapter.ts +394 -0
  162. package/src/adapter/MockQueryEngine.ts +236 -0
  163. package/src/adapter/index.ts +21 -0
  164. package/src/adapter/types.ts +199 -0
  165. package/src/brand/ComponentBrand.ts +28 -0
  166. package/src/contember/index.ts +17 -0
  167. package/src/core/ActionDispatcher.ts +450 -0
  168. package/src/core/Disposable.ts +126 -0
  169. package/src/core/EntityLoader.ts +169 -0
  170. package/src/core/SelectionResolver.ts +78 -0
  171. package/src/core/actionClassification.ts +196 -0
  172. package/src/core/actions.ts +491 -0
  173. package/src/core/index.ts +20 -0
  174. package/src/dataview/filterHandlers.ts +443 -0
  175. package/src/dataview/index.ts +36 -0
  176. package/src/dataview/types.ts +220 -0
  177. package/src/errors/UnfetchedFieldError.ts +18 -0
  178. package/src/errors/errorPathResolver.ts +239 -0
  179. package/src/errors/index.ts +40 -0
  180. package/src/errors/pathMapper.ts +40 -0
  181. package/src/errors/types.ts +223 -0
  182. package/src/events/EventEmitter.ts +447 -0
  183. package/src/events/eventFactory.ts +430 -0
  184. package/src/events/index.ts +89 -0
  185. package/src/events/types.ts +374 -0
  186. package/src/handles/BaseHandle.ts +100 -0
  187. package/src/handles/EntityHandle.ts +538 -0
  188. package/src/handles/FieldHandle.ts +268 -0
  189. package/src/handles/HasManyListHandle.ts +500 -0
  190. package/src/handles/HasOneHandle.ts +500 -0
  191. package/src/handles/PlaceholderHandle.ts +321 -0
  192. package/src/handles/index.ts +37 -0
  193. package/src/handles/proxyFactory.ts +218 -0
  194. package/src/handles/types.ts +743 -0
  195. package/src/index.ts +384 -0
  196. package/src/internal.ts +19 -0
  197. package/src/persistence/BatchPersister.ts +911 -0
  198. package/src/persistence/ChangeRegistry.ts +150 -0
  199. package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
  200. package/src/persistence/MutationCollector.ts +567 -0
  201. package/src/persistence/MutationSchemaProvider.ts +33 -0
  202. package/src/persistence/index.ts +23 -0
  203. package/src/persistence/types.ts +220 -0
  204. package/src/schema/ContemberSchema.ts +132 -0
  205. package/src/schema/SchemaLoader.ts +93 -0
  206. package/src/schema/SchemaRegistry.ts +286 -0
  207. package/src/schema/index.ts +51 -0
  208. package/src/schema/roles.ts +83 -0
  209. package/src/schema/types.ts +238 -0
  210. package/src/selection/SelectionMetaCollector.ts +122 -0
  211. package/src/selection/SelectionScope.ts +208 -0
  212. package/src/selection/buildQuery.ts +134 -0
  213. package/src/selection/createFragment.ts +50 -0
  214. package/src/selection/createSelectionBuilder.ts +283 -0
  215. package/src/selection/index.ts +31 -0
  216. package/src/selection/queryTypes.ts +135 -0
  217. package/src/selection/types.ts +320 -0
  218. package/src/store/DirtyTracker.ts +117 -0
  219. package/src/store/EntityMetaStore.ts +143 -0
  220. package/src/store/EntitySnapshotStore.ts +260 -0
  221. package/src/store/ErrorStore.ts +251 -0
  222. package/src/store/RelationStore.ts +696 -0
  223. package/src/store/SnapshotStore.ts +765 -0
  224. package/src/store/SubscriptionManager.ts +241 -0
  225. package/src/store/TouchedStore.ts +42 -0
  226. package/src/store/entityId.ts +43 -0
  227. package/src/store/index.ts +2 -0
  228. package/src/store/snapshots.ts +203 -0
  229. package/src/undo/UndoManager.ts +418 -0
  230. package/src/undo/index.ts +9 -0
  231. package/src/undo/types.ts +84 -0
  232. package/src/utils/aliasGenerator.ts +56 -0
  233. package/src/utils/deepEqual.ts +33 -0
  234. package/src/utils/index.ts +2 -0
@@ -0,0 +1,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
+ }