@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,122 @@
1
+ import type { SelectionMeta, SelectionFieldMeta } from './types.js'
2
+ import { generateHasManyAlias } from '../utils/aliasGenerator.js'
3
+
4
+ /**
5
+ * Class for collecting field selection metadata during collection phase
6
+ */
7
+ export class SelectionMetaCollector implements SelectionMeta {
8
+ readonly fields = new Map<string, SelectionFieldMeta>()
9
+
10
+ /**
11
+ * Add a field to the selection.
12
+ *
13
+ * For has-many fields with parameters (filter, orderBy, limit, offset),
14
+ * uses alias as the key to allow multiple instances of the same field
15
+ * with different parameters.
16
+ */
17
+ addField(fieldMeta: SelectionFieldMeta): void {
18
+ let key: string
19
+
20
+ // For has-many fields with parameters, use alias as the key
21
+ // This allows multiple HasMany for the same field with different params
22
+ if (fieldMeta.isArray && fieldMeta.hasManyParams) {
23
+ const hasParams = fieldMeta.hasManyParams.filter !== undefined ||
24
+ fieldMeta.hasManyParams.orderBy !== undefined ||
25
+ fieldMeta.hasManyParams.limit !== undefined ||
26
+ fieldMeta.hasManyParams.offset !== undefined
27
+
28
+ if (hasParams) {
29
+ // Generate auto-alias if alias equals fieldName
30
+ if (fieldMeta.alias === fieldMeta.fieldName) {
31
+ fieldMeta.alias = generateHasManyAlias(fieldMeta.fieldName, fieldMeta.hasManyParams)
32
+ }
33
+ // Use alias-based key for has-many with params
34
+ const pathWithAlias = [...fieldMeta.path.slice(0, -1), fieldMeta.alias]
35
+ key = pathWithAlias.join('.')
36
+ } else {
37
+ key = fieldMeta.path.join('.')
38
+ }
39
+ } else {
40
+ key = fieldMeta.path.join('.')
41
+ }
42
+
43
+ const existing = this.fields.get(key)
44
+
45
+ if (existing) {
46
+ // Update relation/array flags if source has more specific info
47
+ if (fieldMeta.isRelation && !existing.isRelation) {
48
+ existing.isRelation = true
49
+ }
50
+ if (fieldMeta.isArray && !existing.isArray) {
51
+ existing.isArray = true
52
+ }
53
+ // Merge nested selections if both have them
54
+ if (fieldMeta.nested && existing.nested) {
55
+ mergeSelections(existing.nested, fieldMeta.nested)
56
+ } else if (fieldMeta.nested) {
57
+ existing.nested = fieldMeta.nested
58
+ }
59
+ } else {
60
+ this.fields.set(key, { ...fieldMeta })
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get all root-level fields (not nested)
66
+ */
67
+ getRootFields(): SelectionFieldMeta[] {
68
+ const result: SelectionFieldMeta[] = []
69
+ for (const field of this.fields.values()) {
70
+ if (field.path.length === 1) {
71
+ result.push(field)
72
+ }
73
+ }
74
+ return result
75
+ }
76
+
77
+ /**
78
+ * Convert to plain object for serialization
79
+ */
80
+ toJSON(): SelectionMeta {
81
+ return {
82
+ fields: new Map(this.fields),
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Merge two selections together
89
+ */
90
+ export function mergeSelections(target: SelectionMeta, source: SelectionMeta): void {
91
+ for (const [key, field] of source.fields) {
92
+ const existing = target.fields.get(key)
93
+ if (existing) {
94
+ // Update relation/array flags if source has more specific info
95
+ if (field.isRelation && !existing.isRelation) {
96
+ existing.isRelation = true
97
+ }
98
+ if (field.isArray && !existing.isArray) {
99
+ existing.isArray = true
100
+ }
101
+ // Merge hasManyParams if source has them
102
+ if (field.hasManyParams && !existing.hasManyParams) {
103
+ existing.hasManyParams = field.hasManyParams
104
+ }
105
+ // Merge nested selections
106
+ if (field.nested && existing.nested) {
107
+ mergeSelections(existing.nested, field.nested)
108
+ } else if (field.nested) {
109
+ existing.nested = field.nested
110
+ }
111
+ } else {
112
+ target.fields.set(key, { ...field })
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Create empty selection metadata
119
+ */
120
+ export function createEmptySelection(): SelectionMeta {
121
+ return { fields: new Map() }
122
+ }
@@ -0,0 +1,208 @@
1
+ import type { SelectionMeta, SelectionFieldMeta } from './types.js'
2
+ import { generateHasManyAlias } from '../utils/aliasGenerator.js'
3
+
4
+ /**
5
+ * HasMany parameters for filtering, ordering, pagination
6
+ */
7
+ export interface HasManyParams {
8
+ filter?: unknown
9
+ orderBy?: unknown
10
+ limit?: number
11
+ offset?: number
12
+ totalCount?: boolean
13
+ }
14
+
15
+ /**
16
+ * Relation metadata stored at parent level
17
+ */
18
+ interface RelationMeta {
19
+ isArray: boolean
20
+ params?: HasManyParams
21
+ }
22
+
23
+ /**
24
+ * Tree-based selection scope for collecting field selections.
25
+ *
26
+ * Unlike the flat path-based SelectionMetaCollector, SelectionScope
27
+ * maintains a hierarchical tree structure that naturally represents
28
+ * nested entity selections.
29
+ *
30
+ * Key benefits:
31
+ * - Natural tree structure matches entity relationships
32
+ * - No path string manipulation or collision risks
33
+ * - Scope carries its context - no need for symbol passing
34
+ * - Simple merge: just combine tree nodes
35
+ */
36
+ export class SelectionScope {
37
+ // Tree structure
38
+ private parent: SelectionScope | null = null
39
+ private children: Map<string, SelectionScope> = new Map()
40
+
41
+ // Fields at this level
42
+ private scalarFields: Set<string> = new Set()
43
+ private relationMeta: Map<string, RelationMeta> = new Map()
44
+
45
+ /**
46
+ * Create or get a child scope for a relation field.
47
+ * Automatically removes the field from scalars if it was there.
48
+ * New child scopes always include 'id' as a minimum selection.
49
+ */
50
+ child(fieldName: string): SelectionScope {
51
+ // Remove from scalars if it was added as scalar first
52
+ this.scalarFields.delete(fieldName)
53
+
54
+ let childScope = this.children.get(fieldName)
55
+ if (!childScope) {
56
+ childScope = new SelectionScope()
57
+ childScope.parent = this
58
+ childScope.addScalar('id')
59
+ this.children.set(fieldName, childScope)
60
+ this.relationMeta.set(fieldName, { isArray: false })
61
+ }
62
+ return childScope
63
+ }
64
+
65
+ /**
66
+ * Add a scalar field to this scope.
67
+ * If the field is already a relation, this is a no-op.
68
+ */
69
+ addScalar(fieldName: string): void {
70
+ if (!this.children.has(fieldName)) {
71
+ this.scalarFields.add(fieldName)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Mark a relation field as array (hasMany).
77
+ */
78
+ markAsArray(fieldName: string): void {
79
+ const meta = this.relationMeta.get(fieldName)
80
+ if (meta) {
81
+ meta.isArray = true
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Set hasMany parameters for a relation field.
87
+ */
88
+ setHasManyParams(fieldName: string, params: HasManyParams): void {
89
+ const meta = this.relationMeta.get(fieldName)
90
+ if (meta) {
91
+ meta.params = params
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Check if this scope has any fields (scalar or relation).
97
+ */
98
+ hasFields(): boolean {
99
+ return this.scalarFields.size > 0 || this.children.size > 0
100
+ }
101
+
102
+ /**
103
+ * Merge another scope into this one.
104
+ * Scalar fields are added, relation children are recursively merged.
105
+ */
106
+ merge(other: SelectionScope): void {
107
+ // Merge scalar fields
108
+ for (const field of other.scalarFields) {
109
+ this.addScalar(field)
110
+ }
111
+
112
+ // Merge relation children
113
+ for (const [fieldName, otherChild] of other.children) {
114
+ const thisChild = this.child(fieldName)
115
+ thisChild.merge(otherChild)
116
+
117
+ // Merge relation metadata
118
+ const otherMeta = other.relationMeta.get(fieldName)
119
+ if (otherMeta) {
120
+ const thisMeta = this.relationMeta.get(fieldName)!
121
+ if (otherMeta.isArray) {
122
+ thisMeta.isArray = true
123
+ }
124
+ if (otherMeta.params && !thisMeta.params) {
125
+ thisMeta.params = otherMeta.params
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Merge fields from a SelectionMeta into this scope.
133
+ * Useful for integrating with existing fluent builder selections.
134
+ */
135
+ mergeFromSelectionMeta(meta: SelectionMeta): void {
136
+ for (const [_key, field] of meta.fields) {
137
+ // Only process root-level fields
138
+ if (field.path.length !== 1) continue
139
+
140
+ if (field.isRelation && field.nested) {
141
+ const childScope = this.child(field.fieldName)
142
+ if (field.isArray) {
143
+ this.markAsArray(field.fieldName)
144
+ }
145
+ if (field.hasManyParams) {
146
+ this.setHasManyParams(field.fieldName, field.hasManyParams)
147
+ }
148
+ childScope.mergeFromSelectionMeta(field.nested)
149
+ } else {
150
+ this.addScalar(field.fieldName)
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Convert this scope to SelectionMeta format for backwards compatibility.
157
+ * This allows the new SelectionScope to be used with existing query builders.
158
+ *
159
+ * For has-many fields with params, generates auto-alias to allow multiple
160
+ * HasMany components for the same field with different parameters.
161
+ */
162
+ toSelectionMeta(): SelectionMeta {
163
+ const fields = new Map<string, SelectionFieldMeta>()
164
+
165
+ // Add scalar fields
166
+ for (const fieldName of this.scalarFields) {
167
+ fields.set(fieldName, {
168
+ fieldName,
169
+ alias: fieldName,
170
+ path: [fieldName],
171
+ isRelation: false,
172
+ isArray: false,
173
+ })
174
+ }
175
+
176
+ // Add relation fields with nested selections
177
+ for (const [fieldName, childScope] of this.children) {
178
+ const meta = this.relationMeta.get(fieldName)!
179
+
180
+ // Generate alias for has-many fields with params
181
+ const alias = meta.isArray && meta.params
182
+ ? generateHasManyAlias(fieldName, meta.params)
183
+ : fieldName
184
+
185
+ fields.set(alias, {
186
+ fieldName,
187
+ alias,
188
+ path: [fieldName],
189
+ isRelation: true,
190
+ isArray: meta.isArray,
191
+ nested: childScope.toSelectionMeta(),
192
+ hasManyParams: meta.params,
193
+ })
194
+ }
195
+
196
+ return { fields }
197
+ }
198
+
199
+ /**
200
+ * Create a SelectionScope from an existing SelectionMeta.
201
+ * Useful for converting explicit selections to scope format.
202
+ */
203
+ static fromSelectionMeta(meta: SelectionMeta): SelectionScope {
204
+ const scope = new SelectionScope()
205
+ scope.mergeFromSelectionMeta(meta)
206
+ return scope
207
+ }
208
+ }
@@ -0,0 +1,134 @@
1
+ import type { SelectionMeta, SelectionFieldMeta } from './types.js'
2
+
3
+ /**
4
+ * Query specification for a single field
5
+ */
6
+ export interface QueryFieldSpec {
7
+ /** Name of the field as it appears in the output */
8
+ name: string
9
+ /** Path to the field in the source entity */
10
+ sourcePath: string[]
11
+ /** For nested objects/entities */
12
+ nested?: QuerySpec
13
+ /** Whether this is an array/collection */
14
+ isArray?: boolean
15
+ /** For has-many: filter parameters */
16
+ filter?: unknown
17
+ /** For has-many: ordering parameters */
18
+ orderBy?: unknown
19
+ /** For has-many: limit */
20
+ limit?: number
21
+ /** For has-many: offset */
22
+ offset?: number
23
+ /** For has-many: request totalCount via paginateRelation */
24
+ totalCount?: boolean
25
+ }
26
+
27
+ /**
28
+ * Query specification for fetching data.
29
+ * Backend adapters use this to construct their queries.
30
+ */
31
+ export interface QuerySpec {
32
+ /** List of fields to fetch */
33
+ fields: QueryFieldSpec[]
34
+ }
35
+
36
+ /**
37
+ * Builds a QuerySpec from SelectionMeta.
38
+ * This is what gets passed to the backend adapter.
39
+ *
40
+ * @param meta - The selection metadata
41
+ */
42
+ export function buildQueryFromSelection(meta: SelectionMeta): QuerySpec {
43
+ const fields: QueryFieldSpec[] = []
44
+
45
+ // Always include 'id' field if not already selected
46
+ // This is needed for entity identity and HasOneAccessor to detect if relation exists
47
+ if (!meta.fields.has('id')) {
48
+ fields.push({
49
+ name: 'id',
50
+ sourcePath: ['id'],
51
+ })
52
+ }
53
+
54
+ for (const [_alias, fieldMeta] of meta.fields) {
55
+ // Skip relation fields where no nested fields were selected.
56
+ // This occurs when a relation handle is passed as a prop (e.g., HasManyDataGrid's `field` prop)
57
+ // without any of its fields being read during JSX collection. The consuming component
58
+ // is expected to handle its own data fetching for that relation.
59
+ if (fieldMeta.isRelation && fieldMeta.nested && fieldMeta.nested.fields.size === 0) {
60
+ continue
61
+ }
62
+ fields.push(buildFieldSpecFromSelection(fieldMeta))
63
+ }
64
+
65
+ return { fields }
66
+ }
67
+
68
+ /**
69
+ * Builds a QueryFieldSpec from SelectionFieldMeta
70
+ */
71
+ function buildFieldSpecFromSelection(meta: SelectionFieldMeta): QueryFieldSpec {
72
+ const spec: QueryFieldSpec = {
73
+ name: meta.alias,
74
+ sourcePath: [meta.fieldName],
75
+ }
76
+
77
+ if (meta.isArray) {
78
+ spec.isArray = true
79
+
80
+ // Add has-many parameters
81
+ if (meta.hasManyParams) {
82
+ if (meta.hasManyParams.filter !== undefined) {
83
+ spec.filter = meta.hasManyParams.filter
84
+ }
85
+ if (meta.hasManyParams.orderBy !== undefined) {
86
+ spec.orderBy = meta.hasManyParams.orderBy
87
+ }
88
+ if (meta.hasManyParams.limit !== undefined) {
89
+ spec.limit = meta.hasManyParams.limit
90
+ }
91
+ if (meta.hasManyParams.offset !== undefined) {
92
+ spec.offset = meta.hasManyParams.offset
93
+ }
94
+ if (meta.hasManyParams.totalCount) {
95
+ spec.totalCount = true
96
+ }
97
+ }
98
+
99
+ if (meta.nested) {
100
+ spec.nested = buildQueryFromSelection(meta.nested)
101
+ }
102
+ } else if (meta.nested) {
103
+ spec.nested = buildQueryFromSelection(meta.nested)
104
+ }
105
+
106
+ return spec
107
+ }
108
+
109
+ /**
110
+ * Collects all unique paths that need to be fetched.
111
+ * Useful for debugging and optimization.
112
+ */
113
+ export function collectPaths(meta: SelectionMeta, basePath: string[] = []): string[][] {
114
+ const paths: string[][] = []
115
+
116
+ for (const [_alias, fieldMeta] of meta.fields) {
117
+ const fullPath = [...basePath, fieldMeta.fieldName]
118
+
119
+ if (fieldMeta.isArray && fieldMeta.nested) {
120
+ // For arrays, collect paths from nested metadata
121
+ const nestedPaths = collectPaths(fieldMeta.nested, fullPath)
122
+ paths.push(...nestedPaths)
123
+ } else if (fieldMeta.nested) {
124
+ // For nested objects, collect paths recursively
125
+ const nestedPaths = collectPaths(fieldMeta.nested, fullPath)
126
+ paths.push(...nestedPaths)
127
+ } else {
128
+ // Scalar field
129
+ paths.push(fullPath)
130
+ }
131
+ }
132
+
133
+ return paths
134
+ }
@@ -0,0 +1,50 @@
1
+ import { createSelectionBuilder } from './createSelectionBuilder.js'
2
+ import { SELECTION_META, type FluentFragment, type SelectionBuilder } from './types.js'
3
+
4
+ /**
5
+ * Creates a reusable fragment using the fluent builder API.
6
+ *
7
+ * This is a curried function - first call provides the model type,
8
+ * second call provides the definer function.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Define a fragment
13
+ * const AuthorFragment = createFragment<Author>()(e =>
14
+ * e.id().name().email()
15
+ * )
16
+ *
17
+ * // Use in useEntity
18
+ * const article = useEntity('Article', { id }, e =>
19
+ * e.title().author(AuthorFragment)
20
+ * )
21
+ *
22
+ * // Or compose fragments
23
+ * const ArticleFragment = createFragment<Article>()(e =>
24
+ * e.id().title()
25
+ * .author(AuthorFragment)
26
+ * .tags(TagFragment)
27
+ * )
28
+ * ```
29
+ */
30
+ export function createFragment<TModel>() {
31
+ return function <TResult extends object>(
32
+ definer: (builder: SelectionBuilder<TModel>) => SelectionBuilder<TModel, TResult, object>,
33
+ ): FluentFragment<TModel, TResult> {
34
+ // Create initial builder
35
+ const builder = createSelectionBuilder<TModel>()
36
+
37
+ // Execute definer to get final builder with accumulated selections
38
+ const resultBuilder = definer(builder)
39
+
40
+ // Extract metadata
41
+ const meta = resultBuilder[SELECTION_META]
42
+
43
+ return {
44
+ __meta: meta,
45
+ __resultType: undefined as unknown as TResult,
46
+ __modelType: undefined as unknown as TModel,
47
+ __isFragment: true,
48
+ }
49
+ }
50
+ }