@contember/bindx 0.1.4 → 0.1.8
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 +11 -14
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -1
- package/dist/brand/ComponentBrand.d.ts +1 -24
- package/dist/brand/ComponentBrand.d.ts.map +1 -1
- package/dist/contember/index.d.ts +1 -2
- package/dist/contember/index.d.ts.map +1 -1
- package/dist/handles/EntityHandle.d.ts +18 -29
- package/dist/handles/EntityHandle.d.ts.map +1 -1
- package/dist/handles/FieldHandle.d.ts +5 -3
- package/dist/handles/FieldHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +4 -3
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/HasOneHandle.d.ts +33 -36
- package/dist/handles/HasOneHandle.d.ts.map +1 -1
- package/dist/handles/PlaceholderHandle.d.ts +9 -19
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
- package/dist/handles/proxyFactory.d.ts +9 -37
- package/dist/handles/proxyFactory.d.ts.map +1 -1
- package/dist/handles/types.d.ts +4 -0
- package/dist/handles/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +24 -1
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -1
- package/dist/schema/ContemberSchema.d.ts +1 -97
- package/dist/schema/ContemberSchema.d.ts.map +1 -1
- package/dist/schema/SchemaLoader.d.ts +1 -27
- package/dist/schema/SchemaLoader.d.ts.map +1 -1
- package/dist/schema/SchemaRegistry.d.ts +1 -94
- package/dist/schema/SchemaRegistry.d.ts.map +1 -1
- package/dist/schema/roles.d.ts +1 -61
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/types.d.ts +1 -178
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/selection/SelectionMetaCollector.d.ts +1 -31
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -1
- package/dist/selection/SelectionScope.d.ts +1 -76
- package/dist/selection/SelectionScope.d.ts.map +1 -1
- package/dist/selection/buildQuery.d.ts +1 -44
- package/dist/selection/buildQuery.d.ts.map +1 -1
- package/dist/selection/createFragment.d.ts +1 -28
- package/dist/selection/createFragment.d.ts.map +1 -1
- package/dist/selection/createSelectionBuilder.d.ts +1 -17
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -1
- package/dist/selection/queryTypes.d.ts +1 -83
- package/dist/selection/queryTypes.d.ts.map +1 -1
- package/dist/selection/types.d.ts +1 -237
- package/dist/selection/types.d.ts.map +1 -1
- package/dist/utils/aliasGenerator.d.ts +1 -24
- package/dist/utils/aliasGenerator.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/adapter/ContemberAdapter.ts +193 -123
- package/src/brand/ComponentBrand.ts +1 -28
- package/src/contember/index.ts +1 -8
- package/src/handles/EntityHandle.ts +131 -61
- package/src/handles/FieldHandle.ts +19 -3
- package/src/handles/HasManyListHandle.ts +15 -12
- package/src/handles/HasOneHandle.ts +98 -66
- package/src/handles/PlaceholderHandle.ts +26 -24
- package/src/handles/proxyFactory.ts +29 -128
- package/src/handles/types.ts +6 -0
- package/src/index.ts +22 -3
- package/src/persistence/ContemberSchemaMutationAdapter.ts +19 -1
- package/src/schema/ContemberSchema.ts +14 -132
- package/src/schema/SchemaLoader.ts +1 -93
- package/src/schema/SchemaRegistry.ts +1 -286
- package/src/schema/roles.ts +10 -83
- package/src/schema/types.ts +21 -238
- package/src/selection/SelectionMetaCollector.ts +1 -122
- package/src/selection/SelectionScope.ts +1 -208
- package/src/selection/buildQuery.ts +1 -134
- package/src/selection/createFragment.ts +1 -50
- package/src/selection/createSelectionBuilder.ts +1 -283
- package/src/selection/queryTypes.ts +8 -135
- package/src/selection/types.ts +13 -320
- package/src/utils/aliasGenerator.ts +1 -56
package/src/schema/types.ts
CHANGED
|
@@ -1,238 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* Enum scalar field definition — carries the allowed values at runtime
|
|
23
|
-
*/
|
|
24
|
-
export interface EnumFieldDef {
|
|
25
|
-
readonly type: 'enum'
|
|
26
|
-
readonly enumName: string
|
|
27
|
-
readonly values: readonly string[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Has-one relation definition
|
|
32
|
-
*/
|
|
33
|
-
export interface HasOneRelationDef<TTarget extends string = string> {
|
|
34
|
-
readonly type: 'hasOne'
|
|
35
|
-
readonly target: TTarget
|
|
36
|
-
/** Inverse field name on the target entity (for bidirectional relations) */
|
|
37
|
-
readonly inverse?: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Has-many relation definition
|
|
42
|
-
*/
|
|
43
|
-
export interface HasManyRelationDef<TTarget extends string = string> {
|
|
44
|
-
readonly type: 'hasMany'
|
|
45
|
-
readonly target: TTarget
|
|
46
|
-
/** Inverse field name on the target entity (for bidirectional relations) */
|
|
47
|
-
readonly inverse?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Union of all field definition types
|
|
52
|
-
*/
|
|
53
|
-
export type FieldDef<TTarget extends string = string> =
|
|
54
|
-
| ScalarFieldDef
|
|
55
|
-
| EnumFieldDef
|
|
56
|
-
| HasOneRelationDef<TTarget>
|
|
57
|
-
| HasManyRelationDef<TTarget>
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Entity schema definition - maps field names to their definitions
|
|
61
|
-
*/
|
|
62
|
-
export interface EntitySchemaDef<TEntityNames extends string = string> {
|
|
63
|
-
readonly fields: Record<string, FieldDef<TEntityNames>>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Complete schema definition mapping entity names to their schemas.
|
|
68
|
-
* TModels provides the TypeScript types for each entity.
|
|
69
|
-
*/
|
|
70
|
-
export interface SchemaDefinition<
|
|
71
|
-
TModels extends { [K in keyof TModels]: object },
|
|
72
|
-
TEntityNames extends string = keyof TModels & string,
|
|
73
|
-
> {
|
|
74
|
-
readonly entities: {
|
|
75
|
-
[K in TEntityNames]: EntitySchemaDef<TEntityNames>
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Helper function to create a scalar field definition
|
|
81
|
-
*/
|
|
82
|
-
export function scalar(): ScalarFieldDef {
|
|
83
|
-
return { type: 'scalar' }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Helper function to create an enum scalar field definition with allowed values.
|
|
88
|
-
*/
|
|
89
|
-
export function enumScalar<T extends string>(enumName: string, values: readonly T[]): EnumFieldDef {
|
|
90
|
-
return { type: 'enum', enumName, values }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Helper function to create a has-one relation definition
|
|
95
|
-
*/
|
|
96
|
-
export function hasOne<TTarget extends string>(
|
|
97
|
-
target: TTarget,
|
|
98
|
-
options?: { inverse?: string },
|
|
99
|
-
): HasOneRelationDef<TTarget> {
|
|
100
|
-
return {
|
|
101
|
-
type: 'hasOne',
|
|
102
|
-
target,
|
|
103
|
-
inverse: options?.inverse,
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Helper function to create a has-many relation definition
|
|
109
|
-
*/
|
|
110
|
-
export function hasMany<TTarget extends string>(
|
|
111
|
-
target: TTarget,
|
|
112
|
-
options?: { inverse?: string },
|
|
113
|
-
): HasManyRelationDef<TTarget> {
|
|
114
|
-
return {
|
|
115
|
-
type: 'hasMany',
|
|
116
|
-
target,
|
|
117
|
-
inverse: options?.inverse,
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Helper function to define a complete schema with full type inference.
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* ```typescript
|
|
126
|
-
* interface Author {
|
|
127
|
-
* id: string
|
|
128
|
-
* name: string
|
|
129
|
-
* articles: Article[]
|
|
130
|
-
* }
|
|
131
|
-
*
|
|
132
|
-
* interface Article {
|
|
133
|
-
* id: string
|
|
134
|
-
* title: string
|
|
135
|
-
* author: Author
|
|
136
|
-
* }
|
|
137
|
-
*
|
|
138
|
-
* const schema = defineSchema<{
|
|
139
|
-
* Author: Author
|
|
140
|
-
* Article: Article
|
|
141
|
-
* }>({
|
|
142
|
-
* entities: {
|
|
143
|
-
* Author: {
|
|
144
|
-
* fields: {
|
|
145
|
-
* id: scalar(),
|
|
146
|
-
* name: scalar(),
|
|
147
|
-
* articles: hasMany('Article', { inverse: 'author' })
|
|
148
|
-
* }
|
|
149
|
-
* },
|
|
150
|
-
* Article: {
|
|
151
|
-
* fields: {
|
|
152
|
-
* id: scalar(),
|
|
153
|
-
* title: scalar(),
|
|
154
|
-
* author: hasOne('Author', { inverse: 'articles' })
|
|
155
|
-
* }
|
|
156
|
-
* }
|
|
157
|
-
* }
|
|
158
|
-
* })
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export function defineSchema<TModels extends { [K in keyof TModels]: object }>(
|
|
162
|
-
definition: SchemaDefinition<TModels>,
|
|
163
|
-
): SchemaDefinition<TModels> {
|
|
164
|
-
return definition
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Infers the model type for a given entity name from a schema
|
|
169
|
-
*/
|
|
170
|
-
export type InferModel<
|
|
171
|
-
TSchema extends SchemaDefinition<any>,
|
|
172
|
-
TEntityName extends keyof TSchema['entities'],
|
|
173
|
-
> = TSchema extends SchemaDefinition<infer TModels>
|
|
174
|
-
? TEntityName extends keyof TModels
|
|
175
|
-
? TModels[TEntityName]
|
|
176
|
-
: never
|
|
177
|
-
: never
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Infers all entity names from a schema
|
|
181
|
-
*/
|
|
182
|
-
export type InferEntityNames<TSchema extends SchemaDefinition<any>> = keyof TSchema['entities'] & string
|
|
183
|
-
|
|
184
|
-
// ============================================================================
|
|
185
|
-
// Entity Definitions (type-safe entity references)
|
|
186
|
-
// ============================================================================
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* A type-safe reference to an entity in the schema.
|
|
190
|
-
* Carries a role map as a phantom type parameter: Record<roleName, entityType>.
|
|
191
|
-
*
|
|
192
|
-
* For schemas without ACL, the role map is `{ _default: TEntity }`.
|
|
193
|
-
* For schemas with ACL, each role maps to a per-role entity type.
|
|
194
|
-
*
|
|
195
|
-
* @example
|
|
196
|
-
* ```ts
|
|
197
|
-
* // Without roles (convenience):
|
|
198
|
-
* const Article = entityDef<ArticleType>('Article') // → EntityDef<{ _default: ArticleType }>
|
|
199
|
-
*
|
|
200
|
-
* // With roles (generator):
|
|
201
|
-
* const Article = roleEntityDef<{ admin: Article$admin; public: Article$public }>('Article')
|
|
202
|
-
* ```
|
|
203
|
-
*/
|
|
204
|
-
export interface EntityDef<TRoleMap extends Record<string, object> = Record<string, object>> {
|
|
205
|
-
readonly $name: string
|
|
206
|
-
/** @internal phantom type for role map — not present at runtime */
|
|
207
|
-
readonly $roleMap?: TRoleMap
|
|
208
|
-
/** @internal reference to the schema definition for collection-time field lookups */
|
|
209
|
-
readonly $schema?: SchemaDefinition<Record<string, object>>
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Infers the common (default) entity type from an EntityDef.
|
|
214
|
-
* Returns the union of all role entity types — only common fields are accessible.
|
|
215
|
-
*/
|
|
216
|
-
export type InferEntityDef<T> = T extends EntityDef<infer TRoleMap> ? CommonEntity<TRoleMap> : never
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Creates a type-safe entity definition reference.
|
|
220
|
-
* Wraps the entity type in a single-role map: `{ _default: TEntity }`.
|
|
221
|
-
*
|
|
222
|
-
* @param name - The entity name as used in the schema
|
|
223
|
-
* @param schema - Optional schema definition for collection-time field lookups
|
|
224
|
-
*/
|
|
225
|
-
export function entityDef<TEntity extends object>(name: string, schema?: SchemaDefinition<Record<string, object>>): EntityDef<SingleRoleMap<TEntity>> {
|
|
226
|
-
return schema ? { $name: name, $schema: schema } : { $name: name }
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Creates a role-aware entity definition reference.
|
|
231
|
-
* Used by the generator when ACL roles are present.
|
|
232
|
-
*
|
|
233
|
-
* @param name - The entity name as used in the schema
|
|
234
|
-
* @param schema - Optional schema definition for collection-time field lookups
|
|
235
|
-
*/
|
|
236
|
-
export function roleEntityDef<TRoleMap extends Record<string, object>>(name: string, schema?: SchemaDefinition<Record<string, object>>): EntityDef<TRoleMap> {
|
|
237
|
-
return schema ? { $name: name, $schema: schema } : { $name: name }
|
|
238
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
type RelationType,
|
|
3
|
+
type ScalarFieldDef,
|
|
4
|
+
type HasOneRelationDef,
|
|
5
|
+
type HasManyRelationDef,
|
|
6
|
+
type FieldDef,
|
|
7
|
+
type EntitySchemaDef,
|
|
8
|
+
type SchemaDefinition,
|
|
9
|
+
type InferModel,
|
|
10
|
+
type InferEntityNames,
|
|
11
|
+
type EntityDef,
|
|
12
|
+
type InferEntityDef,
|
|
13
|
+
type EnumFieldDef,
|
|
14
|
+
scalar,
|
|
15
|
+
enumScalar,
|
|
16
|
+
hasOne,
|
|
17
|
+
hasMany,
|
|
18
|
+
defineSchema,
|
|
19
|
+
entityDef,
|
|
20
|
+
roleEntityDef,
|
|
21
|
+
} from '@contember/bindx-client'
|
|
@@ -1,122 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { SelectionMetaCollector, mergeSelections, createEmptySelection } from '@contember/bindx-client'
|
|
@@ -1,208 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { SelectionScope, type HasManyParams } from '@contember/bindx-client'
|