@contember/bindx-react 0.1.5 → 0.1.11
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/hooks/BackendAdapterContext.d.ts +11 -0
- package/dist/hooks/BackendAdapterContext.d.ts.map +1 -1
- package/dist/hooks/ContemberBindxProvider.d.ts +25 -30
- package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +3 -3
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useEntity.d.ts +43 -75
- package/dist/hooks/useEntity.d.ts.map +1 -1
- package/dist/hooks/useEntityList.d.ts +28 -47
- package/dist/hooks/useEntityList.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/jsx/collectorProxy.d.ts.map +1 -1
- package/dist/jsx/components/Entity.d.ts.map +1 -1
- package/dist/jsx/index.d.ts +1 -1
- package/dist/jsx/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/hooks/BackendAdapterContext.tsx +11 -0
- package/src/hooks/ContemberBindxProvider.tsx +32 -37
- package/src/hooks/index.ts +10 -9
- package/src/hooks/useEntity.ts +108 -217
- package/src/hooks/useEntityList.ts +61 -100
- package/src/index.ts +11 -9
- package/src/jsx/collectorProxy.ts +23 -4
- package/src/jsx/components/Entity.tsx +15 -9
- package/src/jsx/components/EntityList.tsx +5 -5
- package/src/jsx/index.ts +1 -0
|
@@ -34,106 +34,66 @@ export interface UseEntityListOptions {
|
|
|
34
34
|
// Result types
|
|
35
35
|
// ============================================================================
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
readonly status: 'loading'
|
|
42
|
-
readonly isLoading: true
|
|
43
|
-
readonly isError: false
|
|
44
|
-
readonly isDirty: false
|
|
45
|
-
readonly items: never
|
|
46
|
-
readonly length: 0
|
|
47
|
-
add(data?: unknown): string
|
|
48
|
-
remove(key: string): void
|
|
49
|
-
move(fromIndex: number, toIndex: number): void
|
|
37
|
+
interface EntityListResultBase {
|
|
38
|
+
$add(data?: unknown): string
|
|
39
|
+
$remove(key: string): void
|
|
40
|
+
$move(fromIndex: number, toIndex: number): void
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
readonly
|
|
57
|
-
readonly isLoading: false
|
|
58
|
-
readonly isError: true
|
|
59
|
-
readonly error: FieldError
|
|
60
|
-
readonly isDirty: false
|
|
61
|
-
readonly items: never
|
|
62
|
-
readonly length: 0
|
|
63
|
-
add(data?: unknown): string
|
|
64
|
-
remove(key: string): void
|
|
65
|
-
move(fromIndex: number, toIndex: number): void
|
|
43
|
+
export type LoadingEntityListResult = EntityListResultBase & {
|
|
44
|
+
readonly $status: 'loading'
|
|
45
|
+
readonly $isLoading: true
|
|
46
|
+
readonly $isError: false
|
|
47
|
+
readonly $error: null
|
|
66
48
|
}
|
|
67
49
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
readonly
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
export type ErrorEntityListResult = EntityListResultBase & {
|
|
51
|
+
readonly $status: 'error'
|
|
52
|
+
readonly $isLoading: false
|
|
53
|
+
readonly $isError: true
|
|
54
|
+
readonly $error: FieldError
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ReadyEntityListResult<T extends object> = EntityListResultBase & {
|
|
58
|
+
readonly $status: 'ready'
|
|
59
|
+
readonly $isLoading: false
|
|
60
|
+
readonly $isError: false
|
|
61
|
+
readonly $error: null
|
|
62
|
+
readonly $isDirty: boolean
|
|
76
63
|
readonly items: Array<EntityAccessor<T>>
|
|
77
64
|
readonly length: number
|
|
78
|
-
add(data?: Partial<T>): string
|
|
79
|
-
remove(key: string): void
|
|
80
|
-
move(fromIndex: number, toIndex: number): void
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
| LoadingEntityListAccessor
|
|
88
|
-
| ErrorEntityListAccessor
|
|
89
|
-
| ReadyEntityListAccessor<T>
|
|
67
|
+
export type UseEntityListResult<T extends object> =
|
|
68
|
+
| LoadingEntityListResult
|
|
69
|
+
| ErrorEntityListResult
|
|
70
|
+
| ReadyEntityListResult<T>
|
|
90
71
|
|
|
91
72
|
// ============================================================================
|
|
92
73
|
// Internal helpers
|
|
93
74
|
// ============================================================================
|
|
94
75
|
|
|
95
|
-
function
|
|
76
|
+
function createLoadingListResult(): LoadingEntityListResult {
|
|
96
77
|
return {
|
|
97
|
-
status: 'loading',
|
|
98
|
-
isLoading: true,
|
|
99
|
-
isError: false,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
length: 0,
|
|
105
|
-
add() {
|
|
106
|
-
throw new Error('Cannot add items while loading')
|
|
107
|
-
},
|
|
108
|
-
remove() {
|
|
109
|
-
throw new Error('Cannot remove items while loading')
|
|
110
|
-
},
|
|
111
|
-
move() {
|
|
112
|
-
throw new Error('Cannot move items while loading')
|
|
113
|
-
},
|
|
78
|
+
$status: 'loading',
|
|
79
|
+
$isLoading: true,
|
|
80
|
+
$isError: false,
|
|
81
|
+
$error: null,
|
|
82
|
+
$add() { throw new Error('Cannot add items while loading') },
|
|
83
|
+
$remove() { throw new Error('Cannot remove items while loading') },
|
|
84
|
+
$move() { throw new Error('Cannot move items while loading') },
|
|
114
85
|
}
|
|
115
86
|
}
|
|
116
87
|
|
|
117
|
-
function
|
|
88
|
+
function createErrorListResult(error: FieldError): ErrorEntityListResult {
|
|
118
89
|
return {
|
|
119
|
-
status: 'error',
|
|
120
|
-
isLoading: false,
|
|
121
|
-
isError: true,
|
|
122
|
-
error,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
},
|
|
127
|
-
length: 0,
|
|
128
|
-
add() {
|
|
129
|
-
throw new Error('Cannot add items after error')
|
|
130
|
-
},
|
|
131
|
-
remove() {
|
|
132
|
-
throw new Error('Cannot remove items after error')
|
|
133
|
-
},
|
|
134
|
-
move() {
|
|
135
|
-
throw new Error('Cannot move items after error')
|
|
136
|
-
},
|
|
90
|
+
$status: 'error',
|
|
91
|
+
$isLoading: false,
|
|
92
|
+
$isError: true,
|
|
93
|
+
$error: error,
|
|
94
|
+
$add() { throw new Error('Cannot add items after error') },
|
|
95
|
+
$remove() { throw new Error('Cannot remove items after error') },
|
|
96
|
+
$move() { throw new Error('Cannot move items after error') },
|
|
137
97
|
}
|
|
138
98
|
}
|
|
139
99
|
|
|
@@ -157,7 +117,7 @@ export function useEntityList<
|
|
|
157
117
|
entity: EntityDef<TRoleMap>,
|
|
158
118
|
options: UseEntityListOptions & { roles: readonly TRoles[] },
|
|
159
119
|
definer: SelectionInput<EntityForRoles<TRoleMap, TRoles>, TResult>,
|
|
160
|
-
):
|
|
120
|
+
): UseEntityListResult<TResult>
|
|
161
121
|
|
|
162
122
|
/**
|
|
163
123
|
* Hook to fetch and manage a list of entities with full type inference.
|
|
@@ -174,7 +134,7 @@ export function useEntityList<TRoleMap extends Record<string, object>, TResult e
|
|
|
174
134
|
entity: EntityDef<TRoleMap>,
|
|
175
135
|
options: UseEntityListOptions,
|
|
176
136
|
definer: SelectionInput<CommonEntity<TRoleMap>, TResult>,
|
|
177
|
-
):
|
|
137
|
+
): UseEntityListResult<TResult>
|
|
178
138
|
|
|
179
139
|
/**
|
|
180
140
|
* Hook to fetch and manage a list of entities with pre-resolved selection.
|
|
@@ -185,7 +145,7 @@ export function useEntityList<TRoleMap extends Record<string, object>, TResult e
|
|
|
185
145
|
export function useEntityList(
|
|
186
146
|
entity: EntityDef,
|
|
187
147
|
options: UseEntityListOptions & { selection: SelectionMeta },
|
|
188
|
-
):
|
|
148
|
+
): UseEntityListResult<object>
|
|
189
149
|
|
|
190
150
|
// ============================================================================
|
|
191
151
|
// Implementation
|
|
@@ -197,7 +157,7 @@ export function useEntityList(
|
|
|
197
157
|
options: UseEntityListOptions,
|
|
198
158
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
159
|
definer?: SelectionInput<any, any>,
|
|
200
|
-
):
|
|
160
|
+
): UseEntityListResult<any> {
|
|
201
161
|
const schemaRegistry = useSchemaRegistry()
|
|
202
162
|
const entityType = entity.$name
|
|
203
163
|
const { store, dispatcher, batcher } = useBindxContext()
|
|
@@ -250,7 +210,7 @@ export function useEntityList(
|
|
|
250
210
|
version: number
|
|
251
211
|
storeVersion: number
|
|
252
212
|
status: string
|
|
253
|
-
result:
|
|
213
|
+
result: UseEntityListResult<any>
|
|
254
214
|
} | null>(null)
|
|
255
215
|
|
|
256
216
|
// --- Store subscription ---
|
|
@@ -310,7 +270,7 @@ export function useEntityList(
|
|
|
310
270
|
)
|
|
311
271
|
|
|
312
272
|
// --- Snapshot ---
|
|
313
|
-
const getSnapshot = useCallback(():
|
|
273
|
+
const getSnapshot = useCallback((): UseEntityListResult<any> => {
|
|
314
274
|
const state = listStateRef.current
|
|
315
275
|
const version = versionRef.current
|
|
316
276
|
const storeVersion = store.getVersion()
|
|
@@ -320,12 +280,12 @@ export function useEntityList(
|
|
|
320
280
|
return cache.result
|
|
321
281
|
}
|
|
322
282
|
|
|
323
|
-
let result:
|
|
283
|
+
let result: UseEntityListResult<any>
|
|
324
284
|
|
|
325
285
|
if (state.status === 'loading') {
|
|
326
|
-
result =
|
|
286
|
+
result = createLoadingListResult()
|
|
327
287
|
} else if (state.status === 'error') {
|
|
328
|
-
result =
|
|
288
|
+
result = createErrorListResult(state.error!)
|
|
329
289
|
} else {
|
|
330
290
|
const items = state.items.map((item) => {
|
|
331
291
|
return EntityHandle.create<object>(
|
|
@@ -334,19 +294,20 @@ export function useEntityList(
|
|
|
334
294
|
store,
|
|
335
295
|
dispatcher,
|
|
336
296
|
schemaRegistry as SchemaRegistry<Record<string, object>>,
|
|
337
|
-
) as unknown as EntityAccessor<
|
|
297
|
+
) as unknown as EntityAccessor<any>
|
|
338
298
|
})
|
|
339
299
|
|
|
340
300
|
result = {
|
|
341
|
-
status: 'ready',
|
|
342
|
-
isLoading: false,
|
|
343
|
-
isError: false,
|
|
344
|
-
|
|
301
|
+
$status: 'ready',
|
|
302
|
+
$isLoading: false,
|
|
303
|
+
$isError: false,
|
|
304
|
+
$error: null,
|
|
305
|
+
$isDirty: false,
|
|
345
306
|
items,
|
|
346
307
|
length: items.length,
|
|
347
|
-
add: addItem,
|
|
348
|
-
remove: removeItem,
|
|
349
|
-
move: moveItem,
|
|
308
|
+
$add: addItem,
|
|
309
|
+
$remove: removeItem,
|
|
310
|
+
$move: moveItem,
|
|
350
311
|
}
|
|
351
312
|
}
|
|
352
313
|
|
|
@@ -361,7 +322,7 @@ export function useEntityList(
|
|
|
361
322
|
}, [entityType, store, dispatcher, schemaRegistry, addItem, removeItem, moveItem])
|
|
362
323
|
|
|
363
324
|
const isEqual = useCallback(
|
|
364
|
-
(a:
|
|
325
|
+
(a: UseEntityListResult<any>, b: UseEntityListResult<any>): boolean => {
|
|
365
326
|
return a === b
|
|
366
327
|
},
|
|
367
328
|
[],
|
package/src/index.ts
CHANGED
|
@@ -184,17 +184,18 @@ export {
|
|
|
184
184
|
export type {
|
|
185
185
|
BindxProviderProps,
|
|
186
186
|
BindxContextValue,
|
|
187
|
+
BindxGraphQlClient,
|
|
187
188
|
UseEntityOptions,
|
|
188
189
|
UseEntityListOptions,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
LoadingEntityResult,
|
|
191
|
+
ErrorEntityResult,
|
|
192
|
+
NotFoundEntityResult,
|
|
193
|
+
ReadyEntityResult,
|
|
194
|
+
UseEntityResult,
|
|
195
|
+
LoadingEntityListResult,
|
|
196
|
+
ErrorEntityListResult,
|
|
197
|
+
ReadyEntityListResult,
|
|
198
|
+
UseEntityListResult,
|
|
198
199
|
// Persistence hook types
|
|
199
200
|
PersistApi,
|
|
200
201
|
EntityPersistApi,
|
|
@@ -316,6 +317,7 @@ export {
|
|
|
316
317
|
HasRole,
|
|
317
318
|
type HasRoleProps,
|
|
318
319
|
BINDX_COMPONENT,
|
|
320
|
+
SCOPE_REF,
|
|
319
321
|
// Condition DSL for <If> component
|
|
320
322
|
cond,
|
|
321
323
|
type Condition,
|
|
@@ -47,6 +47,7 @@ export function createCollectorProxy<T>(
|
|
|
47
47
|
$fields: fieldsProxy,
|
|
48
48
|
$data: null,
|
|
49
49
|
$isDirty: false,
|
|
50
|
+
$isPersisting: false,
|
|
50
51
|
$persistedId: null,
|
|
51
52
|
$isNew: true,
|
|
52
53
|
__entityType: undefined as unknown as T,
|
|
@@ -189,7 +190,7 @@ function createCollectorFieldRef(
|
|
|
189
190
|
items: [],
|
|
190
191
|
map: mapFn,
|
|
191
192
|
add: () => '',
|
|
192
|
-
getById: () => (
|
|
193
|
+
getById: () => createCollectorProxy<unknown>(getChildScope(), targetEntityName, schemaRegistry),
|
|
193
194
|
remove: () => {},
|
|
194
195
|
move: () => {},
|
|
195
196
|
connect: () => {},
|
|
@@ -226,6 +227,7 @@ function createCollectorFieldRef(
|
|
|
226
227
|
// EntityRef-compatible properties (for HasOneAccessor = EntityAccessor compatibility)
|
|
227
228
|
id: placeholderId,
|
|
228
229
|
$data: null,
|
|
230
|
+
$isPersisting: false,
|
|
229
231
|
$isNew: false,
|
|
230
232
|
$persistedId: null,
|
|
231
233
|
__entityName: targetEntityName ?? '',
|
|
@@ -248,20 +250,27 @@ function createCollectorFieldRef(
|
|
|
248
250
|
// This works for all field types:
|
|
249
251
|
// - Scalar fields: Accessing ref.value works (exists on target, passes through)
|
|
250
252
|
// - Has-many fields: Accessing ref.items/length works (exists on target, passes through)
|
|
251
|
-
// - Has-one with schema: Direct field access proxies to $fields
|
|
253
|
+
// - Has-one with schema: Direct field access proxies to $fields (isHasOneRelation=true)
|
|
252
254
|
// - Has-one without schema: Same - direct field access proxies to $fields
|
|
253
255
|
// The hasOneFieldsProxy creates nested collector refs which properly track selection.
|
|
254
|
-
return wrapCollectorRefWithFieldAccessProxy(refObject, hasOneFieldsProxy)
|
|
256
|
+
return wrapCollectorRefWithFieldAccessProxy(refObject, hasOneFieldsProxy, isHasOneRelation)
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
/**
|
|
258
260
|
* Wraps a collector ref in a Proxy that supports direct field access for hasOne relations.
|
|
259
261
|
* - `ref.fieldName` is equivalent to `ref.$fields.fieldName`
|
|
260
262
|
* - Known ref properties pass through to the target
|
|
263
|
+
*
|
|
264
|
+
* When `isHasOneRelation` is true, matches runtime EntityHandle proxy behavior:
|
|
265
|
+
* only `id`, `$`-prefixed, and `__`-prefixed properties pass through;
|
|
266
|
+
* everything else is delegated to `fieldsProxy` as field access on the related entity.
|
|
267
|
+
* This prevents built-in ref properties (like `items`, `value`, `map`) from
|
|
268
|
+
* shadowing entity field names on the has-one target.
|
|
261
269
|
*/
|
|
262
270
|
function wrapCollectorRefWithFieldAccessProxy(
|
|
263
271
|
ref: CollectorRef,
|
|
264
272
|
fieldsProxy: EntityFields<unknown>,
|
|
273
|
+
isHasOneRelation: boolean,
|
|
265
274
|
): CollectorRef {
|
|
266
275
|
return new Proxy(ref, {
|
|
267
276
|
get(target, prop) {
|
|
@@ -270,7 +279,17 @@ function wrapCollectorRefWithFieldAccessProxy(
|
|
|
270
279
|
return Reflect.get(target, prop)
|
|
271
280
|
}
|
|
272
281
|
|
|
273
|
-
|
|
282
|
+
if (isHasOneRelation) {
|
|
283
|
+
// For has-one relations, match runtime EntityHandle proxy behavior:
|
|
284
|
+
// Only pass through id, $-prefixed, and __-prefixed properties.
|
|
285
|
+
// Everything else is field access on the related entity.
|
|
286
|
+
if (prop === 'id' || prop.startsWith('$') || prop.startsWith('__')) {
|
|
287
|
+
return Reflect.get(target, prop)
|
|
288
|
+
}
|
|
289
|
+
return fieldsProxy[prop as keyof EntityFields<unknown>]
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// For has-many/scalar/unknown: existing behavior
|
|
274
293
|
if (prop in target) {
|
|
275
294
|
return Reflect.get(target, prop)
|
|
276
295
|
}
|
|
@@ -106,18 +106,19 @@ function EntityByMode({
|
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
// Render based on status
|
|
109
|
-
if (result
|
|
109
|
+
if (result.$status === 'loading') {
|
|
110
110
|
return <>{loading ?? <DefaultLoading />}</>
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
if (result
|
|
113
|
+
if (result.$status === 'error') {
|
|
114
|
+
const error: FieldError = result.$error ?? { source: 'load', message: 'Unknown error', category: 'unknown', retryable: false }
|
|
114
115
|
if (errorFallback) {
|
|
115
|
-
return <>{errorFallback(
|
|
116
|
+
return <>{errorFallback(error)}</>
|
|
116
117
|
}
|
|
117
|
-
return <DefaultError error={
|
|
118
|
+
return <DefaultError error={error} />
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
if (result
|
|
121
|
+
if (result.$status === 'not_found') {
|
|
121
122
|
return <>{notFound ?? <DefaultNotFound entityType={entityType} by={by} />}</>
|
|
122
123
|
}
|
|
123
124
|
|
|
@@ -271,14 +272,19 @@ function EntityHandleRenderer({
|
|
|
271
272
|
|
|
272
273
|
const version = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
273
274
|
|
|
274
|
-
const
|
|
275
|
-
() => EntityHandle.
|
|
275
|
+
const rawHandle = useMemo(
|
|
276
|
+
() => EntityHandle.createRaw(entityId, entityType, store, dispatcher, schemaRegistry, undefined, selection),
|
|
276
277
|
[entityId, entityType, store, dispatcher, schemaRegistry, selection, version],
|
|
277
278
|
)
|
|
278
279
|
|
|
279
|
-
|
|
280
|
+
const handle = useMemo(
|
|
281
|
+
() => EntityHandle.wrapProxy(rawHandle),
|
|
282
|
+
[rawHandle],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
useEffect(() => () => { rawHandle.dispose() }, [rawHandle])
|
|
280
286
|
|
|
281
|
-
return <>{children(handle as
|
|
287
|
+
return <>{children(handle as EntityAccessor<unknown>)}</>
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
// ==================== Main Entity Component ====================
|
|
@@ -86,15 +86,15 @@ function EntityListComponent<TRoleMap extends Record<string, object>>({
|
|
|
86
86
|
})
|
|
87
87
|
|
|
88
88
|
// Render based on status
|
|
89
|
-
if (result
|
|
89
|
+
if (result.$status === 'loading') {
|
|
90
90
|
return <>{loading ?? <DefaultLoading />}</>
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (result
|
|
93
|
+
if (result.$status === 'error') {
|
|
94
94
|
if (errorFallback) {
|
|
95
|
-
return <>{errorFallback(result
|
|
95
|
+
return <>{errorFallback(result.$error)}</>
|
|
96
96
|
}
|
|
97
|
-
return <DefaultError error={result
|
|
97
|
+
return <DefaultError error={result.$error} />
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Empty state
|
|
@@ -103,7 +103,7 @@ function EntityListComponent<TRoleMap extends Record<string, object>>({
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Phase 3: Runtime render — items are already EntityAccessors from the hook
|
|
106
|
-
const items = result.items.map((item, index) => {
|
|
106
|
+
const items = result.items.map((item: (typeof result.items)[number], index: number) => {
|
|
107
107
|
return (
|
|
108
108
|
<React.Fragment key={item.id}>
|
|
109
109
|
{children(item as unknown as EntityAccessor<CommonEntity<TRoleMap>>, index)}
|