@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.
Files changed (194) hide show
  1. package/dist/batching/QueryBatcher.d.ts +31 -0
  2. package/dist/batching/QueryBatcher.d.ts.map +1 -0
  3. package/dist/batching/index.d.ts +2 -0
  4. package/dist/batching/index.d.ts.map +1 -0
  5. package/dist/components/EntityScope.d.ts +28 -0
  6. package/dist/components/EntityScope.d.ts.map +1 -0
  7. package/dist/dataview/DataGrid.d.ts +45 -0
  8. package/dist/dataview/DataGrid.d.ts.map +1 -0
  9. package/dist/dataview/DataGridPagination.d.ts +24 -0
  10. package/dist/dataview/DataGridPagination.d.ts.map +1 -0
  11. package/dist/dataview/DataGridToolbar.d.ts +16 -0
  12. package/dist/dataview/DataGridToolbar.d.ts.map +1 -0
  13. package/dist/dataview/DataViewContext.d.ts +32 -0
  14. package/dist/dataview/DataViewContext.d.ts.map +1 -0
  15. package/dist/dataview/columns.d.ts +115 -0
  16. package/dist/dataview/columns.d.ts.map +1 -0
  17. package/dist/dataview/contextHooks.d.ts +48 -0
  18. package/dist/dataview/contextHooks.d.ts.map +1 -0
  19. package/dist/dataview/dataAttribute.d.ts +6 -0
  20. package/dist/dataview/dataAttribute.d.ts.map +1 -0
  21. package/dist/dataview/export.d.ts +46 -0
  22. package/dist/dataview/export.d.ts.map +1 -0
  23. package/dist/dataview/filterComponents.d.ts +127 -0
  24. package/dist/dataview/filterComponents.d.ts.map +1 -0
  25. package/dist/dataview/filterContext.d.ts +10 -0
  26. package/dist/dataview/filterContext.d.ts.map +1 -0
  27. package/dist/dataview/filterHooks.d.ts +74 -0
  28. package/dist/dataview/filterHooks.d.ts.map +1 -0
  29. package/dist/dataview/filterScope.d.ts +54 -0
  30. package/dist/dataview/filterScope.d.ts.map +1 -0
  31. package/dist/dataview/filterWrappers.d.ts +65 -0
  32. package/dist/dataview/filterWrappers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +21 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/infiniteLoad.d.ts +32 -0
  36. package/dist/dataview/infiniteLoad.d.ts.map +1 -0
  37. package/dist/dataview/loaderComponents.d.ts +80 -0
  38. package/dist/dataview/loaderComponents.d.ts.map +1 -0
  39. package/dist/dataview/pagingComponents.d.ts +50 -0
  40. package/dist/dataview/pagingComponents.d.ts.map +1 -0
  41. package/dist/dataview/renderCellValue.d.ts +9 -0
  42. package/dist/dataview/renderCellValue.d.ts.map +1 -0
  43. package/dist/dataview/selectionComponents.d.ts +62 -0
  44. package/dist/dataview/selectionComponents.d.ts.map +1 -0
  45. package/dist/dataview/sortingComponents.d.ts +39 -0
  46. package/dist/dataview/sortingComponents.d.ts.map +1 -0
  47. package/dist/dataview/stateStorage.d.ts +19 -0
  48. package/dist/dataview/stateStorage.d.ts.map +1 -0
  49. package/dist/dataview/useDataView.d.ts +58 -0
  50. package/dist/dataview/useDataView.d.ts.map +1 -0
  51. package/dist/dataview/useDataViewState.d.ts +89 -0
  52. package/dist/dataview/useDataViewState.d.ts.map +1 -0
  53. package/dist/hooks/BackendAdapterContext.d.ts +110 -0
  54. package/dist/hooks/BackendAdapterContext.d.ts.map +1 -0
  55. package/dist/hooks/ContemberBindxProvider.d.ts +67 -0
  56. package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -0
  57. package/dist/hooks/createBindx.d.ts +39 -0
  58. package/dist/hooks/createBindx.d.ts.map +1 -0
  59. package/dist/hooks/index.d.ts +10 -0
  60. package/dist/hooks/index.d.ts.map +1 -0
  61. package/dist/hooks/useBindxEvents.d.ts +83 -0
  62. package/dist/hooks/useBindxEvents.d.ts.map +1 -0
  63. package/dist/hooks/useEntity.d.ts +123 -0
  64. package/dist/hooks/useEntity.d.ts.map +1 -0
  65. package/dist/hooks/useEntityBeforePersist.d.ts +22 -0
  66. package/dist/hooks/useEntityBeforePersist.d.ts.map +1 -0
  67. package/dist/hooks/useEntityCore.d.ts +52 -0
  68. package/dist/hooks/useEntityCore.d.ts.map +1 -0
  69. package/dist/hooks/useEntityImpl.d.ts +105 -0
  70. package/dist/hooks/useEntityImpl.d.ts.map +1 -0
  71. package/dist/hooks/useEntityList.d.ts +102 -0
  72. package/dist/hooks/useEntityList.d.ts.map +1 -0
  73. package/dist/hooks/useEntityListCore.d.ts +49 -0
  74. package/dist/hooks/useEntityListCore.d.ts.map +1 -0
  75. package/dist/hooks/useEntityListImpl.d.ts +70 -0
  76. package/dist/hooks/useEntityListImpl.d.ts.map +1 -0
  77. package/dist/hooks/useErrors.d.ts +72 -0
  78. package/dist/hooks/useErrors.d.ts.map +1 -0
  79. package/dist/hooks/usePersist.d.ts +115 -0
  80. package/dist/hooks/usePersist.d.ts.map +1 -0
  81. package/dist/hooks/useSelectionCollection.d.ts +41 -0
  82. package/dist/hooks/useSelectionCollection.d.ts.map +1 -0
  83. package/dist/hooks/useSelectionCollectionForList.d.ts +40 -0
  84. package/dist/hooks/useSelectionCollectionForList.d.ts.map +1 -0
  85. package/dist/hooks/useStableSelectionMeta.d.ts +11 -0
  86. package/dist/hooks/useStableSelectionMeta.d.ts.map +1 -0
  87. package/dist/hooks/useStoreSubscription.d.ts +19 -0
  88. package/dist/hooks/useStoreSubscription.d.ts.map +1 -0
  89. package/dist/hooks/useUndo.d.ts +45 -0
  90. package/dist/hooks/useUndo.d.ts.map +1 -0
  91. package/dist/index.d.ts +17 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/jsx/SelectionMeta.d.ts +2 -0
  94. package/dist/jsx/SelectionMeta.d.ts.map +1 -0
  95. package/dist/jsx/analyzer.d.ts +22 -0
  96. package/dist/jsx/analyzer.d.ts.map +1 -0
  97. package/dist/jsx/collectorProxy.d.ts +14 -0
  98. package/dist/jsx/collectorProxy.d.ts.map +1 -0
  99. package/dist/jsx/componentBuilder.d.ts +51 -0
  100. package/dist/jsx/componentBuilder.d.ts.map +1 -0
  101. package/dist/jsx/componentBuilder.types.d.ts +268 -0
  102. package/dist/jsx/componentBuilder.types.d.ts.map +1 -0
  103. package/dist/jsx/componentBuilderCompat.d.ts +17 -0
  104. package/dist/jsx/componentBuilderCompat.d.ts.map +1 -0
  105. package/dist/jsx/componentFactory.d.ts +39 -0
  106. package/dist/jsx/componentFactory.d.ts.map +1 -0
  107. package/dist/jsx/components/Entity.d.ts +82 -0
  108. package/dist/jsx/components/Entity.d.ts.map +1 -0
  109. package/dist/jsx/components/EntityList.d.ts +48 -0
  110. package/dist/jsx/components/EntityList.d.ts.map +1 -0
  111. package/dist/jsx/components/Field.d.ts +27 -0
  112. package/dist/jsx/components/Field.d.ts.map +1 -0
  113. package/dist/jsx/components/HasMany.d.ts +34 -0
  114. package/dist/jsx/components/HasMany.d.ts.map +1 -0
  115. package/dist/jsx/components/HasOne.d.ts +25 -0
  116. package/dist/jsx/components/HasOne.d.ts.map +1 -0
  117. package/dist/jsx/components/HasRole.d.ts +51 -0
  118. package/dist/jsx/components/HasRole.d.ts.map +1 -0
  119. package/dist/jsx/components/If.d.ts +51 -0
  120. package/dist/jsx/components/If.d.ts.map +1 -0
  121. package/dist/jsx/components/Show.d.ts +33 -0
  122. package/dist/jsx/components/Show.d.ts.map +1 -0
  123. package/dist/jsx/components/index.d.ts +8 -0
  124. package/dist/jsx/components/index.d.ts.map +1 -0
  125. package/dist/jsx/conditions.d.ts +310 -0
  126. package/dist/jsx/conditions.d.ts.map +1 -0
  127. package/dist/jsx/createComponent.d.ts +44 -0
  128. package/dist/jsx/createComponent.d.ts.map +1 -0
  129. package/dist/jsx/index.d.ts +19 -0
  130. package/dist/jsx/index.d.ts.map +1 -0
  131. package/dist/jsx/inlineProxy.d.ts +14 -0
  132. package/dist/jsx/inlineProxy.d.ts.map +1 -0
  133. package/dist/jsx/legacyTypes.d.ts +26 -0
  134. package/dist/jsx/legacyTypes.d.ts.map +1 -0
  135. package/dist/jsx/proxy.d.ts +2 -0
  136. package/dist/jsx/proxy.d.ts.map +1 -0
  137. package/dist/jsx/proxyShared.d.ts +14 -0
  138. package/dist/jsx/proxyShared.d.ts.map +1 -0
  139. package/dist/jsx/runtimeProxy.d.ts +11 -0
  140. package/dist/jsx/runtimeProxy.d.ts.map +1 -0
  141. package/dist/jsx/standaloneCreateComponent.d.ts +30 -0
  142. package/dist/jsx/standaloneCreateComponent.d.ts.map +1 -0
  143. package/dist/jsx/types.d.ts +109 -0
  144. package/dist/jsx/types.d.ts.map +1 -0
  145. package/dist/jsx/withCollector.d.ts +39 -0
  146. package/dist/jsx/withCollector.d.ts.map +1 -0
  147. package/dist/roles/RoleContext.d.ts +31 -0
  148. package/dist/roles/RoleContext.d.ts.map +1 -0
  149. package/dist/roles/index.d.ts +2 -0
  150. package/dist/roles/index.d.ts.map +1 -0
  151. package/package.json +34 -0
  152. package/src/batching/QueryBatcher.ts +114 -0
  153. package/src/batching/index.ts +1 -0
  154. package/src/components/EntityScope.tsx +43 -0
  155. package/src/hooks/BackendAdapterContext.tsx +214 -0
  156. package/src/hooks/ContemberBindxProvider.tsx +158 -0
  157. package/src/hooks/index.ts +61 -0
  158. package/src/hooks/useBindxEvents.ts +229 -0
  159. package/src/hooks/useEntity.ts +489 -0
  160. package/src/hooks/useEntityBeforePersist.ts +32 -0
  161. package/src/hooks/useEntityList.ts +444 -0
  162. package/src/hooks/useErrors.ts +150 -0
  163. package/src/hooks/usePersist.ts +377 -0
  164. package/src/hooks/useSelectionCollection.ts +122 -0
  165. package/src/hooks/useStoreSubscription.ts +51 -0
  166. package/src/hooks/useUndo.ts +83 -0
  167. package/src/index.ts +357 -0
  168. package/src/jsx/SelectionMeta.ts +2 -0
  169. package/src/jsx/analyzer.ts +261 -0
  170. package/src/jsx/collectorProxy.ts +282 -0
  171. package/src/jsx/componentBuilder.ts +180 -0
  172. package/src/jsx/componentBuilder.types.ts +463 -0
  173. package/src/jsx/componentBuilderCompat.ts +39 -0
  174. package/src/jsx/componentFactory.ts +400 -0
  175. package/src/jsx/components/Entity.tsx +383 -0
  176. package/src/jsx/components/EntityList.tsx +147 -0
  177. package/src/jsx/components/Field.tsx +79 -0
  178. package/src/jsx/components/HasMany.tsx +109 -0
  179. package/src/jsx/components/HasOne.tsx +78 -0
  180. package/src/jsx/components/HasRole.tsx +95 -0
  181. package/src/jsx/components/If.tsx +127 -0
  182. package/src/jsx/components/Show.tsx +56 -0
  183. package/src/jsx/components/index.ts +20 -0
  184. package/src/jsx/conditions.ts +472 -0
  185. package/src/jsx/createComponent.ts +175 -0
  186. package/src/jsx/index.ts +135 -0
  187. package/src/jsx/legacyTypes.ts +30 -0
  188. package/src/jsx/proxy.ts +1 -0
  189. package/src/jsx/proxyShared.ts +42 -0
  190. package/src/jsx/standaloneCreateComponent.ts +47 -0
  191. package/src/jsx/types.ts +192 -0
  192. package/src/jsx/withCollector.ts +45 -0
  193. package/src/roles/RoleContext.tsx +57 -0
  194. 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
+ }