@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,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
|
+
}
|