@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
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import { memo, useMemo, type ReactNode } from 'react'
|
|
2
2
|
import { GraphQlClient } from '@contember/graphql-client'
|
|
3
|
-
import { ContentClient
|
|
3
|
+
import { ContentClient } from '@contember/bindx-client'
|
|
4
4
|
import { ContemberAdapter, SnapshotStore, ActionDispatcher, BatchPersister, MutationCollector, ContemberSchemaMutationAdapter, UndoManager, SchemaRegistry, type SchemaDefinition, type FieldDef, type UndoManagerConfig, type UpdateMode } from '@contember/bindx'
|
|
5
5
|
import { BindxContext, type BindxContextValue } from './BackendAdapterContext.js'
|
|
6
6
|
import { QueryBatcher } from '../batching/QueryBatcher.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Runtime schema names format (from Contember API / generated code).
|
|
10
|
+
* Used by ContemberBindxProvider for backwards compatibility.
|
|
11
|
+
*/
|
|
12
|
+
export interface SchemaNames {
|
|
13
|
+
readonly entities: {
|
|
14
|
+
readonly [entityName: string]: {
|
|
15
|
+
readonly name: string
|
|
16
|
+
readonly scalars: readonly string[]
|
|
17
|
+
readonly fields: {
|
|
18
|
+
readonly [fieldName: string]:
|
|
19
|
+
| { readonly type: 'column'; readonly enumName?: string; readonly columnType?: string }
|
|
20
|
+
| { readonly type: 'many' | 'one'; readonly entity: string }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
readonly enums?: {
|
|
25
|
+
readonly [enumName: string]: readonly string[]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
/**
|
|
9
30
|
* Converts SchemaNames (Contember format) to SchemaDefinition (bindx format)
|
|
10
31
|
* so that a SchemaRegistry can be created for standalone hooks.
|
|
11
32
|
*/
|
|
12
33
|
export function schemaNamesToDef(schemaNames: SchemaNames): SchemaDefinition<Record<string, object>> {
|
|
13
|
-
const enumsMap =
|
|
34
|
+
const enumsMap = schemaNames.enums ?? {}
|
|
14
35
|
const entities: Record<string, { fields: Record<string, FieldDef> }> = {}
|
|
15
36
|
for (const [entityName, entity] of Object.entries(schemaNames.entities)) {
|
|
16
37
|
const fields: Record<string, FieldDef> = {}
|
|
17
38
|
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
18
39
|
if (fieldDef.type === 'column') {
|
|
19
|
-
const enumName =
|
|
20
|
-
const columnType =
|
|
40
|
+
const enumName = fieldDef.enumName
|
|
41
|
+
const columnType = fieldDef.columnType
|
|
21
42
|
const enumValues = enumName ? enumsMap[enumName] : undefined
|
|
22
43
|
if (enumName && enumValues) {
|
|
23
44
|
fields[fieldName] = { type: 'enum', enumName, values: enumValues }
|
|
@@ -64,35 +85,6 @@ export interface ContemberBindxProviderProps {
|
|
|
64
85
|
|
|
65
86
|
/**
|
|
66
87
|
* Provider component that combines Contember authentication with bindx data binding.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```tsx
|
|
70
|
-
* import { ContemberBindxProvider, useUndo } from '@contember/bindx-react'
|
|
71
|
-
* import { schema } from './generated/schema'
|
|
72
|
-
*
|
|
73
|
-
* function App() {
|
|
74
|
-
* return (
|
|
75
|
-
* <ContemberBindxProvider
|
|
76
|
-
* client={graphQlClient}
|
|
77
|
-
* schema={schema}
|
|
78
|
-
* undoManager={true}
|
|
79
|
-
* >
|
|
80
|
-
* <ArticleEditor id="123" />
|
|
81
|
-
* </ContemberBindxProvider>
|
|
82
|
-
* )
|
|
83
|
-
* }
|
|
84
|
-
*
|
|
85
|
-
* // Access undo/redo in child components
|
|
86
|
-
* function UndoControls() {
|
|
87
|
-
* const { canUndo, canRedo, undo, redo } = useUndo()
|
|
88
|
-
* return (
|
|
89
|
-
* <div>
|
|
90
|
-
* <button onClick={undo} disabled={!canUndo}>Undo</button>
|
|
91
|
-
* <button onClick={redo} disabled={!canRedo}>Redo</button>
|
|
92
|
-
* </div>
|
|
93
|
-
* )
|
|
94
|
-
* }
|
|
95
|
-
* ```
|
|
96
88
|
*/
|
|
97
89
|
export const ContemberBindxProvider = memo(function ContemberBindxProvider({
|
|
98
90
|
schema,
|
|
@@ -108,7 +100,12 @@ export const ContemberBindxProvider = memo(function ContemberBindxProvider({
|
|
|
108
100
|
// Create GraphQL client and adapter
|
|
109
101
|
const bindxValue = useMemo((): BindxContextValue => {
|
|
110
102
|
|
|
111
|
-
|
|
103
|
+
// Create schema registry from SchemaNames for standalone hooks
|
|
104
|
+
const schemaRegistry = new SchemaRegistry(schemaNamesToDef(schema))
|
|
105
|
+
|
|
106
|
+
// Create adapter using new bindx-client ContentClient
|
|
107
|
+
const contentClient = new ContentClient(graphQlClient)
|
|
108
|
+
const adapter = new ContemberAdapter(contentClient, schemaRegistry)
|
|
112
109
|
const batcher = new QueryBatcher(adapter)
|
|
113
110
|
|
|
114
111
|
const store = customStore ?? new SnapshotStore()
|
|
@@ -124,9 +121,6 @@ export const ContemberBindxProvider = memo(function ContemberBindxProvider({
|
|
|
124
121
|
dispatcher.addMiddleware(undoManager.createMiddleware())
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
// Create schema registry from SchemaNames for standalone hooks
|
|
128
|
-
const schemaRegistry = new SchemaRegistry(schemaNamesToDef(schema))
|
|
129
|
-
|
|
130
124
|
// Create mutation collector for proper nested operations
|
|
131
125
|
// Use ContemberSchemaMutationAdapter to wrap SchemaNames
|
|
132
126
|
const schemaAdapter = new ContemberSchemaMutationAdapter(schema)
|
|
@@ -146,6 +140,7 @@ export const ContemberBindxProvider = memo(function ContemberBindxProvider({
|
|
|
146
140
|
batchPersister,
|
|
147
141
|
schema: schemaRegistry,
|
|
148
142
|
undoManager,
|
|
143
|
+
graphQlClient,
|
|
149
144
|
debug,
|
|
150
145
|
}
|
|
151
146
|
}, [schema, customStore, undoManagerProp, undoConfig, defaultUpdateMode, debug])
|
package/src/hooks/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export {
|
|
|
8
8
|
useSchemaRegistry,
|
|
9
9
|
type BindxProviderProps,
|
|
10
10
|
type BindxContextValue,
|
|
11
|
+
type BindxGraphQlClient,
|
|
11
12
|
} from './BackendAdapterContext.js'
|
|
12
13
|
|
|
13
14
|
export {
|
|
@@ -21,20 +22,20 @@ export {
|
|
|
21
22
|
export {
|
|
22
23
|
useEntity,
|
|
23
24
|
type UseEntityOptions,
|
|
24
|
-
type
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
type
|
|
25
|
+
type LoadingEntityResult,
|
|
26
|
+
type ErrorEntityResult,
|
|
27
|
+
type NotFoundEntityResult,
|
|
28
|
+
type ReadyEntityResult,
|
|
29
|
+
type UseEntityResult,
|
|
29
30
|
} from './useEntity.js'
|
|
30
31
|
|
|
31
32
|
export {
|
|
32
33
|
useEntityList,
|
|
33
34
|
type UseEntityListOptions,
|
|
34
|
-
type
|
|
35
|
-
type
|
|
36
|
-
type
|
|
37
|
-
type
|
|
35
|
+
type LoadingEntityListResult,
|
|
36
|
+
type ErrorEntityListResult,
|
|
37
|
+
type ReadyEntityListResult,
|
|
38
|
+
type UseEntityListResult,
|
|
38
39
|
} from './useEntityList.js'
|
|
39
40
|
|
|
40
41
|
export {
|
package/src/hooks/useEntity.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
CommonEntity,
|
|
13
13
|
EntityForRoles,
|
|
14
14
|
RoleNames,
|
|
15
|
+
EntityAccessor,
|
|
15
16
|
} from '@contember/bindx'
|
|
16
17
|
import {
|
|
17
18
|
EntityHandle,
|
|
@@ -51,165 +52,62 @@ export interface UseEntityOptions {
|
|
|
51
52
|
// ============================================================================
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
*
|
|
55
|
+
* Common properties on every UseEntityResult state.
|
|
56
|
+
* All prefixed with $ to avoid collision with entity field names.
|
|
55
57
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly isError: false
|
|
60
|
-
readonly isNotFound: false
|
|
61
|
-
readonly isPersisting: false
|
|
62
|
-
readonly isDirty: false
|
|
63
|
-
readonly id: string
|
|
64
|
-
readonly fields: never
|
|
65
|
-
readonly data: never
|
|
66
|
-
persist(): Promise<void>
|
|
67
|
-
reset(): void
|
|
58
|
+
interface UseEntityResultBase {
|
|
59
|
+
$persist(): Promise<void>
|
|
60
|
+
$reset(): void
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
readonly isError: true
|
|
77
|
-
readonly isNotFound: false
|
|
78
|
-
readonly error: FieldError
|
|
79
|
-
readonly isPersisting: false
|
|
80
|
-
readonly isDirty: false
|
|
63
|
+
export type LoadingEntityResult = UseEntityResultBase & {
|
|
64
|
+
readonly $status: 'loading'
|
|
65
|
+
readonly $isLoading: true
|
|
66
|
+
readonly $isError: false
|
|
67
|
+
readonly $isNotFound: false
|
|
68
|
+
readonly $error: null
|
|
81
69
|
readonly id: string
|
|
82
|
-
readonly fields: never
|
|
83
|
-
readonly data: never
|
|
84
|
-
persist(): Promise<void>
|
|
85
|
-
reset(): void
|
|
86
70
|
}
|
|
87
71
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
readonly
|
|
93
|
-
readonly
|
|
94
|
-
readonly isError: false
|
|
95
|
-
readonly isNotFound: true
|
|
96
|
-
readonly isPersisting: false
|
|
97
|
-
readonly isDirty: false
|
|
72
|
+
export type ErrorEntityResult = UseEntityResultBase & {
|
|
73
|
+
readonly $status: 'error'
|
|
74
|
+
readonly $isLoading: false
|
|
75
|
+
readonly $isError: true
|
|
76
|
+
readonly $isNotFound: false
|
|
77
|
+
readonly $error: FieldError
|
|
98
78
|
readonly id: string
|
|
99
|
-
readonly fields: never
|
|
100
|
-
readonly data: never
|
|
101
|
-
persist(): Promise<void>
|
|
102
|
-
reset(): void
|
|
103
79
|
}
|
|
104
80
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
readonly
|
|
110
|
-
readonly
|
|
111
|
-
readonly isError: false
|
|
112
|
-
readonly isNotFound: false
|
|
113
|
-
readonly isPersisting: boolean
|
|
114
|
-
readonly isDirty: boolean
|
|
81
|
+
export type NotFoundEntityResult = UseEntityResultBase & {
|
|
82
|
+
readonly $status: 'not_found'
|
|
83
|
+
readonly $isLoading: false
|
|
84
|
+
readonly $isError: false
|
|
85
|
+
readonly $isNotFound: true
|
|
86
|
+
readonly $error: null
|
|
115
87
|
readonly id: string
|
|
116
|
-
readonly fields: SelectedEntityFields<TEntity, TSelected>
|
|
117
|
-
readonly data: TSelected
|
|
118
|
-
persist(): Promise<void>
|
|
119
|
-
reset(): void
|
|
120
88
|
}
|
|
121
89
|
|
|
122
90
|
/**
|
|
123
|
-
* Ready state
|
|
91
|
+
* Ready state — full EntityAccessor with status metadata.
|
|
124
92
|
*/
|
|
125
|
-
export type
|
|
126
|
-
|
|
93
|
+
export type ReadyEntityResult<TEntity extends object, TSelected extends object = TEntity> =
|
|
94
|
+
UseEntityResultBase & EntityAccessor<TEntity, TSelected> & {
|
|
95
|
+
readonly $status: 'ready'
|
|
96
|
+
readonly $isLoading: false
|
|
97
|
+
readonly $isError: false
|
|
98
|
+
readonly $isNotFound: false
|
|
99
|
+
readonly $error: null
|
|
100
|
+
}
|
|
127
101
|
|
|
128
102
|
/**
|
|
129
|
-
* Union of all
|
|
103
|
+
* Union of all useEntity return states.
|
|
104
|
+
* Discriminated on $status.
|
|
130
105
|
*/
|
|
131
|
-
export type
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
137
|
-
// ============================================================================
|
|
138
|
-
// Internal helpers
|
|
139
|
-
// ============================================================================
|
|
140
|
-
|
|
141
|
-
function createLoadingAccessor(id: string): LoadingEntityAccessor {
|
|
142
|
-
return {
|
|
143
|
-
status: 'loading',
|
|
144
|
-
isLoading: true,
|
|
145
|
-
isError: false,
|
|
146
|
-
isNotFound: false,
|
|
147
|
-
isPersisting: false,
|
|
148
|
-
isDirty: false,
|
|
149
|
-
id,
|
|
150
|
-
get fields(): never {
|
|
151
|
-
throw new Error('Cannot access fields while loading')
|
|
152
|
-
},
|
|
153
|
-
get data(): never {
|
|
154
|
-
throw new Error('Cannot access data while loading')
|
|
155
|
-
},
|
|
156
|
-
async persist() {
|
|
157
|
-
// No-op while loading
|
|
158
|
-
},
|
|
159
|
-
reset() {
|
|
160
|
-
// No-op while loading
|
|
161
|
-
},
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function createErrorAccessor(id: string, error: FieldError): ErrorEntityAccessor {
|
|
166
|
-
return {
|
|
167
|
-
status: 'error',
|
|
168
|
-
isLoading: false,
|
|
169
|
-
isError: true,
|
|
170
|
-
isNotFound: false,
|
|
171
|
-
error,
|
|
172
|
-
isPersisting: false,
|
|
173
|
-
isDirty: false,
|
|
174
|
-
id,
|
|
175
|
-
get fields(): never {
|
|
176
|
-
throw new Error('Cannot access fields after error')
|
|
177
|
-
},
|
|
178
|
-
get data(): never {
|
|
179
|
-
throw new Error('Cannot access data after error')
|
|
180
|
-
},
|
|
181
|
-
async persist() {
|
|
182
|
-
// No-op after error
|
|
183
|
-
},
|
|
184
|
-
reset() {
|
|
185
|
-
// No-op after error
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function createNotFoundAccessor(id: string): NotFoundEntityAccessor {
|
|
191
|
-
return {
|
|
192
|
-
status: 'not_found',
|
|
193
|
-
isLoading: false,
|
|
194
|
-
isError: false,
|
|
195
|
-
isNotFound: true,
|
|
196
|
-
isPersisting: false,
|
|
197
|
-
isDirty: false,
|
|
198
|
-
id,
|
|
199
|
-
get fields(): never {
|
|
200
|
-
throw new Error('Cannot access fields — entity not found')
|
|
201
|
-
},
|
|
202
|
-
get data(): never {
|
|
203
|
-
throw new Error('Cannot access data — entity not found')
|
|
204
|
-
},
|
|
205
|
-
async persist() {
|
|
206
|
-
// No-op for not found
|
|
207
|
-
},
|
|
208
|
-
reset() {
|
|
209
|
-
// No-op for not found
|
|
210
|
-
},
|
|
211
|
-
}
|
|
212
|
-
}
|
|
106
|
+
export type UseEntityResult<TEntity extends object, TSelected extends object = TEntity> =
|
|
107
|
+
| LoadingEntityResult
|
|
108
|
+
| ErrorEntityResult
|
|
109
|
+
| NotFoundEntityResult
|
|
110
|
+
| ReadyEntityResult<TEntity, TSelected>
|
|
213
111
|
|
|
214
112
|
// ============================================================================
|
|
215
113
|
// Hook overloads
|
|
@@ -217,11 +115,6 @@ function createNotFoundAccessor(id: string): NotFoundEntityAccessor {
|
|
|
217
115
|
|
|
218
116
|
/**
|
|
219
117
|
* Hook to fetch and manage a single entity with role-expanded type inference.
|
|
220
|
-
*
|
|
221
|
-
* @example
|
|
222
|
-
* ```tsx
|
|
223
|
-
* const article = useEntity(schema.Article, { by: { id }, roles: ['admin'] }, e => e.title().internalNotes())
|
|
224
|
-
* ```
|
|
225
118
|
*/
|
|
226
119
|
export function useEntity<
|
|
227
120
|
TRoleMap extends Record<string, object>,
|
|
@@ -231,16 +124,15 @@ export function useEntity<
|
|
|
231
124
|
entity: EntityDef<TRoleMap>,
|
|
232
125
|
options: UseEntityOptions & { roles: readonly TRoles[] },
|
|
233
126
|
definer: SelectionInput<EntityForRoles<TRoleMap, TRoles>, TResult>,
|
|
234
|
-
):
|
|
127
|
+
): UseEntityResult<EntityForRoles<TRoleMap, TRoles>, TResult>
|
|
235
128
|
|
|
236
129
|
/**
|
|
237
130
|
* Hook to fetch and manage a single entity with full type inference.
|
|
238
|
-
* Uses the common (narrowest) entity type when no roles are specified.
|
|
239
131
|
*
|
|
240
132
|
* @example
|
|
241
133
|
* ```tsx
|
|
242
134
|
* const article = useEntity(schema.Article, { by: { id } }, e => e.title().content())
|
|
243
|
-
* if (article
|
|
135
|
+
* if (article.$status !== 'ready') return <Loading />
|
|
244
136
|
* return <input value={article.title.value} onChange={...} />
|
|
245
137
|
* ```
|
|
246
138
|
*/
|
|
@@ -248,31 +140,26 @@ export function useEntity<TRoleMap extends Record<string, object>, TResult exten
|
|
|
248
140
|
entity: EntityDef<TRoleMap>,
|
|
249
141
|
options: UseEntityOptions,
|
|
250
142
|
definer: SelectionInput<CommonEntity<TRoleMap>, TResult>,
|
|
251
|
-
):
|
|
143
|
+
): UseEntityResult<CommonEntity<TRoleMap>, TResult>
|
|
252
144
|
|
|
253
145
|
/**
|
|
254
146
|
* Hook to fetch and manage a single entity with pre-resolved selection.
|
|
255
|
-
*
|
|
256
147
|
* Used internally by Entity component that collects selection from JSX.
|
|
257
148
|
*/
|
|
258
149
|
export function useEntity(
|
|
259
150
|
entity: EntityDef,
|
|
260
151
|
options: UseEntityOptions & { selection: SelectionMeta },
|
|
261
|
-
):
|
|
152
|
+
): UseEntityResult<object, object>
|
|
262
153
|
|
|
263
154
|
// ============================================================================
|
|
264
155
|
// Implementation
|
|
265
156
|
// ============================================================================
|
|
266
157
|
|
|
267
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
268
158
|
export function useEntity(
|
|
269
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
270
159
|
entity: EntityDef<any>,
|
|
271
160
|
options: UseEntityOptions,
|
|
272
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
161
|
definer?: SelectionInput<any, any>,
|
|
274
|
-
|
|
275
|
-
): EntityAccessorResult<any, any> {
|
|
162
|
+
): UseEntityResult<any, any> {
|
|
276
163
|
const schemaRegistry = useSchemaRegistry()
|
|
277
164
|
const entityType = entity.$name
|
|
278
165
|
const { store, dispatcher, batcher, batchPersister } = useBindxContext()
|
|
@@ -409,81 +296,85 @@ export function useEntity(
|
|
|
409
296
|
}, [entityType, id, byKey, effectiveQueryKey, options.cache, batcher, store, dispatcher, selectionMeta])
|
|
410
297
|
|
|
411
298
|
// --- EntityHandle ---
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const handle = useMemo(
|
|
415
|
-
() => EntityHandle.create(id, entityType, store, dispatcher, schemaRegistry as SchemaRegistry<Record<string, object>>),
|
|
299
|
+
const rawHandle = useMemo(
|
|
300
|
+
() => EntityHandle.createRaw(id, entityType, store, dispatcher, schemaRegistry as SchemaRegistry<Record<string, object>>),
|
|
416
301
|
[id, entityType, store, dispatcher, schemaRegistry, snapshot],
|
|
417
302
|
)
|
|
418
303
|
|
|
304
|
+
const handle = useMemo(
|
|
305
|
+
() => EntityHandle.wrapProxy(rawHandle),
|
|
306
|
+
[rawHandle],
|
|
307
|
+
)
|
|
308
|
+
|
|
419
309
|
useEffect(() => {
|
|
420
310
|
return () => {
|
|
421
|
-
|
|
311
|
+
rawHandle.dispose()
|
|
422
312
|
}
|
|
423
|
-
}, [
|
|
313
|
+
}, [rawHandle])
|
|
314
|
+
|
|
315
|
+
// --- Persist & reset callbacks ---
|
|
316
|
+
const persist = useCallback(async () => {
|
|
317
|
+
await batchPersister.persist(entityType, id)
|
|
318
|
+
}, [batchPersister, entityType, id])
|
|
424
319
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
320
|
+
const reset = useCallback(() => {
|
|
321
|
+
rawHandle.reset()
|
|
322
|
+
}, [rawHandle])
|
|
323
|
+
|
|
324
|
+
// --- Build result ---
|
|
325
|
+
const result = useMemo((): UseEntityResult<object, object> => {
|
|
326
|
+
if (!loadState || loadState.status === 'loading' || (!snapshot && loadState.status === 'success')) {
|
|
327
|
+
return { $status: 'loading', $isLoading: true, $isError: false, $isNotFound: false, $error: null, id, $persist: persist, $reset: reset }
|
|
430
328
|
}
|
|
431
329
|
|
|
432
330
|
if (loadState.status === 'error') {
|
|
433
|
-
return
|
|
331
|
+
return { $status: 'error', $isLoading: false, $isError: true, $isNotFound: false, $error: loadState.error!, id, $persist: persist, $reset: reset }
|
|
434
332
|
}
|
|
435
333
|
|
|
436
334
|
if (loadState.status === 'not_found') {
|
|
437
|
-
return
|
|
335
|
+
return { $status: 'not_found', $isLoading: false, $isError: false, $isNotFound: true, $error: null, id, $persist: persist, $reset: reset }
|
|
438
336
|
}
|
|
439
337
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
338
|
+
// Ready — layer status metadata on top of EntityHandle proxy
|
|
339
|
+
return createReadyResult(handle, persist, reset)
|
|
340
|
+
}, [snapshot, loadState, isPersisting, id, handle, persist, reset])
|
|
443
341
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
448
|
-
const baseAccessor: ReadyEntityAccessorBase<any, any> = {
|
|
449
|
-
status: 'ready',
|
|
450
|
-
isLoading: false,
|
|
451
|
-
isError: false,
|
|
452
|
-
isNotFound: false,
|
|
453
|
-
isPersisting,
|
|
454
|
-
get isDirty() {
|
|
455
|
-
return handle.$isDirty
|
|
456
|
-
},
|
|
457
|
-
id: realId,
|
|
458
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
459
|
-
fields: handle.$fields as any,
|
|
460
|
-
data: snapshot.data,
|
|
461
|
-
async persist() {
|
|
462
|
-
await batchPersister.persist(entityType, realId)
|
|
463
|
-
},
|
|
464
|
-
reset() {
|
|
465
|
-
handle.reset()
|
|
466
|
-
},
|
|
467
|
-
}
|
|
342
|
+
// Proxy-based result satisfies the full UseEntityResult<T> at runtime via field access delegation
|
|
343
|
+
return result as UseEntityResult<any, any>
|
|
344
|
+
}
|
|
468
345
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Result factories
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
function createReadyResult(
|
|
351
|
+
handle: EntityAccessor<object, object>,
|
|
352
|
+
persist: () => Promise<void>,
|
|
353
|
+
reset: () => void,
|
|
354
|
+
): UseEntityResult<object, object> {
|
|
355
|
+
const meta: Record<string, unknown> = {
|
|
356
|
+
$status: 'ready' as const,
|
|
357
|
+
$isLoading: false as const,
|
|
358
|
+
$isError: false as const,
|
|
359
|
+
$isNotFound: false as const,
|
|
360
|
+
$error: null,
|
|
361
|
+
$persist: persist,
|
|
362
|
+
$reset: reset,
|
|
363
|
+
}
|
|
487
364
|
|
|
488
|
-
|
|
365
|
+
// Proxy merges status metadata onto EntityAccessor — satisfies ReadyEntityResult at runtime
|
|
366
|
+
return new Proxy(handle as object, {
|
|
367
|
+
get(target, prop, receiver) {
|
|
368
|
+
if (typeof prop === 'string' && prop in meta) {
|
|
369
|
+
return meta[prop]
|
|
370
|
+
}
|
|
371
|
+
return Reflect.get(target, prop, receiver)
|
|
372
|
+
},
|
|
373
|
+
has(target, prop) {
|
|
374
|
+
if (typeof prop === 'string' && prop in meta) {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
return Reflect.has(target, prop)
|
|
378
|
+
},
|
|
379
|
+
}) as unknown as UseEntityResult<object, object>
|
|
489
380
|
}
|