@contember/bindx 0.1.5 → 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
|
@@ -1,134 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { buildQueryFromSelection, collectPaths, type QuerySpec, type QueryFieldSpec } from '@contember/bindx-client'
|
|
@@ -1,50 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { createFragment } from '@contember/bindx-client'
|
|
@@ -1,283 +1 @@
|
|
|
1
|
-
|
|
2
|
-
SELECTION_META,
|
|
3
|
-
type FluentFragment,
|
|
4
|
-
type SelectionBuilder,
|
|
5
|
-
type SelectionFieldMeta,
|
|
6
|
-
type SelectionMeta,
|
|
7
|
-
type HasManyOptions,
|
|
8
|
-
} from './types.js'
|
|
9
|
-
import { mergeSelections, SelectionMetaCollector } from './SelectionMetaCollector.js'
|
|
10
|
-
import { generateHasManyAlias } from '../utils/aliasGenerator.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Creates a fluent selection builder for an entity type.
|
|
14
|
-
* Uses Proxy to intercept method calls and build selection metadata.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* const builder = createSelectionBuilder<Article>()
|
|
19
|
-
* const result = builder.id().title().author(a => a.name())
|
|
20
|
-
* const meta = result[SELECTION_META]
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function createSelectionBuilder<TEntity, TSelected extends object = object, THasManyParams extends object = object>(
|
|
24
|
-
initialMeta: SelectionMeta = { fields: new Map() },
|
|
25
|
-
): SelectionBuilder<TEntity, TSelected, THasManyParams> {
|
|
26
|
-
const handler: ProxyHandler<object> = {
|
|
27
|
-
get(_target, prop: string | symbol) {
|
|
28
|
-
// Return metadata for introspection
|
|
29
|
-
if (prop === SELECTION_META) {
|
|
30
|
-
return initialMeta
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Type brand accessor
|
|
34
|
-
if (prop === '__selected') {
|
|
35
|
-
return undefined
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Skip internal symbols
|
|
39
|
-
if (typeof prop === 'symbol') {
|
|
40
|
-
return undefined
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Return a function that handles the field selection
|
|
44
|
-
return createFieldMethod<TEntity, TSelected, THasManyParams>(prop, initialMeta)
|
|
45
|
-
},
|
|
46
|
-
has(_target, prop: string | symbol) {
|
|
47
|
-
if (prop === SELECTION_META) {
|
|
48
|
-
return true
|
|
49
|
-
}
|
|
50
|
-
return false
|
|
51
|
-
},
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return new Proxy({}, handler) as SelectionBuilder<TEntity, TSelected, THasManyParams>
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Creates a method for selecting a specific field.
|
|
59
|
-
* The method signature depends on the field type (scalar, has-one, has-many).
|
|
60
|
-
*/
|
|
61
|
-
function createFieldMethod<TEntity, TSelected extends object, THasManyParams extends object>(
|
|
62
|
-
fieldName: string,
|
|
63
|
-
currentMeta: SelectionMeta,
|
|
64
|
-
) {
|
|
65
|
-
return function selectField(...args: unknown[]): SelectionBuilder<TEntity, object, object> {
|
|
66
|
-
// Parse arguments to determine what kind of selection this is
|
|
67
|
-
const { options, selector, fragment, fragments } = parseFieldArgs(args)
|
|
68
|
-
|
|
69
|
-
// Build field metadata
|
|
70
|
-
const fieldMeta: SelectionFieldMeta = {
|
|
71
|
-
fieldName,
|
|
72
|
-
alias: fieldName, // Will be updated below
|
|
73
|
-
path: [fieldName],
|
|
74
|
-
isRelation: false,
|
|
75
|
-
isArray: false,
|
|
76
|
-
nested: undefined,
|
|
77
|
-
hasManyParams: undefined,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Handle relation with callback
|
|
81
|
-
if (typeof selector === 'function') {
|
|
82
|
-
const nestedBuilder = createSelectionBuilder<unknown>()
|
|
83
|
-
const resultBuilder = selector(nestedBuilder) as SelectionBuilder<unknown, object, object>
|
|
84
|
-
fieldMeta.nested = resultBuilder[SELECTION_META]
|
|
85
|
-
fieldMeta.isRelation = true
|
|
86
|
-
|
|
87
|
-
// Check if this looks like a has-many (options has filter/orderBy/limit)
|
|
88
|
-
if (options && isHasManyOptions(options)) {
|
|
89
|
-
fieldMeta.isArray = true
|
|
90
|
-
fieldMeta.hasManyParams = {
|
|
91
|
-
filter: (options as HasManyOptions).filter,
|
|
92
|
-
orderBy: (options as HasManyOptions).orderBy,
|
|
93
|
-
limit: (options as HasManyOptions).limit,
|
|
94
|
-
offset: (options as HasManyOptions).offset,
|
|
95
|
-
totalCount: (options as HasManyOptions).totalCount,
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Handle relation with single fragment
|
|
101
|
-
if (fragment && isFluentFragment(fragment)) {
|
|
102
|
-
fieldMeta.nested = fragment.__meta
|
|
103
|
-
fieldMeta.isRelation = true
|
|
104
|
-
|
|
105
|
-
// Check has-many params
|
|
106
|
-
if (options && isHasManyOptions(options)) {
|
|
107
|
-
fieldMeta.isArray = true
|
|
108
|
-
fieldMeta.hasManyParams = {
|
|
109
|
-
filter: (options as HasManyOptions).filter,
|
|
110
|
-
orderBy: (options as HasManyOptions).orderBy,
|
|
111
|
-
limit: (options as HasManyOptions).limit,
|
|
112
|
-
offset: (options as HasManyOptions).offset,
|
|
113
|
-
totalCount: (options as HasManyOptions).totalCount,
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Handle relation with multiple fragments (merged)
|
|
119
|
-
if (fragments && fragments.length > 0) {
|
|
120
|
-
// Merge all fragment selections into one
|
|
121
|
-
const mergedSelection = new SelectionMetaCollector()
|
|
122
|
-
for (const frag of fragments) {
|
|
123
|
-
mergeSelections(mergedSelection, frag.__meta)
|
|
124
|
-
}
|
|
125
|
-
fieldMeta.nested = mergedSelection
|
|
126
|
-
fieldMeta.isRelation = true
|
|
127
|
-
|
|
128
|
-
// Check has-many params
|
|
129
|
-
if (options && isHasManyOptions(options)) {
|
|
130
|
-
fieldMeta.isArray = true
|
|
131
|
-
fieldMeta.hasManyParams = {
|
|
132
|
-
filter: (options as HasManyOptions).filter,
|
|
133
|
-
orderBy: (options as HasManyOptions).orderBy,
|
|
134
|
-
limit: (options as HasManyOptions).limit,
|
|
135
|
-
offset: (options as HasManyOptions).offset,
|
|
136
|
-
totalCount: (options as HasManyOptions).totalCount,
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Determine alias:
|
|
142
|
-
// 1. Explicit alias from options takes priority
|
|
143
|
-
// 2. For has-many with params, auto-generate alias
|
|
144
|
-
// 3. Otherwise, use fieldName
|
|
145
|
-
const explicitAlias = (options as { as?: string } | undefined)?.as
|
|
146
|
-
if (explicitAlias) {
|
|
147
|
-
fieldMeta.alias = explicitAlias
|
|
148
|
-
} else if (fieldMeta.isArray && fieldMeta.hasManyParams) {
|
|
149
|
-
// Auto-generate alias for has-many with params
|
|
150
|
-
fieldMeta.alias = generateHasManyAlias(fieldName, fieldMeta.hasManyParams)
|
|
151
|
-
}
|
|
152
|
-
// else alias is already set to fieldName
|
|
153
|
-
|
|
154
|
-
// If it's a relation call without options but first arg is callback/fragment
|
|
155
|
-
// and there's no filter/orderBy, check if it's has-many by detecting array at runtime
|
|
156
|
-
// This is handled by the type system - if the field type is Array<T>, it will use HasManyMethod
|
|
157
|
-
|
|
158
|
-
// Create new metadata with this field added
|
|
159
|
-
const newMeta: SelectionMeta = {
|
|
160
|
-
fields: new Map(currentMeta.fields),
|
|
161
|
-
}
|
|
162
|
-
newMeta.fields.set(fieldMeta.alias, fieldMeta)
|
|
163
|
-
|
|
164
|
-
// Return new builder with accumulated selection
|
|
165
|
-
return createSelectionBuilder<TEntity, TSelected, THasManyParams>(newMeta)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Type guard for HasManyOptions
|
|
171
|
-
*/
|
|
172
|
-
function isHasManyOptions(value: unknown): value is HasManyOptions {
|
|
173
|
-
if (typeof value !== 'object' || value === null) {
|
|
174
|
-
return false
|
|
175
|
-
}
|
|
176
|
-
return 'filter' in value || 'orderBy' in value || 'limit' in value || 'offset' in value
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Type guard for FluentFragment
|
|
181
|
-
*/
|
|
182
|
-
function isFluentFragment(value: unknown): value is FluentFragment<unknown, unknown> {
|
|
183
|
-
return (
|
|
184
|
-
value !== null &&
|
|
185
|
-
typeof value === 'object' &&
|
|
186
|
-
'__isFragment' in value &&
|
|
187
|
-
(value as FluentFragment<unknown, unknown>).__isFragment === true
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Parses the variable arguments to a field method.
|
|
193
|
-
*
|
|
194
|
-
* Possible call patterns:
|
|
195
|
-
* - e.title() -> scalar, no args
|
|
196
|
-
* - e.title({ as: 'x' }) -> scalar with alias
|
|
197
|
-
* - e.author(a => a.name()) -> has-one with callback
|
|
198
|
-
* - e.author(AuthorFragment) -> has-one with fragment
|
|
199
|
-
* - e.author(Frag1, Frag2, ...) -> has-one with multiple fragments (merged)
|
|
200
|
-
* - e.author({ as: 'x' }, a => a.name())-> has-one with alias + callback
|
|
201
|
-
* - e.author({ as: 'x' }, AuthorFrag) -> has-one with alias + fragment
|
|
202
|
-
* - e.author({ as: 'x' }, Frag1, Frag2) -> has-one with alias + multiple fragments
|
|
203
|
-
* - e.tags(t => t.name()) -> has-many with callback
|
|
204
|
-
* - e.tags(TagFragment) -> has-many with fragment
|
|
205
|
-
* - e.tags(Frag1, Frag2, ...) -> has-many with multiple fragments (merged)
|
|
206
|
-
* - e.tags({ filter: {} }, t => t.name()) -> has-many with options + callback
|
|
207
|
-
* - e.tags({ filter: {} }, TagFragment) -> has-many with options + fragment
|
|
208
|
-
* - e.tags({ filter: {} }, Frag1, Frag2) -> has-many with options + multiple fragments
|
|
209
|
-
*/
|
|
210
|
-
function parseFieldArgs(args: unknown[]): {
|
|
211
|
-
options?: Record<string, unknown>
|
|
212
|
-
selector?: (builder: SelectionBuilder<unknown>) => SelectionBuilder<unknown, object, object>
|
|
213
|
-
fragment?: FluentFragment<unknown, unknown>
|
|
214
|
-
fragments?: FluentFragment<unknown, unknown>[]
|
|
215
|
-
} {
|
|
216
|
-
if (args.length === 0) {
|
|
217
|
-
// Just field name, no args: e.title()
|
|
218
|
-
return {}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (args.length === 1) {
|
|
222
|
-
const arg = args[0]
|
|
223
|
-
if (typeof arg === 'function') {
|
|
224
|
-
// Selector callback: e.author(a => a.name())
|
|
225
|
-
return { selector: arg as (builder: SelectionBuilder<unknown>) => SelectionBuilder<unknown, object, object> }
|
|
226
|
-
}
|
|
227
|
-
if (typeof arg === 'object' && arg !== null) {
|
|
228
|
-
if (isFluentFragment(arg)) {
|
|
229
|
-
// Fragment: e.author(AuthorFragment)
|
|
230
|
-
return { fragment: arg }
|
|
231
|
-
}
|
|
232
|
-
// Options without selector: e.title({ as: 'caption' })
|
|
233
|
-
return { options: arg as Record<string, unknown> }
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Check for multiple fragments: e.author(Frag1, Frag2, ...)
|
|
238
|
-
if (args.length >= 2 && args.every(isFluentFragment)) {
|
|
239
|
-
return { fragments: args as FluentFragment<unknown, unknown>[] }
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (args.length >= 2) {
|
|
243
|
-
const [first, ...rest] = args
|
|
244
|
-
|
|
245
|
-
// Check if first is options object
|
|
246
|
-
if (typeof first === 'object' && first !== null && !isFluentFragment(first)) {
|
|
247
|
-
const second = rest[0]
|
|
248
|
-
|
|
249
|
-
if (typeof second === 'function') {
|
|
250
|
-
// Options + selector: e.tags({ filter: {...} }, t => t.name())
|
|
251
|
-
return {
|
|
252
|
-
options: first as Record<string, unknown>,
|
|
253
|
-
selector: second as (builder: SelectionBuilder<unknown>) => SelectionBuilder<unknown, object, object>,
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Check for options + multiple fragments
|
|
258
|
-
if (rest.every(isFluentFragment)) {
|
|
259
|
-
if (rest.length === 1) {
|
|
260
|
-
// Options + single fragment: e.tags({ filter: {...} }, TagFragment)
|
|
261
|
-
return {
|
|
262
|
-
options: first as Record<string, unknown>,
|
|
263
|
-
fragment: rest[0] as FluentFragment<unknown, unknown>,
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// Options + multiple fragments: e.tags({ filter: {} }, Frag1, Frag2)
|
|
267
|
-
return {
|
|
268
|
-
options: first as Record<string, unknown>,
|
|
269
|
-
fragments: rest as FluentFragment<unknown, unknown>[],
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return {}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Extracts SelectionMeta from a builder
|
|
280
|
-
*/
|
|
281
|
-
export function getSelectionMeta(builder: SelectionBuilder<unknown, object, object>): SelectionMeta {
|
|
282
|
-
return builder[SELECTION_META]
|
|
283
|
-
}
|
|
1
|
+
export { createSelectionBuilder, getSelectionMeta } from '@contember/bindx-client'
|
|
@@ -1,135 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { Input } from '@contember/schema'
|
|
10
|
-
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Re-export Contember types
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Condition operators for scalar values - re-exported from @contember/schema
|
|
17
|
-
*/
|
|
18
|
-
export type ScalarCondition<T> = Input.Condition<T>
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Order direction enum values - string literal union from Contember enum
|
|
22
|
-
*/
|
|
23
|
-
export type OrderDirection = `${Input.OrderDirection}`
|
|
24
|
-
|
|
25
|
-
// ============================================================================
|
|
26
|
-
// Where Types (for filtering)
|
|
27
|
-
// ============================================================================
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Base composed where with AND/OR/NOT logic
|
|
31
|
-
*/
|
|
32
|
-
export interface ComposedWhere<TEntity> {
|
|
33
|
-
readonly and?: readonly EntityWhere<TEntity>[]
|
|
34
|
-
readonly or?: readonly EntityWhere<TEntity>[]
|
|
35
|
-
readonly not?: EntityWhere<TEntity>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Checks if a type is a "plain object" (entity) vs a scalar like Date
|
|
40
|
-
* Date and other built-in objects are not treated as relations
|
|
41
|
-
*/
|
|
42
|
-
type IsPlainObject<T> =
|
|
43
|
-
T extends Date ? false :
|
|
44
|
-
T extends Array<any> ? false :
|
|
45
|
-
T extends Function ? false :
|
|
46
|
-
T extends object ? true :
|
|
47
|
-
false
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Field-level where clause for an entity
|
|
51
|
-
* Maps each field to its appropriate condition type using Input.Condition
|
|
52
|
-
*/
|
|
53
|
-
export type FieldsWhere<TEntity> = {
|
|
54
|
-
readonly [K in keyof TEntity]?:
|
|
55
|
-
NonNullable<TEntity[K]> extends Array<infer U>
|
|
56
|
-
? IsPlainObject<U> extends true
|
|
57
|
-
? EntityWhere<U> | null // has-many: filter on related items (array of entities)
|
|
58
|
-
: Input.Condition<NonNullable<TEntity[K]>> | null // scalar array: use Input.Condition on the array (supports hasSome, contains, etc.)
|
|
59
|
-
: IsPlainObject<NonNullable<TEntity[K]>> extends true
|
|
60
|
-
? EntityWhere<NonNullable<TEntity[K]>> | null // has-one: filter on related entity
|
|
61
|
-
: Input.Condition<NonNullable<TEntity[K]>> | null // scalar: use Input.Condition
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Full where type for an entity - combines composed and field conditions
|
|
66
|
-
*/
|
|
67
|
-
export type EntityWhere<TEntity> = ComposedWhere<TEntity> & FieldsWhere<TEntity>
|
|
68
|
-
|
|
69
|
-
// ============================================================================
|
|
70
|
-
// OrderBy Types (for sorting)
|
|
71
|
-
// ============================================================================
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Order by clause for an entity
|
|
75
|
-
* Each scalar field can have a direction, relations can have nested ordering
|
|
76
|
-
*/
|
|
77
|
-
export type EntityOrderBy<TEntity> = {
|
|
78
|
-
readonly [K in keyof TEntity]?:
|
|
79
|
-
NonNullable<TEntity[K]> extends Array<any>
|
|
80
|
-
? never // has-many cannot be ordered by directly
|
|
81
|
-
: IsPlainObject<NonNullable<TEntity[K]>> extends true
|
|
82
|
-
? EntityOrderBy<NonNullable<TEntity[K]>> | null // has-one: nested ordering
|
|
83
|
-
: OrderDirection | null // scalar: direction (including Date)
|
|
84
|
-
} & {
|
|
85
|
-
readonly _random?: boolean
|
|
86
|
-
readonly _randomSeeded?: number
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ============================================================================
|
|
90
|
-
// HasMany Options with Typed Parameters
|
|
91
|
-
// ============================================================================
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Options for has-many relation selection with typed filter/orderBy
|
|
95
|
-
*
|
|
96
|
-
* @typeParam TItem - The related entity type (array item type)
|
|
97
|
-
* @typeParam TAlias - Optional alias for the field
|
|
98
|
-
*/
|
|
99
|
-
export interface TypedHasManyOptions<
|
|
100
|
-
TItem,
|
|
101
|
-
TAlias extends string = string,
|
|
102
|
-
> {
|
|
103
|
-
readonly as?: TAlias
|
|
104
|
-
readonly filter?: EntityWhere<TItem>
|
|
105
|
-
readonly orderBy?: readonly EntityOrderBy<TItem>[]
|
|
106
|
-
readonly limit?: number
|
|
107
|
-
readonly offset?: number
|
|
108
|
-
readonly totalCount?: boolean
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Alias-only options (for scalar or has-one with alias)
|
|
113
|
-
*/
|
|
114
|
-
export interface AliasOptions<TAlias extends string = string> {
|
|
115
|
-
readonly as?: TAlias
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ============================================================================
|
|
119
|
-
// Type Utilities
|
|
120
|
-
// ============================================================================
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Extracts the item type from an array type
|
|
124
|
-
*/
|
|
125
|
-
export type ArrayItemType<T> = T extends Array<infer U> ? U : never
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Checks if a type is an array
|
|
129
|
-
*/
|
|
130
|
-
export type IsArray<T> = T extends Array<any> ? true : false
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Extracts non-nullable type
|
|
134
|
-
*/
|
|
135
|
-
export type NonNullableType<T> = Exclude<T, null | undefined>
|
|
1
|
+
export {
|
|
2
|
+
type EntityWhere,
|
|
3
|
+
type EntityOrderBy,
|
|
4
|
+
type ScalarCondition,
|
|
5
|
+
type OrderDirection,
|
|
6
|
+
type TypedHasManyOptions,
|
|
7
|
+
type AliasOptions,
|
|
8
|
+
} from '@contember/bindx-client'
|