@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,472 @@
1
+ /**
2
+ * Composable condition DSL for the <If> component.
3
+ *
4
+ * Provides a type-safe way to express conditions on FieldRef, HasManyRef, and HasOneRef
5
+ * that works correctly during collection phase. The condition builders ensure that
6
+ * referenced fields are collected for the GraphQL query, even when the condition
7
+ * evaluates to false during collection.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { cond } from '@contember/bindx-react'
12
+ *
13
+ * // Simple conditions
14
+ * <If condition={cond.hasItems(task.developers)} then={...} />
15
+ * <If condition={cond.eq(task.status, 'active')} then={...} />
16
+ *
17
+ * // Complex conditions with AND/OR/NOT
18
+ * <If condition={cond.and(
19
+ * cond.hasItems(task.developers),
20
+ * cond.eq(task.status, 'active')
21
+ * )} then={...} />
22
+ *
23
+ * <If condition={cond.or(
24
+ * cond.isEmpty(task.developers),
25
+ * cond.not(cond.isConnected(task.project))
26
+ * )} then={...} />
27
+ * ```
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+
32
+ import type {
33
+ FieldRef,
34
+ FieldRefBase,
35
+ HasManyRef,
36
+ HasManyRefBase,
37
+ HasOneRef,
38
+ HasOneRefBase,
39
+ } from '@contember/bindx'
40
+
41
+ // ============================================================================
42
+ // Symbols and Types
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Symbol to identify condition objects
47
+ */
48
+ export const CONDITION_META = Symbol('CONDITION_META')
49
+
50
+ /**
51
+ * Internal metadata for a condition.
52
+ */
53
+ export interface ConditionMeta {
54
+ /** Condition type identifier for debugging */
55
+ readonly type: string
56
+ /** Fields to collect for selection (FieldRef/HasManyRef/HasOneRef) */
57
+ readonly fields: unknown[]
58
+ /** Evaluate condition at runtime */
59
+ readonly evaluate: () => boolean
60
+ }
61
+
62
+ /**
63
+ * A composable condition for the <If> component.
64
+ *
65
+ * Conditions can be combined using `cond.and()`, `cond.or()`, and `cond.not()`.
66
+ * All referenced fields are automatically collected for the GraphQL query.
67
+ */
68
+ export interface Condition {
69
+ readonly [CONDITION_META]: ConditionMeta
70
+ }
71
+
72
+ // ============================================================================
73
+ // Type Helpers
74
+ // ============================================================================
75
+
76
+ /**
77
+ * Base type for scalar fields - accepts both FieldRef and FieldRefBase.
78
+ */
79
+ type AnyScalarField<T> = FieldRefBase<T>
80
+
81
+ /**
82
+ * Base type for has-many - accepts both HasManyRef and HasManyRefBase.
83
+ */
84
+ type AnyHasManyField = HasManyRefBase<any, any, any, any, any>
85
+
86
+ /**
87
+ * Base type for has-one - accepts both HasOneRef and HasOneRefBase.
88
+ */
89
+ type AnyHasOneField = HasOneRefBase<any, any, any, any, any>
90
+
91
+ // ============================================================================
92
+ // Condition Builder Object
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Condition builder - single import for all condition operations.
97
+ *
98
+ * Use this to create conditions for the <If> component that work correctly
99
+ * during both collection and runtime phases.
100
+ *
101
+ * @example
102
+ * ```tsx
103
+ * import { cond } from '@contember/bindx-react'
104
+ *
105
+ * // Simple conditions
106
+ * <If condition={cond.hasItems(task.developers)} then={...} />
107
+ * <If condition={cond.eq(task.status, 'active')} then={...} />
108
+ *
109
+ * // Complex conditions with AND/OR/NOT
110
+ * <If condition={cond.and(
111
+ * cond.hasItems(task.developers),
112
+ * cond.eq(task.status, 'active')
113
+ * )} then={...} />
114
+ *
115
+ * <If condition={cond.or(
116
+ * cond.isEmpty(task.developers),
117
+ * cond.not(cond.isConnected(task.project))
118
+ * )} then={...} />
119
+ * ```
120
+ */
121
+ export const cond = {
122
+ // ==================== Has-Many Conditions ====================
123
+
124
+ /**
125
+ * Check if a has-many relation has items.
126
+ *
127
+ * @param field - The has-many field reference
128
+ * @returns A condition that is true when the relation has at least one item
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * <If condition={cond.hasItems(task.developers)} then={
133
+ * <HasMany field={task.developers}>{dev => <span>{dev.name.value}</span>}</HasMany>
134
+ * } />
135
+ * ```
136
+ */
137
+ hasItems(field: AnyHasManyField): Condition {
138
+ return {
139
+ [CONDITION_META]: {
140
+ type: 'hasItems',
141
+ fields: [field],
142
+ evaluate: () => (field as HasManyRef<any>).length > 0,
143
+ },
144
+ }
145
+ },
146
+
147
+ /**
148
+ * Check if a has-many relation is empty.
149
+ *
150
+ * @param field - The has-many field reference
151
+ * @returns A condition that is true when the relation has no items
152
+ *
153
+ * @example
154
+ * ```tsx
155
+ * <If condition={cond.isEmpty(task.developers)} then={
156
+ * <span>No developers assigned</span>
157
+ * } />
158
+ * ```
159
+ */
160
+ isEmpty(field: AnyHasManyField): Condition {
161
+ return {
162
+ [CONDITION_META]: {
163
+ type: 'isEmpty',
164
+ fields: [field],
165
+ evaluate: () => (field as HasManyRef<any>).length === 0,
166
+ },
167
+ }
168
+ },
169
+
170
+ // ==================== Has-One Conditions ====================
171
+
172
+ /**
173
+ * Check if a has-one relation is connected.
174
+ *
175
+ * @param field - The has-one field reference
176
+ * @returns A condition that is true when the relation points to an entity
177
+ *
178
+ * @example
179
+ * ```tsx
180
+ * <If condition={cond.isConnected(task.project)} then={
181
+ * <HasOne field={task.project}>{project => <span>{project.name.value}</span>}</HasOne>
182
+ * } />
183
+ * ```
184
+ */
185
+ isConnected(field: AnyHasOneField): Condition {
186
+ return {
187
+ [CONDITION_META]: {
188
+ type: 'isConnected',
189
+ fields: [field],
190
+ evaluate: () => (field as HasOneRef<any>).$state === 'connected',
191
+ },
192
+ }
193
+ },
194
+
195
+ /**
196
+ * Check if a has-one relation is disconnected.
197
+ *
198
+ * @param field - The has-one field reference
199
+ * @returns A condition that is true when the relation is not connected
200
+ *
201
+ * @example
202
+ * ```tsx
203
+ * <If condition={cond.isDisconnected(task.project)} then={
204
+ * <span>No project assigned</span>
205
+ * } />
206
+ * ```
207
+ */
208
+ isDisconnected(field: AnyHasOneField): Condition {
209
+ return {
210
+ [CONDITION_META]: {
211
+ type: 'isDisconnected',
212
+ fields: [field],
213
+ evaluate: () => (field as HasOneRef<any>).$state === 'disconnected',
214
+ },
215
+ }
216
+ },
217
+
218
+ // ==================== Field Value Conditions ====================
219
+
220
+ /**
221
+ * Check if a field equals a value.
222
+ *
223
+ * @param field - The field reference
224
+ * @param value - The value to compare against
225
+ * @returns A condition that is true when the field value equals the given value
226
+ *
227
+ * @example
228
+ * ```tsx
229
+ * <If condition={cond.eq(task.status, 'completed')} then={
230
+ * <span className="text-green-500">Done!</span>
231
+ * } />
232
+ * ```
233
+ */
234
+ eq<T>(field: AnyScalarField<T>, value: T): Condition {
235
+ return {
236
+ [CONDITION_META]: {
237
+ type: 'eq',
238
+ fields: [field],
239
+ evaluate: () => (field as FieldRef<T>).value === value,
240
+ },
241
+ }
242
+ },
243
+
244
+ /**
245
+ * Check if a field is not equal to a value.
246
+ *
247
+ * @param field - The field reference
248
+ * @param value - The value to compare against
249
+ * @returns A condition that is true when the field value does not equal the given value
250
+ *
251
+ * @example
252
+ * ```tsx
253
+ * <If condition={cond.neq(task.status, 'cancelled')} then={
254
+ * <TaskActions task={task} />
255
+ * } />
256
+ * ```
257
+ */
258
+ neq<T>(field: AnyScalarField<T>, value: T): Condition {
259
+ return {
260
+ [CONDITION_META]: {
261
+ type: 'neq',
262
+ fields: [field],
263
+ evaluate: () => (field as FieldRef<T>).value !== value,
264
+ },
265
+ }
266
+ },
267
+
268
+ /**
269
+ * Check if a field is null.
270
+ *
271
+ * @param field - The field reference
272
+ * @returns A condition that is true when the field value is null
273
+ *
274
+ * @example
275
+ * ```tsx
276
+ * <If condition={cond.isNull(task.completedAt)} then={
277
+ * <span>In progress</span>
278
+ * } />
279
+ * ```
280
+ */
281
+ isNull<T>(field: AnyScalarField<T>): Condition {
282
+ return {
283
+ [CONDITION_META]: {
284
+ type: 'isNull',
285
+ fields: [field],
286
+ evaluate: () => (field as FieldRef<T>).value === null,
287
+ },
288
+ }
289
+ },
290
+
291
+ /**
292
+ * Check if a field is not null.
293
+ *
294
+ * @param field - The field reference
295
+ * @returns A condition that is true when the field value is not null
296
+ *
297
+ * @example
298
+ * ```tsx
299
+ * <If condition={cond.isNotNull(task.completedAt)} then={
300
+ * <span>Completed: {task.completedAt.value}</span>
301
+ * } />
302
+ * ```
303
+ */
304
+ isNotNull<T>(field: AnyScalarField<T>): Condition {
305
+ return {
306
+ [CONDITION_META]: {
307
+ type: 'isNotNull',
308
+ fields: [field],
309
+ evaluate: () => (field as FieldRef<T>).value !== null,
310
+ },
311
+ }
312
+ },
313
+
314
+ /**
315
+ * Check if a field value is truthy.
316
+ *
317
+ * @param field - The field reference (typically boolean)
318
+ * @returns A condition that is true when the field value is truthy
319
+ *
320
+ * @example
321
+ * ```tsx
322
+ * <If condition={cond.isTruthy(task.isUrgent)} then={
323
+ * <Badge variant="destructive">Urgent!</Badge>
324
+ * } />
325
+ * ```
326
+ */
327
+ isTruthy<T>(field: AnyScalarField<T>): Condition {
328
+ return {
329
+ [CONDITION_META]: {
330
+ type: 'isTruthy',
331
+ fields: [field],
332
+ evaluate: () => Boolean((field as FieldRef<T>).value),
333
+ },
334
+ }
335
+ },
336
+
337
+ /**
338
+ * Check if a field value is falsy.
339
+ *
340
+ * @param field - The field reference (typically boolean)
341
+ * @returns A condition that is true when the field value is falsy
342
+ *
343
+ * @example
344
+ * ```tsx
345
+ * <If condition={cond.isFalsy(task.isArchived)} then={
346
+ * <TaskActions task={task} />
347
+ * } />
348
+ * ```
349
+ */
350
+ isFalsy<T>(field: AnyScalarField<T>): Condition {
351
+ return {
352
+ [CONDITION_META]: {
353
+ type: 'isFalsy',
354
+ fields: [field],
355
+ evaluate: () => !(field as FieldRef<T>).value,
356
+ },
357
+ }
358
+ },
359
+
360
+ // ==================== Logical Combinators ====================
361
+
362
+ /**
363
+ * Logical AND - all conditions must be true.
364
+ *
365
+ * @param conditions - Conditions to combine
366
+ * @returns A condition that is true when all given conditions are true
367
+ *
368
+ * @example
369
+ * ```tsx
370
+ * <If condition={cond.and(
371
+ * cond.hasItems(task.developers),
372
+ * cond.eq(task.status, 'active'),
373
+ * cond.isConnected(task.project)
374
+ * )} then={
375
+ * <span>Task is ready</span>
376
+ * } />
377
+ * ```
378
+ */
379
+ and(...conditions: Condition[]): Condition {
380
+ return {
381
+ [CONDITION_META]: {
382
+ type: 'and',
383
+ fields: conditions.flatMap(c => c[CONDITION_META].fields),
384
+ evaluate: () => conditions.every(c => c[CONDITION_META].evaluate()),
385
+ },
386
+ }
387
+ },
388
+
389
+ /**
390
+ * Logical OR - at least one condition must be true.
391
+ *
392
+ * @param conditions - Conditions to combine
393
+ * @returns A condition that is true when at least one given condition is true
394
+ *
395
+ * @example
396
+ * ```tsx
397
+ * <If condition={cond.or(
398
+ * cond.eq(task.status, 'completed'),
399
+ * cond.eq(task.status, 'cancelled')
400
+ * )} then={
401
+ * <span>Task is finished</span>
402
+ * } />
403
+ * ```
404
+ */
405
+ or(...conditions: Condition[]): Condition {
406
+ return {
407
+ [CONDITION_META]: {
408
+ type: 'or',
409
+ fields: conditions.flatMap(c => c[CONDITION_META].fields),
410
+ evaluate: () => conditions.some(c => c[CONDITION_META].evaluate()),
411
+ },
412
+ }
413
+ },
414
+
415
+ /**
416
+ * Logical NOT - negate a condition.
417
+ *
418
+ * @param condition - Condition to negate
419
+ * @returns A condition that is true when the given condition is false
420
+ *
421
+ * @example
422
+ * ```tsx
423
+ * <If condition={cond.not(cond.isConnected(task.project))} then={
424
+ * <span>No project assigned</span>
425
+ * } />
426
+ * ```
427
+ */
428
+ not(condition: Condition): Condition {
429
+ return {
430
+ [CONDITION_META]: {
431
+ type: 'not',
432
+ fields: condition[CONDITION_META].fields,
433
+ evaluate: () => !condition[CONDITION_META].evaluate(),
434
+ },
435
+ }
436
+ },
437
+ } as const
438
+
439
+ // ============================================================================
440
+ // Helper Functions
441
+ // ============================================================================
442
+
443
+ /**
444
+ * Check if a value is a Condition object.
445
+ *
446
+ * @param value - Value to check
447
+ * @returns True if the value is a Condition
448
+ */
449
+ export function isCondition(value: unknown): value is Condition {
450
+ return typeof value === 'object' && value !== null && CONDITION_META in value
451
+ }
452
+
453
+ /**
454
+ * Collect all field references from a condition.
455
+ * Used by the selection collector to ensure all fields are included in the query.
456
+ *
457
+ * @param condition - The condition to collect fields from
458
+ * @returns Array of field references
459
+ */
460
+ export function collectConditionFields(condition: Condition): unknown[] {
461
+ return condition[CONDITION_META].fields
462
+ }
463
+
464
+ /**
465
+ * Evaluate a condition at runtime.
466
+ *
467
+ * @param condition - The condition to evaluate
468
+ * @returns The boolean result of the condition
469
+ */
470
+ export function evaluateCondition(condition: Condition): boolean {
471
+ return condition[CONDITION_META].evaluate()
472
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Component creation utilities for bindx.
3
+ *
4
+ * The main createComponent function is now provided by createBindx() using the builder API.
5
+ * This module re-exports builder utilities and provides helper functions.
6
+ */
7
+
8
+ import type { FluentFragment, AnyBrand } from '@contember/bindx'
9
+ import { ComponentBrand } from '@contember/bindx'
10
+ import { SelectionMetaCollector, mergeSelections } from './SelectionMeta.js'
11
+ import { COMPONENT_BRAND } from './componentBuilder.js'
12
+
13
+ // Re-export from builder module
14
+ export {
15
+ COMPONENT_MARKER,
16
+ COMPONENT_BRAND,
17
+ COMPONENT_SELECTIONS,
18
+ ComponentBuilderImpl,
19
+ createComponentBuilder,
20
+ isBindxComponent,
21
+ getComponentBrand,
22
+ } from './componentBuilder.js'
23
+
24
+ export type {
25
+ SelectionPropMeta,
26
+ BindxComponentBase,
27
+ BindxComponent,
28
+ ComponentBuilder,
29
+ ComponentBuilderState,
30
+ CreateComponentOptions,
31
+ // Interface types
32
+ InterfaceEntityPropConfig,
33
+ ImplicitInterfaceEntityConfig,
34
+ ExplicitInterfaceEntityConfig,
35
+ AddInterfaces,
36
+ InterfaceSelectorsMap,
37
+ AnyEntityPropConfig,
38
+ // Entity prop config types
39
+ EntityPropConfig,
40
+ ImplicitEntityConfig,
41
+ ExplicitEntityConfig,
42
+ // State helpers
43
+ AddImplicitEntity,
44
+ AddExplicitEntity,
45
+ AddImplicitInterfaceEntity,
46
+ AddExplicitInterfaceEntity,
47
+ SetScalarProps,
48
+ // Props building
49
+ BuildEntityProps,
50
+ BuildProps,
51
+ BuildFragmentProps,
52
+ InitialBuilderState,
53
+ } from './componentBuilder.types.js'
54
+
55
+ // ============================================================================
56
+ // Legacy exports for backwards compatibility
57
+ // ============================================================================
58
+
59
+ // These are kept for backwards compatibility with existing code
60
+ // that imports from createComponent.ts
61
+
62
+ export { assignFragmentProperties } from './componentBuilderCompat.js'
63
+
64
+ // ============================================================================
65
+ // Brand Validation
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Flag to enable/disable brand validation.
70
+ * Set to false in production for performance.
71
+ */
72
+ let brandValidationEnabled = true
73
+
74
+ /**
75
+ * Enables or disables brand validation.
76
+ * Call with false in production to disable warnings.
77
+ */
78
+ export function setBrandValidation(enabled: boolean): void {
79
+ brandValidationEnabled = enabled
80
+ }
81
+
82
+ /**
83
+ * Validates that an EntityRef has the required component brand.
84
+ * Logs a warning in development mode if the brand is missing.
85
+ *
86
+ * @param entityRef - The EntityRef to validate
87
+ * @param component - The component that requires the brand
88
+ * @param propName - The prop name for error messages
89
+ */
90
+ export function validateBrand(
91
+ entityRef: { __brands?: Set<symbol> } | null | undefined,
92
+ component: { [key: symbol]: ComponentBrand | undefined },
93
+ propName: string,
94
+ ): void {
95
+ if (!brandValidationEnabled || !entityRef) {
96
+ return
97
+ }
98
+
99
+ const componentBrand = component[COMPONENT_BRAND] as ComponentBrand | undefined
100
+ if (!componentBrand) {
101
+ return
102
+ }
103
+
104
+ const entityBrands = entityRef.__brands
105
+ if (!entityBrands) {
106
+ // EntityRef doesn't have brands yet (might be from legacy code)
107
+ return
108
+ }
109
+
110
+ if (!entityBrands.has(componentBrand.brandSymbol)) {
111
+ console.warn(
112
+ `[bindx] EntityRef passed to "${propName}" is missing the required component brand. ` +
113
+ `Use mergeFragments() to combine fragment selections when using multiple components. ` +
114
+ `Expected brand: ${componentBrand.name}`,
115
+ )
116
+ }
117
+ }
118
+
119
+ // ============================================================================
120
+ // Fragment Merging
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Merges multiple fragment selections into one.
125
+ * Combines both the selection metadata and brand symbols.
126
+ *
127
+ * @example
128
+ * ```tsx
129
+ * const article = useEntity('Article', { id }, e =>
130
+ * e.author(mergeFragments(AuthorInfo.$author, AuthorCard.$author))
131
+ * )
132
+ * ```
133
+ */
134
+ export function mergeFragments<T, R1 extends object, B1 extends AnyBrand>(
135
+ fragment1: FluentFragment<T, R1, B1>,
136
+ ): FluentFragment<T, R1, B1>
137
+ export function mergeFragments<T, R1 extends object, R2 extends object, B1 extends AnyBrand, B2 extends AnyBrand>(
138
+ fragment1: FluentFragment<T, R1, B1>,
139
+ fragment2: FluentFragment<T, R2, B2>,
140
+ ): FluentFragment<T, R1 & R2, B1 | B2>
141
+ export function mergeFragments<T, R1 extends object, R2 extends object, R3 extends object, B1 extends AnyBrand, B2 extends AnyBrand, B3 extends AnyBrand>(
142
+ fragment1: FluentFragment<T, R1, B1>,
143
+ fragment2: FluentFragment<T, R2, B2>,
144
+ fragment3: FluentFragment<T, R3, B3>,
145
+ ): FluentFragment<T, R1 & R2 & R3, B1 | B2 | B3>
146
+ export function mergeFragments<T>(
147
+ ...fragments: FluentFragment<T, object, AnyBrand>[]
148
+ ): FluentFragment<T, object, AnyBrand> {
149
+ if (fragments.length === 0) {
150
+ throw new Error('mergeFragments requires at least one fragment')
151
+ }
152
+
153
+ if (fragments.length === 1) {
154
+ return fragments[0]!
155
+ }
156
+
157
+ const mergedSelection = new SelectionMetaCollector()
158
+ const mergedBrands = new Set<symbol>()
159
+
160
+ for (const fragment of fragments) {
161
+ mergeSelections(mergedSelection, fragment.__meta)
162
+ // Merge brand symbols from all fragments
163
+ if (fragment.__brands) {
164
+ fragment.__brands.forEach(b => mergedBrands.add(b))
165
+ }
166
+ }
167
+
168
+ return {
169
+ __meta: mergedSelection,
170
+ __resultType: {} as object,
171
+ __modelType: undefined as unknown as T,
172
+ __isFragment: true,
173
+ __brands: mergedBrands,
174
+ }
175
+ }