@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,282 @@
|
|
|
1
|
+
import type { SchemaRegistry } from '@contember/bindx'
|
|
2
|
+
import { SelectionScope, generatePlaceholderId } from '@contember/bindx'
|
|
3
|
+
import {
|
|
4
|
+
type EntityRef,
|
|
5
|
+
type EntityAccessor,
|
|
6
|
+
type EntityFields,
|
|
7
|
+
type FieldRef,
|
|
8
|
+
type HasManyRef,
|
|
9
|
+
type HasOneRef,
|
|
10
|
+
FIELD_REF_META,
|
|
11
|
+
SCOPE_REF,
|
|
12
|
+
} from './types.js'
|
|
13
|
+
import { wrapEntityRefWithFieldAccessProxy } from './proxyShared.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Combined ref type for collector that satisfies all ref interfaces
|
|
17
|
+
*/
|
|
18
|
+
type CollectorRef = FieldRef<unknown> & HasManyRef<unknown> & HasOneRef<unknown>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a collector proxy for the collection phase.
|
|
22
|
+
* This proxy captures field access and builds selection metadata using SelectionScope.
|
|
23
|
+
* Supports direct field access: `entity.fieldName` is equivalent to `entity.$fields.fieldName`.
|
|
24
|
+
*
|
|
25
|
+
* @param scope - The SelectionScope to collect fields into
|
|
26
|
+
* @param entityName - The entity type name (e.g., 'Task'), used for schema lookups
|
|
27
|
+
* @param schemaRegistry - The schema registry for field type information
|
|
28
|
+
*/
|
|
29
|
+
export function createCollectorProxy<T>(
|
|
30
|
+
scope: SelectionScope,
|
|
31
|
+
entityName: string | null = null,
|
|
32
|
+
schemaRegistry: SchemaRegistry<Record<string, object>> | null = null,
|
|
33
|
+
): EntityAccessor<T> {
|
|
34
|
+
const fieldsProxy = new Proxy({} as EntityFields<T>, {
|
|
35
|
+
get(_, fieldName: string): FieldRef<unknown> | HasManyRef<unknown> | HasOneRef<unknown> {
|
|
36
|
+
// Return a collector ref that works for all field types
|
|
37
|
+
// The actual type (scalar/hasMany/hasOne) will be determined
|
|
38
|
+
// by how it's used in components or by schema lookup
|
|
39
|
+
return createCollectorFieldRef(scope, fieldName, entityName, schemaRegistry)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const noop = () => () => {}
|
|
44
|
+
|
|
45
|
+
const ref: EntityRef<T> = {
|
|
46
|
+
id: '__collector__',
|
|
47
|
+
$fields: fieldsProxy,
|
|
48
|
+
$data: null,
|
|
49
|
+
$isDirty: false,
|
|
50
|
+
$persistedId: null,
|
|
51
|
+
$isNew: true,
|
|
52
|
+
__entityType: undefined as unknown as T,
|
|
53
|
+
__entityName: '__collector__',
|
|
54
|
+
|
|
55
|
+
__schema: {} as Record<string, object>,
|
|
56
|
+
// Error properties (stubs for collection phase)
|
|
57
|
+
$errors: [],
|
|
58
|
+
$hasError: false,
|
|
59
|
+
$addError: () => {},
|
|
60
|
+
$clearErrors: () => {},
|
|
61
|
+
$clearAllErrors: () => {},
|
|
62
|
+
// Event methods (stubs for collection phase)
|
|
63
|
+
$on: noop,
|
|
64
|
+
$intercept: noop,
|
|
65
|
+
$onPersisted: noop,
|
|
66
|
+
$interceptPersisting: noop,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add scope reference for nested component merging
|
|
70
|
+
// This is an internal implementation detail, not part of public EntityRef interface
|
|
71
|
+
;(ref as unknown as Record<symbol, unknown>)[SCOPE_REF] = scope
|
|
72
|
+
|
|
73
|
+
// Wrap in Proxy to support direct field access
|
|
74
|
+
return wrapEntityRefWithFieldAccessProxy(ref)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a field reference for collection phase using SelectionScope.
|
|
79
|
+
*
|
|
80
|
+
* Key design:
|
|
81
|
+
* - Initially marks field as scalar
|
|
82
|
+
* - Lazily creates child scope when relation access happens (.fields, .entity, .map)
|
|
83
|
+
* - Child scope creation automatically upgrades from scalar to relation
|
|
84
|
+
* - Uses schema to determine field type and enable direct field access for hasOne relations
|
|
85
|
+
*
|
|
86
|
+
* @param parentScope - The parent SelectionScope
|
|
87
|
+
* @param fieldName - The field being accessed
|
|
88
|
+
* @param entityName - The parent entity type name (e.g., 'Task')
|
|
89
|
+
* @param schemaRegistry - The schema registry for field type lookups
|
|
90
|
+
*/
|
|
91
|
+
function createCollectorFieldRef(
|
|
92
|
+
parentScope: SelectionScope,
|
|
93
|
+
fieldName: string,
|
|
94
|
+
entityName: string | null,
|
|
95
|
+
schemaRegistry: SchemaRegistry<Record<string, object>> | null,
|
|
96
|
+
): CollectorRef {
|
|
97
|
+
// Look up field type from schema if available
|
|
98
|
+
const fieldDef = entityName && schemaRegistry
|
|
99
|
+
? schemaRegistry.getFieldDef(entityName, fieldName)
|
|
100
|
+
: null
|
|
101
|
+
const isHasOneRelation = fieldDef?.type === 'hasOne'
|
|
102
|
+
const isHasManyRelation = fieldDef?.type === 'hasMany'
|
|
103
|
+
const isEnum = fieldDef?.type === 'enum'
|
|
104
|
+
const targetEntityName = (fieldDef?.type === 'hasOne' || fieldDef?.type === 'hasMany')
|
|
105
|
+
? fieldDef.target
|
|
106
|
+
: null
|
|
107
|
+
const enumName = isEnum ? fieldDef.enumName : undefined
|
|
108
|
+
|
|
109
|
+
// Initially add as scalar (will be upgraded to relation if .fields/.entity/.map is accessed)
|
|
110
|
+
// Or immediately mark as relation if schema tells us it is one
|
|
111
|
+
if (isHasOneRelation || isHasManyRelation) {
|
|
112
|
+
parentScope.child(fieldName) // This upgrades to relation
|
|
113
|
+
if (isHasManyRelation) {
|
|
114
|
+
parentScope.markAsArray(fieldName)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
parentScope.addScalar(fieldName)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Lazy child scope - created only when relation access happens
|
|
121
|
+
let childScope: SelectionScope | null = null
|
|
122
|
+
|
|
123
|
+
const getChildScope = (): SelectionScope => {
|
|
124
|
+
if (!childScope) {
|
|
125
|
+
// This automatically removes from scalars and creates relation
|
|
126
|
+
childScope = parentScope.child(fieldName)
|
|
127
|
+
}
|
|
128
|
+
return childScope
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const meta = {
|
|
132
|
+
entityType: targetEntityName ?? '', // Collection phase - entity type from schema
|
|
133
|
+
entityId: '', // Collection phase - no entity
|
|
134
|
+
path: [fieldName],
|
|
135
|
+
fieldName,
|
|
136
|
+
isArray: isHasManyRelation,
|
|
137
|
+
isRelation: isHasOneRelation || isHasManyRelation,
|
|
138
|
+
enumName,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const noop = () => () => {}
|
|
142
|
+
const placeholderId = generatePlaceholderId()
|
|
143
|
+
|
|
144
|
+
const hasOneFieldsProxy = new Proxy({} as EntityFields<unknown>, {
|
|
145
|
+
get(_, nestedFieldName: string) {
|
|
146
|
+
// Get child scope (upgrades to relation)
|
|
147
|
+
const scope = getChildScope()
|
|
148
|
+
// Create nested field ref in the child scope, passing target entity info
|
|
149
|
+
return createCollectorFieldRef(scope, nestedFieldName, targetEntityName, schemaRegistry)
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const mapFn = <R>(fn: (item: EntityAccessor<unknown>, index: number) => R): R[] => {
|
|
154
|
+
// Get child scope and mark as array relation
|
|
155
|
+
const scope = getChildScope()
|
|
156
|
+
parentScope.markAsArray(fieldName)
|
|
157
|
+
// Call fn once with collector to gather nested selection, passing target entity info
|
|
158
|
+
fn(createCollectorProxy<unknown>(scope, targetEntityName, schemaRegistry), 0)
|
|
159
|
+
return []
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Base object that satisfies all ref interfaces
|
|
163
|
+
// Components will use only the parts they need
|
|
164
|
+
const refObject = {
|
|
165
|
+
[FIELD_REF_META]: meta,
|
|
166
|
+
// SCOPE_REF allows nested components to merge their selection into this field's scope
|
|
167
|
+
// This is accessed lazily to create the child scope only when needed
|
|
168
|
+
get [SCOPE_REF](): SelectionScope {
|
|
169
|
+
return getChildScope()
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// FieldRef properties (non-$ versions)
|
|
173
|
+
value: null,
|
|
174
|
+
serverValue: null,
|
|
175
|
+
isDirty: false,
|
|
176
|
+
isTouched: false,
|
|
177
|
+
touch: () => {},
|
|
178
|
+
setValue: () => {},
|
|
179
|
+
inputProps: { value: null, setValue: () => {}, onChange: () => {} },
|
|
180
|
+
errors: [],
|
|
181
|
+
hasError: false,
|
|
182
|
+
addError: () => {},
|
|
183
|
+
clearErrors: () => {},
|
|
184
|
+
onChange: noop,
|
|
185
|
+
onChanging: noop,
|
|
186
|
+
|
|
187
|
+
// HasManyRef properties (non-$ versions)
|
|
188
|
+
length: 0,
|
|
189
|
+
items: [],
|
|
190
|
+
map: mapFn,
|
|
191
|
+
add: () => '',
|
|
192
|
+
getById: () => ({} as any),
|
|
193
|
+
remove: () => {},
|
|
194
|
+
move: () => {},
|
|
195
|
+
connect: () => {},
|
|
196
|
+
disconnect: () => {},
|
|
197
|
+
reset: () => {},
|
|
198
|
+
onItemConnected: noop,
|
|
199
|
+
onItemDisconnected: noop,
|
|
200
|
+
interceptItemConnecting: noop,
|
|
201
|
+
interceptItemDisconnecting: noop,
|
|
202
|
+
|
|
203
|
+
// HasOneRef properties ($ prefix only - for collision safety)
|
|
204
|
+
$state: 'disconnected' as const,
|
|
205
|
+
$id: placeholderId,
|
|
206
|
+
$isDirty: false,
|
|
207
|
+
$fields: hasOneFieldsProxy,
|
|
208
|
+
get $entity(): EntityAccessor<unknown> {
|
|
209
|
+
// Get child scope (upgrades to relation) and return proxy with scope
|
|
210
|
+
const scope = getChildScope()
|
|
211
|
+
return createCollectorProxy<unknown>(scope, targetEntityName, schemaRegistry)
|
|
212
|
+
},
|
|
213
|
+
$delete: () => {},
|
|
214
|
+
$errors: [],
|
|
215
|
+
$hasError: false,
|
|
216
|
+
$addError: () => {},
|
|
217
|
+
$clearErrors: () => {},
|
|
218
|
+
$connect: () => {},
|
|
219
|
+
$disconnect: () => {},
|
|
220
|
+
$reset: () => {},
|
|
221
|
+
$onConnect: noop,
|
|
222
|
+
$onDisconnect: noop,
|
|
223
|
+
$interceptConnect: noop,
|
|
224
|
+
$interceptDisconnect: noop,
|
|
225
|
+
|
|
226
|
+
// EntityRef-compatible properties (for HasOneAccessor = EntityAccessor compatibility)
|
|
227
|
+
id: placeholderId,
|
|
228
|
+
$data: null,
|
|
229
|
+
$isNew: false,
|
|
230
|
+
$persistedId: null,
|
|
231
|
+
__entityName: targetEntityName ?? '',
|
|
232
|
+
|
|
233
|
+
__schema: {} as Record<string, object>,
|
|
234
|
+
$clearAllErrors: () => {},
|
|
235
|
+
$on: noop,
|
|
236
|
+
$intercept: noop,
|
|
237
|
+
$onPersisted: noop,
|
|
238
|
+
$interceptPersisting: noop,
|
|
239
|
+
|
|
240
|
+
// paginateRelation total count
|
|
241
|
+
totalCount: undefined,
|
|
242
|
+
|
|
243
|
+
// Type brand (phantom property - only exists in type system)
|
|
244
|
+
__entityType: undefined as unknown,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Always wrap in proxy to support direct field access (e.g., task.project.name).
|
|
248
|
+
// This works for all field types:
|
|
249
|
+
// - Scalar fields: Accessing ref.value works (exists on target, passes through)
|
|
250
|
+
// - Has-many fields: Accessing ref.items/length works (exists on target, passes through)
|
|
251
|
+
// - Has-one with schema: Direct field access proxies to $fields
|
|
252
|
+
// - Has-one without schema: Same - direct field access proxies to $fields
|
|
253
|
+
// The hasOneFieldsProxy creates nested collector refs which properly track selection.
|
|
254
|
+
return wrapCollectorRefWithFieldAccessProxy(refObject, hasOneFieldsProxy)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Wraps a collector ref in a Proxy that supports direct field access for hasOne relations.
|
|
259
|
+
* - `ref.fieldName` is equivalent to `ref.$fields.fieldName`
|
|
260
|
+
* - Known ref properties pass through to the target
|
|
261
|
+
*/
|
|
262
|
+
function wrapCollectorRefWithFieldAccessProxy(
|
|
263
|
+
ref: CollectorRef,
|
|
264
|
+
fieldsProxy: EntityFields<unknown>,
|
|
265
|
+
): CollectorRef {
|
|
266
|
+
return new Proxy(ref, {
|
|
267
|
+
get(target, prop) {
|
|
268
|
+
// Symbols - pass through
|
|
269
|
+
if (typeof prop !== 'string') {
|
|
270
|
+
return Reflect.get(target, prop)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if property exists on target - if so, pass through
|
|
274
|
+
if (prop in target) {
|
|
275
|
+
return Reflect.get(target, prop)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Otherwise, treat as field access
|
|
279
|
+
return fieldsProxy[prop as keyof EntityFields<unknown>]
|
|
280
|
+
},
|
|
281
|
+
}) as CollectorRef
|
|
282
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified createComponent builder implementation.
|
|
3
|
+
*
|
|
4
|
+
* Provides a fluent builder for creating bindx components with support for
|
|
5
|
+
* both implicit (JSX-analyzed) and explicit (selector-based) entity props.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ReactNode } from 'react'
|
|
9
|
+
import type {
|
|
10
|
+
SchemaRegistry,
|
|
11
|
+
SelectionBuilder,
|
|
12
|
+
EntityDef,
|
|
13
|
+
} from '@contember/bindx'
|
|
14
|
+
import {
|
|
15
|
+
ComponentBrand,
|
|
16
|
+
} from '@contember/bindx'
|
|
17
|
+
import type {
|
|
18
|
+
ComponentBuilder,
|
|
19
|
+
ComponentBuilderState,
|
|
20
|
+
CreateComponentOptions,
|
|
21
|
+
} from './componentBuilder.types.js'
|
|
22
|
+
export type { SelectionPropMeta } from './componentBuilder.types.js'
|
|
23
|
+
import type { Condition } from './conditions.js'
|
|
24
|
+
import { buildComponent, type EntityConfig } from './componentFactory.js'
|
|
25
|
+
|
|
26
|
+
// Re-export symbols from componentFactory for backwards compatibility
|
|
27
|
+
export { COMPONENT_MARKER, COMPONENT_BRAND, COMPONENT_SELECTIONS } from './componentFactory.js'
|
|
28
|
+
import { COMPONENT_MARKER, COMPONENT_BRAND } from './componentFactory.js'
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Builder Implementation
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Implementation of the ComponentBuilder interface.
|
|
36
|
+
* Accumulates entity configs and builds the component on .render().
|
|
37
|
+
*
|
|
38
|
+
* Note: The implementation uses broad types (Record<string, unknown>, object)
|
|
39
|
+
* because TypeScript cannot track the accumulating state types through method chaining.
|
|
40
|
+
* Type safety for consumers is provided by the ComponentBuilder interface.
|
|
41
|
+
*/
|
|
42
|
+
export class ComponentBuilderImpl<
|
|
43
|
+
TState extends ComponentBuilderState,
|
|
44
|
+
> {
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly schemaRegistry: SchemaRegistry<Record<string, object>> | null,
|
|
47
|
+
private readonly entityConfigs: Map<string, EntityConfig>,
|
|
48
|
+
private readonly roles: readonly string[],
|
|
49
|
+
private readonly hasInterfacesMode: boolean = false,
|
|
50
|
+
private readonly conditionFn: ((props: Record<string, unknown>) => Condition) | null = null,
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
entity(
|
|
54
|
+
propName: string,
|
|
55
|
+
entity: EntityDef | string,
|
|
56
|
+
selector?: (builder: SelectionBuilder<object>) => SelectionBuilder<object, object, object>,
|
|
57
|
+
): ComponentBuilderImpl<TState> {
|
|
58
|
+
const entityName = typeof entity === 'string' ? entity : entity.$name
|
|
59
|
+
const entitySchema = typeof entity === 'object' ? entity.$schema : undefined
|
|
60
|
+
const newConfigs = new Map(this.entityConfigs)
|
|
61
|
+
newConfigs.set(propName, { entityName, selector, schema: entitySchema })
|
|
62
|
+
return new ComponentBuilderImpl(
|
|
63
|
+
this.schemaRegistry,
|
|
64
|
+
newConfigs,
|
|
65
|
+
this.roles,
|
|
66
|
+
this.hasInterfacesMode,
|
|
67
|
+
this.conditionFn,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interfaces(
|
|
72
|
+
selectors?: Record<string, (builder: SelectionBuilder<object>) => SelectionBuilder<object, object, object>>,
|
|
73
|
+
): ComponentBuilderImpl<TState> {
|
|
74
|
+
// Note: At runtime we don't know the TInterfaces keys, so we rely on
|
|
75
|
+
// the selectors parameter to determine which props have explicit selectors.
|
|
76
|
+
// For props without selectors (including pure implicit mode with no selectors param),
|
|
77
|
+
// entity-like props are discovered at render time via proxy.
|
|
78
|
+
const newConfigs = new Map(this.entityConfigs)
|
|
79
|
+
|
|
80
|
+
if (selectors) {
|
|
81
|
+
for (const [propName, selector] of Object.entries(selectors)) {
|
|
82
|
+
newConfigs.set(propName, {
|
|
83
|
+
entityName: null,
|
|
84
|
+
selector,
|
|
85
|
+
isInterface: true,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new ComponentBuilderImpl(
|
|
91
|
+
this.schemaRegistry,
|
|
92
|
+
newConfigs,
|
|
93
|
+
this.roles,
|
|
94
|
+
true, // Enable interfaces mode for discovery of implicit interface props
|
|
95
|
+
this.conditionFn,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
props<TNewScalarProps extends object>(): ComponentBuilderImpl<TState> {
|
|
100
|
+
// Type-only operation - runtime is a no-op
|
|
101
|
+
return new ComponentBuilderImpl(
|
|
102
|
+
this.schemaRegistry,
|
|
103
|
+
this.entityConfigs,
|
|
104
|
+
this.roles,
|
|
105
|
+
this.hasInterfacesMode,
|
|
106
|
+
this.conditionFn,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if(conditionFn: (props: Record<string, unknown>) => Condition): ComponentBuilderImpl<TState> {
|
|
111
|
+
return new ComponentBuilderImpl(
|
|
112
|
+
this.schemaRegistry,
|
|
113
|
+
this.entityConfigs,
|
|
114
|
+
this.roles,
|
|
115
|
+
this.hasInterfacesMode,
|
|
116
|
+
conditionFn,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render(renderFn: (props: Record<string, unknown>) => ReactNode): unknown {
|
|
121
|
+
return buildComponent(
|
|
122
|
+
this.entityConfigs,
|
|
123
|
+
this.roles,
|
|
124
|
+
renderFn,
|
|
125
|
+
this.hasInterfacesMode,
|
|
126
|
+
this.schemaRegistry,
|
|
127
|
+
this.conditionFn,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Factory Function
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates a new component builder with initial state.
|
|
138
|
+
*
|
|
139
|
+
* @param schemaRegistry - Optional schema registry for validation
|
|
140
|
+
* @param roles - Optional role constraints
|
|
141
|
+
*/
|
|
142
|
+
export function createComponentBuilder(
|
|
143
|
+
schemaRegistry: SchemaRegistry<Record<string, object>> | null,
|
|
144
|
+
roles: readonly string[] = [],
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
146
|
+
): ComponentBuilder<ComponentBuilderState<{}, object, typeof roles>> {
|
|
147
|
+
return new ComponentBuilderImpl(
|
|
148
|
+
schemaRegistry,
|
|
149
|
+
new Map(),
|
|
150
|
+
roles,
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
152
|
+
) as unknown as ComponentBuilder<ComponentBuilderState<{}, object, typeof roles>>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Type Guards & Utilities
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Type guard to check if a value is a component created with createComponent
|
|
161
|
+
*/
|
|
162
|
+
export function isBindxComponent(value: unknown): boolean {
|
|
163
|
+
if (value === null || value === undefined) {
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
if (typeof value !== 'object' && typeof value !== 'function') {
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
return COMPONENT_MARKER in value && (value as Record<symbol, unknown>)[COMPONENT_MARKER] === true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gets the component brand from a bindx component
|
|
174
|
+
*/
|
|
175
|
+
export function getComponentBrand(component: unknown): ComponentBrand | undefined {
|
|
176
|
+
if (!isBindxComponent(component)) {
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
return (component as unknown as Record<symbol, ComponentBrand>)[COMPONENT_BRAND]
|
|
180
|
+
}
|