@contember/bindx-react 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/batching/QueryBatcher.d.ts +31 -0
- package/dist/batching/QueryBatcher.d.ts.map +1 -0
- package/dist/batching/index.d.ts +2 -0
- package/dist/batching/index.d.ts.map +1 -0
- package/dist/components/EntityScope.d.ts +28 -0
- package/dist/components/EntityScope.d.ts.map +1 -0
- package/dist/dataview/DataGrid.d.ts +45 -0
- package/dist/dataview/DataGrid.d.ts.map +1 -0
- package/dist/dataview/DataGridPagination.d.ts +24 -0
- package/dist/dataview/DataGridPagination.d.ts.map +1 -0
- package/dist/dataview/DataGridToolbar.d.ts +16 -0
- package/dist/dataview/DataGridToolbar.d.ts.map +1 -0
- package/dist/dataview/DataViewContext.d.ts +32 -0
- package/dist/dataview/DataViewContext.d.ts.map +1 -0
- package/dist/dataview/columns.d.ts +115 -0
- package/dist/dataview/columns.d.ts.map +1 -0
- package/dist/dataview/contextHooks.d.ts +48 -0
- package/dist/dataview/contextHooks.d.ts.map +1 -0
- package/dist/dataview/dataAttribute.d.ts +6 -0
- package/dist/dataview/dataAttribute.d.ts.map +1 -0
- package/dist/dataview/export.d.ts +46 -0
- package/dist/dataview/export.d.ts.map +1 -0
- package/dist/dataview/filterComponents.d.ts +127 -0
- package/dist/dataview/filterComponents.d.ts.map +1 -0
- package/dist/dataview/filterContext.d.ts +10 -0
- package/dist/dataview/filterContext.d.ts.map +1 -0
- package/dist/dataview/filterHooks.d.ts +74 -0
- package/dist/dataview/filterHooks.d.ts.map +1 -0
- package/dist/dataview/filterScope.d.ts +54 -0
- package/dist/dataview/filterScope.d.ts.map +1 -0
- package/dist/dataview/filterWrappers.d.ts +65 -0
- package/dist/dataview/filterWrappers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +21 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/infiniteLoad.d.ts +32 -0
- package/dist/dataview/infiniteLoad.d.ts.map +1 -0
- package/dist/dataview/loaderComponents.d.ts +80 -0
- package/dist/dataview/loaderComponents.d.ts.map +1 -0
- package/dist/dataview/pagingComponents.d.ts +50 -0
- package/dist/dataview/pagingComponents.d.ts.map +1 -0
- package/dist/dataview/renderCellValue.d.ts +9 -0
- package/dist/dataview/renderCellValue.d.ts.map +1 -0
- package/dist/dataview/selectionComponents.d.ts +62 -0
- package/dist/dataview/selectionComponents.d.ts.map +1 -0
- package/dist/dataview/sortingComponents.d.ts +39 -0
- package/dist/dataview/sortingComponents.d.ts.map +1 -0
- package/dist/dataview/stateStorage.d.ts +19 -0
- package/dist/dataview/stateStorage.d.ts.map +1 -0
- package/dist/dataview/useDataView.d.ts +58 -0
- package/dist/dataview/useDataView.d.ts.map +1 -0
- package/dist/dataview/useDataViewState.d.ts +89 -0
- package/dist/dataview/useDataViewState.d.ts.map +1 -0
- package/dist/hooks/BackendAdapterContext.d.ts +110 -0
- package/dist/hooks/BackendAdapterContext.d.ts.map +1 -0
- package/dist/hooks/ContemberBindxProvider.d.ts +67 -0
- package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -0
- package/dist/hooks/createBindx.d.ts +39 -0
- package/dist/hooks/createBindx.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useBindxEvents.d.ts +83 -0
- package/dist/hooks/useBindxEvents.d.ts.map +1 -0
- package/dist/hooks/useEntity.d.ts +123 -0
- package/dist/hooks/useEntity.d.ts.map +1 -0
- package/dist/hooks/useEntityBeforePersist.d.ts +22 -0
- package/dist/hooks/useEntityBeforePersist.d.ts.map +1 -0
- package/dist/hooks/useEntityCore.d.ts +52 -0
- package/dist/hooks/useEntityCore.d.ts.map +1 -0
- package/dist/hooks/useEntityImpl.d.ts +105 -0
- package/dist/hooks/useEntityImpl.d.ts.map +1 -0
- package/dist/hooks/useEntityList.d.ts +102 -0
- package/dist/hooks/useEntityList.d.ts.map +1 -0
- package/dist/hooks/useEntityListCore.d.ts +49 -0
- package/dist/hooks/useEntityListCore.d.ts.map +1 -0
- package/dist/hooks/useEntityListImpl.d.ts +70 -0
- package/dist/hooks/useEntityListImpl.d.ts.map +1 -0
- package/dist/hooks/useErrors.d.ts +72 -0
- package/dist/hooks/useErrors.d.ts.map +1 -0
- package/dist/hooks/usePersist.d.ts +115 -0
- package/dist/hooks/usePersist.d.ts.map +1 -0
- package/dist/hooks/useSelectionCollection.d.ts +41 -0
- package/dist/hooks/useSelectionCollection.d.ts.map +1 -0
- package/dist/hooks/useSelectionCollectionForList.d.ts +40 -0
- package/dist/hooks/useSelectionCollectionForList.d.ts.map +1 -0
- package/dist/hooks/useStableSelectionMeta.d.ts +11 -0
- package/dist/hooks/useStableSelectionMeta.d.ts.map +1 -0
- package/dist/hooks/useStoreSubscription.d.ts +19 -0
- package/dist/hooks/useStoreSubscription.d.ts.map +1 -0
- package/dist/hooks/useUndo.d.ts +45 -0
- package/dist/hooks/useUndo.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/jsx/SelectionMeta.d.ts +2 -0
- package/dist/jsx/SelectionMeta.d.ts.map +1 -0
- package/dist/jsx/analyzer.d.ts +22 -0
- package/dist/jsx/analyzer.d.ts.map +1 -0
- package/dist/jsx/collectorProxy.d.ts +14 -0
- package/dist/jsx/collectorProxy.d.ts.map +1 -0
- package/dist/jsx/componentBuilder.d.ts +51 -0
- package/dist/jsx/componentBuilder.d.ts.map +1 -0
- package/dist/jsx/componentBuilder.types.d.ts +268 -0
- package/dist/jsx/componentBuilder.types.d.ts.map +1 -0
- package/dist/jsx/componentBuilderCompat.d.ts +17 -0
- package/dist/jsx/componentBuilderCompat.d.ts.map +1 -0
- package/dist/jsx/componentFactory.d.ts +39 -0
- package/dist/jsx/componentFactory.d.ts.map +1 -0
- package/dist/jsx/components/Entity.d.ts +82 -0
- package/dist/jsx/components/Entity.d.ts.map +1 -0
- package/dist/jsx/components/EntityList.d.ts +48 -0
- package/dist/jsx/components/EntityList.d.ts.map +1 -0
- package/dist/jsx/components/Field.d.ts +27 -0
- package/dist/jsx/components/Field.d.ts.map +1 -0
- package/dist/jsx/components/HasMany.d.ts +34 -0
- package/dist/jsx/components/HasMany.d.ts.map +1 -0
- package/dist/jsx/components/HasOne.d.ts +25 -0
- package/dist/jsx/components/HasOne.d.ts.map +1 -0
- package/dist/jsx/components/HasRole.d.ts +51 -0
- package/dist/jsx/components/HasRole.d.ts.map +1 -0
- package/dist/jsx/components/If.d.ts +51 -0
- package/dist/jsx/components/If.d.ts.map +1 -0
- package/dist/jsx/components/Show.d.ts +33 -0
- package/dist/jsx/components/Show.d.ts.map +1 -0
- package/dist/jsx/components/index.d.ts +8 -0
- package/dist/jsx/components/index.d.ts.map +1 -0
- package/dist/jsx/conditions.d.ts +310 -0
- package/dist/jsx/conditions.d.ts.map +1 -0
- package/dist/jsx/createComponent.d.ts +44 -0
- package/dist/jsx/createComponent.d.ts.map +1 -0
- package/dist/jsx/index.d.ts +19 -0
- package/dist/jsx/index.d.ts.map +1 -0
- package/dist/jsx/inlineProxy.d.ts +14 -0
- package/dist/jsx/inlineProxy.d.ts.map +1 -0
- package/dist/jsx/legacyTypes.d.ts +26 -0
- package/dist/jsx/legacyTypes.d.ts.map +1 -0
- package/dist/jsx/proxy.d.ts +2 -0
- package/dist/jsx/proxy.d.ts.map +1 -0
- package/dist/jsx/proxyShared.d.ts +14 -0
- package/dist/jsx/proxyShared.d.ts.map +1 -0
- package/dist/jsx/runtimeProxy.d.ts +11 -0
- package/dist/jsx/runtimeProxy.d.ts.map +1 -0
- package/dist/jsx/standaloneCreateComponent.d.ts +30 -0
- package/dist/jsx/standaloneCreateComponent.d.ts.map +1 -0
- package/dist/jsx/types.d.ts +109 -0
- package/dist/jsx/types.d.ts.map +1 -0
- package/dist/jsx/withCollector.d.ts +39 -0
- package/dist/jsx/withCollector.d.ts.map +1 -0
- package/dist/roles/RoleContext.d.ts +31 -0
- package/dist/roles/RoleContext.d.ts.map +1 -0
- package/dist/roles/index.d.ts +2 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/package.json +34 -0
- package/src/batching/QueryBatcher.ts +114 -0
- package/src/batching/index.ts +1 -0
- package/src/components/EntityScope.tsx +43 -0
- package/src/hooks/BackendAdapterContext.tsx +214 -0
- package/src/hooks/ContemberBindxProvider.tsx +158 -0
- package/src/hooks/index.ts +61 -0
- package/src/hooks/useBindxEvents.ts +229 -0
- package/src/hooks/useEntity.ts +489 -0
- package/src/hooks/useEntityBeforePersist.ts +32 -0
- package/src/hooks/useEntityList.ts +444 -0
- package/src/hooks/useErrors.ts +150 -0
- package/src/hooks/usePersist.ts +377 -0
- package/src/hooks/useSelectionCollection.ts +122 -0
- package/src/hooks/useStoreSubscription.ts +51 -0
- package/src/hooks/useUndo.ts +83 -0
- package/src/index.ts +357 -0
- package/src/jsx/SelectionMeta.ts +2 -0
- package/src/jsx/analyzer.ts +261 -0
- package/src/jsx/collectorProxy.ts +282 -0
- package/src/jsx/componentBuilder.ts +180 -0
- package/src/jsx/componentBuilder.types.ts +463 -0
- package/src/jsx/componentBuilderCompat.ts +39 -0
- package/src/jsx/componentFactory.ts +400 -0
- package/src/jsx/components/Entity.tsx +383 -0
- package/src/jsx/components/EntityList.tsx +147 -0
- package/src/jsx/components/Field.tsx +79 -0
- package/src/jsx/components/HasMany.tsx +109 -0
- package/src/jsx/components/HasOne.tsx +78 -0
- package/src/jsx/components/HasRole.tsx +95 -0
- package/src/jsx/components/If.tsx +127 -0
- package/src/jsx/components/Show.tsx +56 -0
- package/src/jsx/components/index.ts +20 -0
- package/src/jsx/conditions.ts +472 -0
- package/src/jsx/createComponent.ts +175 -0
- package/src/jsx/index.ts +135 -0
- package/src/jsx/legacyTypes.ts +30 -0
- package/src/jsx/proxy.ts +1 -0
- package/src/jsx/proxyShared.ts +42 -0
- package/src/jsx/standaloneCreateComponent.ts +47 -0
- package/src/jsx/types.ts +192 -0
- package/src/jsx/withCollector.ts +45 -0
- package/src/roles/RoleContext.tsx +57 -0
- package/src/roles/index.ts +1 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component factory - builds React components with bindx selection metadata.
|
|
3
|
+
*
|
|
4
|
+
* This module handles the actual component creation, including:
|
|
5
|
+
* - Building React components with selection metadata
|
|
6
|
+
* - Implicit selection collection from JSX
|
|
7
|
+
* - Fragment creation
|
|
8
|
+
* - Selection provider interface
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ReactNode } from 'react'
|
|
12
|
+
import { memo } from 'react'
|
|
13
|
+
import type {
|
|
14
|
+
FluentFragment,
|
|
15
|
+
SelectionMeta,
|
|
16
|
+
SelectionBuilder,
|
|
17
|
+
AnyBrand,
|
|
18
|
+
SchemaDefinition,
|
|
19
|
+
} from '@contember/bindx'
|
|
20
|
+
import {
|
|
21
|
+
SchemaRegistry,
|
|
22
|
+
SELECTION_META,
|
|
23
|
+
ComponentBrand,
|
|
24
|
+
createSelectionBuilder,
|
|
25
|
+
SelectionScope,
|
|
26
|
+
} from '@contember/bindx'
|
|
27
|
+
import type {
|
|
28
|
+
SelectionPropMeta,
|
|
29
|
+
} from './componentBuilder.types.js'
|
|
30
|
+
import type { SelectionProvider, SelectionFieldMeta } from './types.js'
|
|
31
|
+
import { FIELD_REF_META, BINDX_COMPONENT } from './types.js'
|
|
32
|
+
import { createCollectorProxy } from './proxy.js'
|
|
33
|
+
import { collectSelection } from './analyzer.js'
|
|
34
|
+
import { type Condition, evaluateCondition } from './conditions.js'
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Symbols
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Marker symbol for identifying components created with createComponent
|
|
42
|
+
*/
|
|
43
|
+
export const COMPONENT_MARKER = Symbol('BINDX_COMPONENT')
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Symbol for storing component brand on the component
|
|
47
|
+
*/
|
|
48
|
+
export const COMPONENT_BRAND = Symbol('COMPONENT_BRAND')
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Symbol for stored selection metadata
|
|
52
|
+
*/
|
|
53
|
+
export const COMPONENT_SELECTIONS = Symbol('COMPONENT_SELECTIONS')
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Entity Config (Runtime)
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
export interface EntityConfig {
|
|
60
|
+
readonly entityName: string | null // null for interface-based props
|
|
61
|
+
readonly selector?: (builder: SelectionBuilder<object>) => SelectionBuilder<object, object, object>
|
|
62
|
+
readonly isInterface?: boolean
|
|
63
|
+
readonly schema?: SchemaDefinition<Record<string, object>>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Component Building
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds a bindx component from entity configs and render function.
|
|
72
|
+
*
|
|
73
|
+
* Note: Implicit selection collection is deferred (lazy) to avoid TDZ errors
|
|
74
|
+
* when components reference other components defined later in the same file.
|
|
75
|
+
*/
|
|
76
|
+
export function buildComponent<TProps extends object>(
|
|
77
|
+
entityConfigs: Map<string, EntityConfig>,
|
|
78
|
+
roles: readonly string[],
|
|
79
|
+
renderFn: (props: TProps) => ReactNode,
|
|
80
|
+
hasInterfacesMode: boolean,
|
|
81
|
+
schemaRegistry: SchemaRegistry<Record<string, object>> | null,
|
|
82
|
+
conditionFn: ((props: TProps) => Condition) | null,
|
|
83
|
+
): unknown {
|
|
84
|
+
const selectionsMap = new Map<string, SelectionPropMeta>()
|
|
85
|
+
|
|
86
|
+
// Generate unique brand for this component
|
|
87
|
+
const componentBrand = new ComponentBrand(`component_${Math.random().toString(36).slice(2)}`)
|
|
88
|
+
|
|
89
|
+
// 1. Process explicit entities (those with selectors) - these are safe to do eagerly
|
|
90
|
+
for (const [propName, config] of entityConfigs) {
|
|
91
|
+
if (config.selector) {
|
|
92
|
+
const builder = createSelectionBuilder<object>()
|
|
93
|
+
const resultBuilder = config.selector(builder)
|
|
94
|
+
const selection = resultBuilder[SELECTION_META]
|
|
95
|
+
|
|
96
|
+
selectionsMap.set(propName, {
|
|
97
|
+
selection,
|
|
98
|
+
fragment: createFragment(selection, componentBrand, roles),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Implicit entities - collect lazily to avoid TDZ errors
|
|
104
|
+
const implicitConfigs = [...entityConfigs.entries()].filter(([_, c]) => !c.selector)
|
|
105
|
+
let implicitCollected = false
|
|
106
|
+
|
|
107
|
+
function ensureImplicitCollected(): void {
|
|
108
|
+
// In interfaces mode, we also need to collect even if no explicit implicit configs exist
|
|
109
|
+
// (because interface props may be discovered dynamically)
|
|
110
|
+
// Also collect if there's a conditionFn (it may access entity fields)
|
|
111
|
+
if (implicitCollected || (implicitConfigs.length === 0 && !hasInterfacesMode && !conditionFn)) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
implicitCollected = true
|
|
115
|
+
collectImplicitSelections(implicitConfigs, renderFn, selectionsMap, componentBrand, roles, hasInterfacesMode, schemaRegistry, conditionFn)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3. Create React component
|
|
119
|
+
function ComponentImpl(props: TProps): ReactNode {
|
|
120
|
+
ensureImplicitCollected()
|
|
121
|
+
// Evaluate condition at runtime
|
|
122
|
+
if (conditionFn) {
|
|
123
|
+
const condition = conditionFn(props)
|
|
124
|
+
if (!evaluateCondition(condition)) {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return renderFn(props)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const MemoizedComponent = memo(ComponentImpl)
|
|
132
|
+
|
|
133
|
+
// 4. Attach metadata
|
|
134
|
+
const comp = MemoizedComponent as unknown as Record<symbol | string, unknown>
|
|
135
|
+
comp[BINDX_COMPONENT] = true
|
|
136
|
+
comp[COMPONENT_MARKER] = true
|
|
137
|
+
comp[COMPONENT_SELECTIONS] = selectionsMap
|
|
138
|
+
comp[COMPONENT_BRAND] = componentBrand
|
|
139
|
+
|
|
140
|
+
if (roles.length > 0) {
|
|
141
|
+
comp['__componentRoles'] = roles
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 5. Add SelectionProvider interface with lazy collection
|
|
145
|
+
;(MemoizedComponent as unknown as SelectionProvider).getSelection = (
|
|
146
|
+
props: Record<string, unknown>,
|
|
147
|
+
collectNested,
|
|
148
|
+
): SelectionFieldMeta | SelectionFieldMeta[] | null => {
|
|
149
|
+
ensureImplicitCollected()
|
|
150
|
+
return createGetSelection(selectionsMap)(props, collectNested)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 6. Attach fragment properties ($propName) for explicit entities
|
|
154
|
+
for (const [propName, meta] of selectionsMap) {
|
|
155
|
+
comp[`$${propName}`] = meta.fragment
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 7. Define lazy getters for implicit entity props
|
|
159
|
+
// This allows accessing $propName without first rendering the component
|
|
160
|
+
for (const [propName] of implicitConfigs) {
|
|
161
|
+
Object.defineProperty(comp, `$${propName}`, {
|
|
162
|
+
get(): FluentFragment<unknown, object, AnyBrand> | undefined {
|
|
163
|
+
ensureImplicitCollected()
|
|
164
|
+
return selectionsMap.get(propName)?.fragment
|
|
165
|
+
},
|
|
166
|
+
enumerable: true,
|
|
167
|
+
configurable: true,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 8. In interfaces mode, wrap the component in a Proxy to handle dynamic $propName access
|
|
172
|
+
// This is needed because interface prop names are only known at type-level, not runtime
|
|
173
|
+
if (hasInterfacesMode) {
|
|
174
|
+
return new Proxy(MemoizedComponent, {
|
|
175
|
+
get(target, prop, receiver): unknown {
|
|
176
|
+
// Handle $propName access for interface props discovered during collection
|
|
177
|
+
if (typeof prop === 'string' && prop.startsWith('$')) {
|
|
178
|
+
const propName = prop.slice(1)
|
|
179
|
+
// Check if it's already defined on the target
|
|
180
|
+
if (prop in target) {
|
|
181
|
+
return Reflect.get(target, prop, receiver)
|
|
182
|
+
}
|
|
183
|
+
// Trigger collection and return the fragment
|
|
184
|
+
ensureImplicitCollected()
|
|
185
|
+
return selectionsMap.get(propName)?.fragment
|
|
186
|
+
}
|
|
187
|
+
return Reflect.get(target, prop, receiver)
|
|
188
|
+
},
|
|
189
|
+
has(target, prop): boolean {
|
|
190
|
+
if (typeof prop === 'string' && prop.startsWith('$')) {
|
|
191
|
+
ensureImplicitCollected()
|
|
192
|
+
const propName = prop.slice(1)
|
|
193
|
+
return selectionsMap.has(propName)
|
|
194
|
+
}
|
|
195
|
+
return Reflect.has(target, prop)
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return MemoizedComponent
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Implicit Selection Collection
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Creates a mock object for explicit entity props during collection phase.
|
|
209
|
+
* This prevents crashes when accessing .data or .fields on explicit props.
|
|
210
|
+
*/
|
|
211
|
+
function createExplicitPropMock(): unknown {
|
|
212
|
+
const fieldsProxy = new Proxy({}, {
|
|
213
|
+
get(): unknown {
|
|
214
|
+
return { value: null }
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
return new Proxy({}, {
|
|
219
|
+
get(_target, prop): unknown {
|
|
220
|
+
switch (prop) {
|
|
221
|
+
case 'data':
|
|
222
|
+
case 'id':
|
|
223
|
+
return null
|
|
224
|
+
case 'fields':
|
|
225
|
+
return fieldsProxy
|
|
226
|
+
default:
|
|
227
|
+
return undefined
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Collects selections from JSX for implicit entity props.
|
|
235
|
+
*
|
|
236
|
+
* In interfaces mode (hasInterfacesMode=true), any prop that is accessed
|
|
237
|
+
* and used as an entity-like object will be captured as a potential interface prop.
|
|
238
|
+
*/
|
|
239
|
+
function collectImplicitSelections<TProps extends object>(
|
|
240
|
+
implicitConfigs: [string, EntityConfig][],
|
|
241
|
+
renderFn: (props: TProps) => ReactNode,
|
|
242
|
+
selectionsMap: Map<string, SelectionPropMeta>,
|
|
243
|
+
componentBrand: ComponentBrand,
|
|
244
|
+
roles: readonly string[],
|
|
245
|
+
hasInterfacesMode: boolean,
|
|
246
|
+
schemaRegistry: SchemaRegistry<Record<string, object>> | null,
|
|
247
|
+
conditionFn: ((props: TProps) => Condition) | null,
|
|
248
|
+
): void {
|
|
249
|
+
const propScopes = new Map<string, SelectionScope>()
|
|
250
|
+
const implicitConfigsMap = new Map(implicitConfigs)
|
|
251
|
+
const implicitPropNames = new Set(implicitConfigs.map(([name]) => name))
|
|
252
|
+
const explicitPropNames = new Set(
|
|
253
|
+
[...selectionsMap.keys()].filter(name => !implicitPropNames.has(name)),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Create proxy for props that captures field accesses using SelectionScope
|
|
257
|
+
const propsProxy = new Proxy({} as TProps, {
|
|
258
|
+
get(_target, propName: string | symbol): unknown {
|
|
259
|
+
if (typeof propName === 'symbol') {
|
|
260
|
+
return undefined
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// For explicit entity props, return a mock object that won't crash
|
|
264
|
+
// when accessing .data or .fields during the collection phase
|
|
265
|
+
if (explicitPropNames.has(propName)) {
|
|
266
|
+
return createExplicitPropMock()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// For implicit entity props, create or reuse scopes
|
|
270
|
+
if (implicitPropNames.has(propName)) {
|
|
271
|
+
// Reuse existing scope or create new one
|
|
272
|
+
// This ensures both conditionFn and renderFn selections are captured in the same scope
|
|
273
|
+
let scope = propScopes.get(propName)
|
|
274
|
+
if (!scope) {
|
|
275
|
+
scope = new SelectionScope()
|
|
276
|
+
propScopes.set(propName, scope)
|
|
277
|
+
}
|
|
278
|
+
const config = implicitConfigsMap.get(propName)
|
|
279
|
+
const entityName = config?.entityName ?? null
|
|
280
|
+
// Prefer schema from entity def, fall back to provided schemaRegistry
|
|
281
|
+
const resolvedRegistry = config?.schema
|
|
282
|
+
? new SchemaRegistry(config.schema)
|
|
283
|
+
: schemaRegistry
|
|
284
|
+
return createCollectorProxy(scope, entityName, resolvedRegistry)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// In interfaces mode, any unknown prop could be an interface entity prop
|
|
288
|
+
// Create or reuse a scope for it and return a collector proxy
|
|
289
|
+
if (hasInterfacesMode) {
|
|
290
|
+
let scope = propScopes.get(propName)
|
|
291
|
+
if (!scope) {
|
|
292
|
+
scope = new SelectionScope()
|
|
293
|
+
propScopes.set(propName, scope)
|
|
294
|
+
}
|
|
295
|
+
return createCollectorProxy(scope, null, schemaRegistry)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Scalar prop - return undefined
|
|
299
|
+
return undefined
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Execute conditionFn to capture field accesses from the condition
|
|
304
|
+
if (conditionFn) {
|
|
305
|
+
conditionFn(propsProxy)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Execute render to capture field accesses
|
|
309
|
+
const jsx = renderFn(propsProxy)
|
|
310
|
+
|
|
311
|
+
// Analyze JSX tree for component-level selections (handles nested createComponent)
|
|
312
|
+
try {
|
|
313
|
+
collectSelection(jsx)
|
|
314
|
+
} catch {
|
|
315
|
+
// staticRender of nested components may crash during collection phase
|
|
316
|
+
// when collector proxies don't fully implement all runtime interfaces.
|
|
317
|
+
// Field accesses are still captured in the scope tree via proxy.
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Create fragments for captured entities
|
|
321
|
+
for (const [propName, scope] of propScopes) {
|
|
322
|
+
if (scope.hasFields()) {
|
|
323
|
+
const selection = scope.toSelectionMeta()
|
|
324
|
+
|
|
325
|
+
selectionsMap.set(propName, {
|
|
326
|
+
selection,
|
|
327
|
+
fragment: createFragment(selection, componentBrand, roles),
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// Fragment Creation
|
|
335
|
+
// ============================================================================
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Creates a FluentFragment from selection metadata.
|
|
339
|
+
*/
|
|
340
|
+
function createFragment(
|
|
341
|
+
selection: SelectionMeta,
|
|
342
|
+
componentBrand: ComponentBrand,
|
|
343
|
+
roles: readonly string[],
|
|
344
|
+
): FluentFragment<unknown, object, AnyBrand> {
|
|
345
|
+
return {
|
|
346
|
+
__meta: selection,
|
|
347
|
+
__resultType: {} as object,
|
|
348
|
+
__modelType: undefined as unknown,
|
|
349
|
+
__isFragment: true,
|
|
350
|
+
__brand: componentBrand,
|
|
351
|
+
__brands: new Set([componentBrand.brandSymbol]),
|
|
352
|
+
__roles: roles.length > 0 ? roles : undefined,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// Selection Provider
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Creates the getSelection function for SelectionProvider interface.
|
|
362
|
+
*/
|
|
363
|
+
function createGetSelection(
|
|
364
|
+
selectionsMap: Map<string, SelectionPropMeta>,
|
|
365
|
+
): SelectionProvider['getSelection'] {
|
|
366
|
+
return (
|
|
367
|
+
props: Record<string, unknown>,
|
|
368
|
+
_collectNested,
|
|
369
|
+
): SelectionFieldMeta[] | null => {
|
|
370
|
+
const fields: SelectionFieldMeta[] = []
|
|
371
|
+
|
|
372
|
+
for (const [propName, meta] of selectionsMap) {
|
|
373
|
+
const propValue = props[propName]
|
|
374
|
+
|
|
375
|
+
// Case 1: Prop is a field reference (from relation)
|
|
376
|
+
if (propValue && typeof propValue === 'object' && FIELD_REF_META in propValue) {
|
|
377
|
+
const refMeta = (propValue as { [FIELD_REF_META]: { path: string[]; fieldName: string } })[FIELD_REF_META]
|
|
378
|
+
|
|
379
|
+
for (const [_key, field] of meta.selection.fields) {
|
|
380
|
+
if (field.path.length === 1) {
|
|
381
|
+
fields.push({
|
|
382
|
+
...field,
|
|
383
|
+
path: [...refMeta.path, ...field.path],
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Case 2: Prop is an EntityRef from root level
|
|
389
|
+
else if (propValue && typeof propValue === 'object' && 'id' in propValue && 'fields' in propValue) {
|
|
390
|
+
for (const [_key, field] of meta.selection.fields) {
|
|
391
|
+
if (field.path.length === 1) {
|
|
392
|
+
fields.push({ ...field })
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return fields.length > 0 ? fields : null
|
|
399
|
+
}
|
|
400
|
+
}
|