@collective-intelligence/external-apps-client-sdk-react 0.1.35 → 0.1.37
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/README.md +112 -22
- package/dist/effect.d.ts +60 -0
- package/dist/effect.js +2 -0
- package/dist/index-6ra894v2.js +2 -0
- package/dist/index-bjd4xpqf.js +2 -0
- package/dist/index.d.ts +104 -925
- package/dist/index.js +1 -1
- package/dist/resources.d.ts +79 -337
- package/dist/resources.js +1 -1
- package/package.json +20 -6
- package/dist/index-m8zsc3k7.js +0 -49
package/README.md
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
UI-minimal React bindings over the Promise Client SDK. The package has no private application UI dependencies.
|
|
4
4
|
|
|
5
|
-
`CollectiveIntelligenceProvider` owns a stable, request-isolated QueryClient
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
`CollectiveIntelligenceProvider` owns a stable, request-isolated QueryClient; applications do not
|
|
6
|
+
configure a QueryClient for SDK CRUD. To use one cache for the SDK hooks and the application, pass
|
|
7
|
+
the application's client as `queryClient`. Then the defaults of that client apply to the `/resources`
|
|
8
|
+
reads, and the application isolates requests on the server. Entity hooks keep `retry: false`.
|
|
9
|
+
|
|
10
|
+
Install these required peer dependencies in the application: `react`, `react-dom`,
|
|
11
|
+
`@tanstack/react-query`, `xstate`, `effect` and `@collective-intelligence/ui-react`. The package
|
|
12
|
+
depends on `@collective-intelligence/external-apps-client-sdk` as a normal dependency. The packed
|
|
13
|
+
JavaScript bundles none of these packages, so the application holds one copy of each. The `./effect` entry
|
|
14
|
+
holds the [Effect bridge](#effect-bridge) for XState machines.
|
|
8
15
|
|
|
9
16
|
Configure the provider's `entitySource` once, alongside its existing Promise `client`. `EntitySource`
|
|
10
17
|
exposes a stable `getSnapshot`, a `subscribe` function, and a typed `bind(contract)` function returning
|
|
@@ -48,6 +55,13 @@ revisions. Form unmount does not cancel a dispatched write. Source transitions c
|
|
|
48
55
|
and reject stale queued actions before dispatch. Queries and mutations never automatically retry;
|
|
49
56
|
there is no persistence, reload recovery, or automatic revision rebasing.
|
|
50
57
|
|
|
58
|
+
Entity reads and writes run on the Effect Core of the client SDK, through the entity clients that
|
|
59
|
+
the entity source binds. Their errors stay `ResourceResult` values: a read that fails gives
|
|
60
|
+
`error`, a `ResourceQueryError` with the resource error; a write that fails in flight is
|
|
61
|
+
`uncertain`. Other client SDK calls follow the client SDK error model: a `CiResult` operation
|
|
62
|
+
resolves `{ ok: false, error }` with a `CiError`, and rejects with `CiTransportError` when the
|
|
63
|
+
request does not reach CI. `useAgentInvocation` shows such a rejection as its `error` state.
|
|
64
|
+
|
|
51
65
|
The old public TanStack option factories and native-result resource hooks are no longer exported.
|
|
52
66
|
Use the deliberate entity hooks above. Existing generic slotted `Resource*` presentation remains
|
|
53
67
|
separate from common entity CRUD.
|
|
@@ -116,8 +130,14 @@ Drafts and operation keys never enter the URL.
|
|
|
116
130
|
|
|
117
131
|
## Realtime topics
|
|
118
132
|
|
|
119
|
-
`RealtimeProvider`
|
|
120
|
-
connection,
|
|
133
|
+
`RealtimeProvider` takes the options of `createRealtimeClient` as props. It
|
|
134
|
+
creates one client, and so one connection, for its tree, and closes it when the
|
|
135
|
+
tree unmounts. Its effects and the hooks below it hold the client by reference
|
|
136
|
+
count. Thus a Strict Mode double mount opens one stream, and a server render
|
|
137
|
+
creates no client. Without a `document` prop the client follows the page of
|
|
138
|
+
the browser. The client reads the other props, such as `ticketUrl` and
|
|
139
|
+
`fetch`, once, when the provider creates it; a later change has no effect. To
|
|
140
|
+
use new values, remount the provider, for example with a React `key`.
|
|
121
141
|
`useRealtimeTopic(topic, onChanges)` subscribes while the component is mounted
|
|
122
142
|
and unsubscribes on unmount. A new topic object for the same topic keeps the
|
|
123
143
|
subscription; `null` subscribes to nothing. The latest `onChanges` is called
|
|
@@ -125,22 +145,29 @@ without resubscribing. It receives the changes of the topic in batches: one
|
|
|
125
145
|
delivery per tab, 250 to 1000 ms after the first pending change, with the
|
|
126
146
|
latest change of each record.
|
|
127
147
|
|
|
128
|
-
Pass `onReset`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
Pass `onReset` and refetch every realtime-driven query in it. The client calls
|
|
149
|
+
it when the server cannot replay a gap and once after a stream opens without a
|
|
150
|
+
last event id, the first one of each page included. Without `onReset` the
|
|
151
|
+
client cannot recover from such a gap, so a change can be lost. The client
|
|
152
|
+
calls `onAccessChanged` when the user's access changes. Both stay callbacks of
|
|
153
|
+
the application; the client calls the ones of the latest render, also when a
|
|
154
|
+
render adds or removes one. Only a client with `onReset` resyncs, so a render
|
|
155
|
+
that adds or removes `onReset` replaces the client and its stream. The new
|
|
156
|
+
stream opens without a last event id, so with `onReset` it resyncs once.
|
|
133
157
|
|
|
134
158
|
```tsx
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
const AppRealtime = ({ children }: { children: ReactNode }) => {
|
|
160
|
+
const queryClient = useQueryClient()
|
|
161
|
+
return (
|
|
162
|
+
<RealtimeProvider
|
|
163
|
+
ticketUrl="/api/realtime/ticket"
|
|
164
|
+
onReset={() => queryClient.invalidateQueries()}
|
|
165
|
+
onAccessChanged={() => queryClient.invalidateQueries()}
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
</RealtimeProvider>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
144
171
|
|
|
145
172
|
const LeadBoard = () => {
|
|
146
173
|
const queryClient = useQueryClient()
|
|
@@ -152,6 +179,68 @@ const LeadBoard = () => {
|
|
|
152
179
|
}
|
|
153
180
|
```
|
|
154
181
|
|
|
182
|
+
## Effect bridge
|
|
183
|
+
|
|
184
|
+
`./effect` connects XState machines to Effect and to the application's
|
|
185
|
+
QueryClient. It has no Effect Atom bindings.
|
|
186
|
+
|
|
187
|
+
- `fromEffect(run)` makes actor logic from an Effect. The actor runs the effect
|
|
188
|
+
on `input.runtime`, so annotate `input` with a `runtime: EffectRunner<R>`
|
|
189
|
+
that provides every service of the effect. The actor's output is the
|
|
190
|
+
effect's value; on a failure, `onError` gets the typed error or the defect.
|
|
191
|
+
A stop of the actor interrupts the fiber, and the run ends. After the stop,
|
|
192
|
+
no output, error or interruption of that run gets to the actor, also when
|
|
193
|
+
React Strict Mode starts the actor of `useActorRef` again.
|
|
194
|
+
- `EffectRuntimeProvider` gives its tree a runtime with `QueryCache` over the
|
|
195
|
+
nearest QueryClient; `useEffectRuntime()` returns it. Put it inside
|
|
196
|
+
`CollectiveIntelligenceProvider`. The runtime holds no resource, so the
|
|
197
|
+
provider never disposes it.
|
|
198
|
+
- `QueryCache` is the one way for machine actors to read and write remote data;
|
|
199
|
+
machine context holds none. `fetch({ queryKey, query, staleTime? })` returns
|
|
200
|
+
a fresh cached value or runs `query` once, with no automatic retry, and caches
|
|
201
|
+
its value. `CiError` and `CiTransportError` stay typed errors; another failure
|
|
202
|
+
is a defect. While a `fetch` waits, it keeps the key in use, as a mounted
|
|
203
|
+
view does. When the last view or `fetch` of the key stops waiting, for
|
|
204
|
+
example because its actor stops, TanStack Query cancels the fetch and
|
|
205
|
+
`query` is interrupted. Thus in React Strict Mode, the actor that
|
|
206
|
+
`useActorRef` starts again can run `query` a second time. A cancel of the
|
|
207
|
+
key, for example `queryClient.cancelQueries({ queryKey })`, also interrupts
|
|
208
|
+
`query`. A `fetch` that waits then returns the value from before the fetch,
|
|
209
|
+
or is interrupted when the key has no value. `invalidate(queryKey)` marks
|
|
210
|
+
values stale and refetches the ones that views show.
|
|
211
|
+
`setData(queryKey, value)` writes a value that views show at once. Views
|
|
212
|
+
read the same keys with `useQuery`.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import type { EffectRunner } from "@collective-intelligence/external-apps-client-sdk-react/effect"
|
|
216
|
+
import {
|
|
217
|
+
EffectRuntimeProvider,
|
|
218
|
+
QueryCache,
|
|
219
|
+
fromEffect,
|
|
220
|
+
useEffectRuntime
|
|
221
|
+
} from "@collective-intelligence/external-apps-client-sdk-react/effect"
|
|
222
|
+
|
|
223
|
+
const loadLead = fromEffect(
|
|
224
|
+
(input: Readonly<{ id: string; runtime: EffectRunner<QueryCache> }>) =>
|
|
225
|
+
QueryCache.use((cache) =>
|
|
226
|
+
cache.fetch({ queryKey: ["crm", "lead", input.id], query: readLead(input.id) })
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
<CollectiveIntelligenceProvider client={appSdk} queryClient={queryClient}>
|
|
231
|
+
<EffectRuntimeProvider>
|
|
232
|
+
<LeadEditor id={leadId} />
|
|
233
|
+
</EffectRuntimeProvider>
|
|
234
|
+
</CollectiveIntelligenceProvider>
|
|
235
|
+
|
|
236
|
+
const LeadEditor = ({ id }: { id: string }) => {
|
|
237
|
+
const runtime = useEffectRuntime()
|
|
238
|
+
// `leadMachine` invokes `loadLead` with `{ id, runtime }` from its input.
|
|
239
|
+
const lead = useActorRef(leadMachine, { input: { id, runtime } })
|
|
240
|
+
// …
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
155
244
|
## Workspace development
|
|
156
245
|
|
|
157
246
|
See the repository's [Effect demo](../../../demo/external-app-effect/README.md)
|
|
@@ -164,9 +253,10 @@ are consumed without an intermediate rebuild. Release builds disable custom cond
|
|
|
164
253
|
consumers resolve generated `dist` JavaScript and declarations. Shared runtime chunks keep provider
|
|
165
254
|
contexts identical across the root and `/resources` entrypoints.
|
|
166
255
|
|
|
167
|
-
Run `
|
|
168
|
-
executes a real root-provider/resources-hook consumer without caller TanStack setup and
|
|
169
|
-
temporary installation on success
|
|
256
|
+
Run `bun run test:unit`, `bun run type-check`, `bun run lint`, and `bun run test:pack`. The pack check
|
|
257
|
+
executes a real root-provider/resources-hook consumer without caller TanStack setup, and a `./effect`
|
|
258
|
+
consumer that shares the application's QueryClient. It removes its temporary installation on success
|
|
259
|
+
or failure. The repository root runs `bun run lint:effect` for the Effect rules.
|
|
170
260
|
|
|
171
261
|
## Enum value compatibility
|
|
172
262
|
|
package/dist/effect.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
import { CiError, CiTransportError } from '@collective-intelligence/external-apps-client-sdk/effect';
|
|
4
|
+
import { QueryKey } from '@tanstack/react-query';
|
|
5
|
+
import * as Context from 'effect/Context';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Layer from 'effect/Layer';
|
|
8
|
+
import * as ManagedRuntime from 'effect/ManagedRuntime';
|
|
9
|
+
import { ReactNode } from 'react';
|
|
10
|
+
import { PromiseActorLogic } from 'xstate';
|
|
11
|
+
|
|
12
|
+
/** What runs an effect for an actor: a `ManagedRuntime` that provides `R`. */
|
|
13
|
+
export type EffectRunner<R> = Pick<ManagedRuntime.ManagedRuntime<R, never>, "runPromise">;
|
|
14
|
+
/**
|
|
15
|
+
* XState actor logic that runs the effect of `run` on the runtime of the actor
|
|
16
|
+
* input. The actor's output is the effect's value; a failure or a defect fails
|
|
17
|
+
* the actor. A stop of the actor interrupts the fiber, so its run settles.
|
|
18
|
+
* After the stop, no result of that run gets to the actor.
|
|
19
|
+
*/
|
|
20
|
+
export declare const fromEffect: <A, E, R, I extends {
|
|
21
|
+
readonly runtime: EffectRunner<R>;
|
|
22
|
+
}>(run: (input: I) => Effect.Effect<A, E, R>) => PromiseActorLogic<A, I>;
|
|
23
|
+
export type QueryCacheFetchOptions<Data> = Readonly<{
|
|
24
|
+
queryKey: QueryKey;
|
|
25
|
+
/** Runs when the cache holds no fresh value for `queryKey`. */
|
|
26
|
+
query: Effect.Effect<Data, CiError | CiTransportError>;
|
|
27
|
+
/** Milliseconds for which a cached value stays fresh; without it, the QueryClient default applies. */
|
|
28
|
+
staleTime?: number;
|
|
29
|
+
}>;
|
|
30
|
+
declare const QueryCache_base: Context.ServiceClass<QueryCache, "@collective-intelligence/external-apps-client-sdk-react/QueryCache", Readonly<{
|
|
31
|
+
/**
|
|
32
|
+
* The fresh cached value of `queryKey`, or the value of one run of `query`,
|
|
33
|
+
* then cached. When the last view or caller of the key stops waiting, or
|
|
34
|
+
* TanStack Query cancels the key, `query` is interrupted.
|
|
35
|
+
*/
|
|
36
|
+
fetch: <Data>(options: QueryCacheFetchOptions<Data>) => Effect.Effect<Data, CiError | CiTransportError>;
|
|
37
|
+
/** Marks the values of `queryKey` and of the keys below it stale, and refetches the ones that views show. */
|
|
38
|
+
invalidate: (queryKey: QueryKey) => Effect.Effect<void>;
|
|
39
|
+
/** Caches `data` as the value of `queryKey`; views show it at once. */
|
|
40
|
+
setData: <Data>(queryKey: QueryKey, data: Data) => Effect.Effect<void>;
|
|
41
|
+
}>>;
|
|
42
|
+
/**
|
|
43
|
+
* The remote data of the app's `QueryClient`, as machine actors read and
|
|
44
|
+
* write it. Views read the same entries with `useQuery`.
|
|
45
|
+
*/
|
|
46
|
+
export declare class QueryCache extends QueryCache_base {
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gives its tree an Effect runtime with the `QueryCache` over the nearest
|
|
50
|
+
* `QueryClient`. A view passes it to the machines it starts, in their input.
|
|
51
|
+
* The runtime holds no resource, so the provider never disposes it: a Strict
|
|
52
|
+
* Mode remount and a machine that keeps it stay valid.
|
|
53
|
+
*/
|
|
54
|
+
export declare const EffectRuntimeProvider: ({ children }: Readonly<{
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}>) => import("react").JSX.Element;
|
|
57
|
+
/** The runtime of the nearest `EffectRuntimeProvider`. */
|
|
58
|
+
export declare const useEffectRuntime: () => EffectRunner<QueryCache>;
|
|
59
|
+
|
|
60
|
+
export {};
|
package/dist/effect.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{__export}from"./index-6ra894v2.js";var exports_effect={};__export(exports_effect,{EffectRuntimeProvider:()=>EffectRuntimeProvider,QueryCache:()=>QueryCache,fromEffect:()=>fromEffect,useEffectRuntime:()=>useEffectRuntime});import{fromPromise}from"xstate";var unsettled=()=>Promise.race([]);var fromEffect=(run)=>fromPromise(async({input,signal})=>{try{const value=await input.runtime.runPromise(run(input),{signal});return signal.aborted?unsettled():value}catch(error){if(signal.aborted){return unsettled()}throw error}});import{CiError,CiTransportError}from"@collective-intelligence/external-apps-client-sdk/effect";import{isCancelledError,QueryObserver}from"@tanstack/react-query";import*as Context from"effect/Context";import*as Effect from"effect/Effect";import*as Exit from"effect/Exit";import{constVoid}from"effect/Function";import*as Layer from"effect/Layer";class QueryCache extends Context.Service()("@collective-intelligence/external-apps-client-sdk-react/QueryCache"){}var make=(queryClient)=>{const fetchExit=async({query,...options},context)=>{try{return Exit.succeed(await queryClient.fetchQuery({...options,queryFn:({signal})=>Effect.runPromiseWith(context)(query,{signal}),retry:false}))}catch(error){if(isCancelledError(error)){return Exit.interrupt()}return error instanceof CiError||error instanceof CiTransportError?Exit.fail(error):Exit.die(error)}};const waitOn=(queryKey)=>Effect.sync(()=>new QueryObserver(queryClient,{enabled:false,queryKey}).subscribe(constVoid));return QueryCache.of({fetch:(options)=>Effect.flatMap(Effect.context(),(context)=>Effect.acquireUseRelease(waitOn(options.queryKey),()=>Effect.flatten(Effect.promise(()=>fetchExit(options,context))),(stopWaiting)=>Effect.sync(stopWaiting))),invalidate:(queryKey)=>Effect.promise(()=>queryClient.invalidateQueries({queryKey})),setData:(queryKey,data)=>Effect.sync(()=>{queryClient.setQueryData(queryKey,data)})})};var layer=(queryClient)=>Layer.succeed(QueryCache,make(queryClient));import{useQueryClient}from"@tanstack/react-query";import*as ManagedRuntime from"effect/ManagedRuntime";import{createContext,useContext,useMemo}from"react";import{jsx}from"react/jsx-runtime";var RuntimeContext=createContext(null);var EffectRuntimeProvider=({children})=>{const queryClient=useQueryClient();const runtime=useMemo(()=>ManagedRuntime.make(layer(queryClient)),[queryClient]);return jsx(RuntimeContext.Provider,{value:runtime,children})};var useEffectRuntime=()=>{const runtime=useContext(RuntimeContext);if(runtime===null){throw new Error("useEffectRuntime requires an EffectRuntimeProvider")}return runtime};"use client";"use client";var{EffectRuntimeProvider:EffectRuntimeProvider2,QueryCache:QueryCache2,fromEffect:fromEffect2,useEffectRuntime:useEffectRuntime2}=exports_effect;export{EffectRuntimeProvider2 as EffectRuntimeProvider,QueryCache2 as QueryCache,fromEffect2 as fromEffect,useEffectRuntime2 as useEffectRuntime};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var __defProp=Object.defineProperty;var __returnValue=(v)=>v;function __exportSetter(name,newValue){this[name]=__returnValue.bind(null,newValue)}var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true,configurable:true,set:__exportSetter.bind(all,name)})};
|
|
2
|
+
export{__export};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createEntityHttpTransport,createResourceClient}from"@collective-intelligence/external-apps-client-sdk";import{useQueryClient}from"@tanstack/react-query";import{createContext,useContext,useLayoutEffect,useMemo,useSyncExternalStore}from"react";import{queryOptions}from"@tanstack/react-query";var rootKey=["ci-resources"];var resourceKeys={all:rootKey,list:(organizationId,resourceId,version,query)=>[...resourceKeys.lists(organizationId,resourceId,version),query],lists:(organizationId,resourceId,version)=>[...resourceKeys.resource(organizationId,resourceId,version),"lists"],organization:(organizationId)=>[...rootKey,"organizations",organizationId],record:(organizationId,resourceId,version,recordId,includeDeleted=false)=>[...resourceKeys.records(organizationId,resourceId,version),recordId,includeDeleted],records:(organizationId,resourceId,version)=>[...resourceKeys.resource(organizationId,resourceId,version),"records"],resource:(organizationId,resourceId,version)=>[...resourceKeys.organization(organizationId),"resources",resourceId,"versions",version]};class ResourceQueryError2 extends Error{correlationId;resourceError;constructor(correlationId,error){super(error.message);this.name="ResourceQueryError";this.correlationId=correlationId;this.resourceError=error}}var unwrapResourceResult2=(result)=>{if(!result.ok){throw new ResourceQueryError2(result.correlationId,result.error)}return result.value};var createResourceListQueryOptions=(options)=>queryOptions({queryFn:async({signal})=>unwrapResourceResult2(await options.client.list(options.query,{signal})),queryKey:resourceKeys.list(options.organizationId,options.client.definition.id,options.client.definition.version,options.query)});var mapResourceAuthState2=(source)=>{if("ok"in source){if(source.ok){return{status:"authenticated"}}if(source.error.code==="forbidden"){return{status:"forbidden"}}if(source.error.code==="unauthenticated"){return{status:"unauthenticated"}}return{status:"authenticated"}}if(source.status==="authenticated"){return{status:"authenticated"}}if(source.status==="expired"){return{status:"expired"}}if(source.status==="error"&&source.error.code==="reauthorization_required"){return{status:"reauthentication_required"}}return{status:"unauthenticated"}};var hasResourceCapability2=(capabilities,capability)=>capabilities[capability]===true;var clearProtectedResourceQueries=async(queryClient,organizationId)=>{const queryKey=organizationId===undefined?resourceKeys.all:resourceKeys.organization(organizationId);await queryClient.cancelQueries({queryKey});queryClient.removeQueries({queryKey})};import{jsx}from"react/jsx-runtime";var entityScopeError=(outcome="not_dispatched")=>new ResourceQueryError2("client-scope",{code:"unauthenticated",message:"The authenticated entity source has changed",outcome,retryable:false});var identityKey=(state)=>state.status==="authenticated"?[state.identity.sourceId,state.identity.applicationId,state.identity.userId,state.identity.organizationId]:[state.status];var EntityContext=createContext(null);var createRuntime=(source,state)=>{let active=true;return{activate:()=>{active=true},current:()=>active&&source.getSnapshot()===state&&state.status==="authenticated",deactivate:()=>{active=false},formScope:globalThis.crypto.randomUUID(),key:identityKey(state),source,state}};var EntityRuntimeProvider=({source,children})=>{const queryClient=useQueryClient();const state=useSyncExternalStore(source.subscribe,source.getSnapshot,source.getSnapshot);const runtime=useMemo(()=>createRuntime(source,state),[source,state]);useLayoutEffect(()=>{runtime.activate();return()=>{runtime.deactivate();queryClient.cancelQueries({queryKey:["ci-entities",...runtime.key]});queryClient.removeQueries({queryKey:["ci-entities",...runtime.key]})}},[queryClient,runtime]);return jsx(EntityContext.Provider,{value:runtime,children})};var useEntityRuntime=()=>{const context=useContext(EntityContext);if(context===null){throw new Error("Configure an entity source on CollectiveIntelligenceProvider")}return context};var defaultPathFor=(contract)=>`/api/resources/${contract.id}`;var createDelegatedEntitySource=(options)=>{const fetch=options.fetch??globalThis.fetch;const pathFor=options.pathFor??defaultPathFor;const sourceId=options.sourceId??`${options.origin}/api/resources`;let cached;const snapshot=()=>{const identity=options.getIdentity();const status=options.getStatus();const next=status==="authenticated"&&identity!==null?{identity:{applicationId:options.applicationId,organizationId:identity.organizationId,sourceId,userId:identity.userId},status:"authenticated"}:{status:status==="expired"?"expired":"unauthenticated"};if(cached!==undefined&&identityKey(cached).join("\x00")===identityKey(next).join("\x00")){return cached}cached=next;return next};return{bind:(contract)=>createResourceClient({definition:contract,protocol:"entity",transport:createEntityHttpTransport({authentication:{credentials:"same-origin",mode:"server_delegated"},destination:{endpoint:new URL(pathFor(contract),options.origin).href,kind:"gateway"},fetch})}),getSnapshot:snapshot,subscribe:options.subscribe}};var assertConfiguredFields=(config)=>{const declared=new Set(Object.keys(config.fields));const referenced=[...config.table.columns,...Object.values(config.table.variants??{}).flatMap((variant)=>variant.columns),config.list.title,...config.list.description===undefined?[]:[config.list.description],...config.forms.create.fields,...config.forms.update.fields];const unknown=referenced.find((field)=>!declared.has(field));if(unknown!==undefined){throw new Error(`Entity view references unconfigured field: ${unknown}`)}};var defineEntity2=(declaration)=>{assertConfiguredFields(declaration.config);const schemaFields=new Set(Object.keys(declaration.contract.schemas.entity.properties??{}));const unknown=Object.keys(declaration.config.fields).find((field)=>!schemaFields.has(field));if(unknown!==undefined){throw new Error(`Configured field is absent from the entity schema: ${unknown}`)}return Object.freeze(declaration)};import{ResourceFilterPrimitive,ResourceFiltersBar,ResourceFormPrimitive,ResourceListPrimitive,ResourcePaginationPrimitive,ResourceTablePrimitive}from"@collective-intelligence/ui-react/resources";import{useMemo as useMemo5,useState as useState2}from"react";import{useIsMutating,useMutation,useQueryClient as useQueryClient2}from"@tanstack/react-query";import{useMemo as useMemo2,useRef}from"react";var immutable=(value)=>{if(value!==null&&typeof value==="object"){for(const child of Object.values(value)){immutable(child)}Object.freeze(value)}return value};var useEntityMutation=(contract,options)=>{const runtime=useEntityRuntime();const queryClient=useQueryClient2();const client=useMemo2(()=>runtime.source.bind(contract),[runtime,contract]);const resourceKey=["ci-entities",...runtime.key,contract.id,contract.version];const mutationKey=[...resourceKey,"write",options.id??"create"];const lastAction=useRef(null);const scopeOptions={};if(options.id!==undefined){scopeOptions.scope={id:JSON.stringify(mutationKey)}}const mutation=useMutation({mutationKey,...scopeOptions,mutationFn:async(action)=>{if(!action.current()){throw entityScopeError()}const result=await action.execute();if(!action.current()){throw entityScopeError("uncertain")}if(!result.ok){throw new ResourceQueryError2(result.correlationId,result.error)}return result},networkMode:"always",onSuccess:async(_data,action)=>{if(action.current()){await queryClient.invalidateQueries({queryKey:resourceKey})}},retry:false});const pending=useIsMutating({mutationKey})>0;const send=async(action)=>{try{const result=await mutation.mutateAsync(action);if(!action.current()){const error=entityScopeError("uncertain");return{correlationId:error.correlationId,error:error.resourceError,ok:false}}return result}catch(error){if(error instanceof ResourceQueryError2){return{correlationId:error.correlationId,error:error.resourceError,ok:false}}throw error}};const submit=(input)=>{const captured=immutable(structuredClone(options.prepare(input)));const action={current:runtime.current,execute:()=>options.execute(client,captured),input:captured,runtime,target:options.id};lastAction.current=action;return send(action)};const resubmit=()=>{const action=lastAction.current;if(action===null){throw new Error("No submitted entity action to resubmit")}if(action.target!==options.id){throw new Error("The submitted action belongs to a different record")}return send(action)};const current=mutation.variables===undefined||mutation.variables.runtime===runtime&&mutation.variables.target===options.id&&mutation.variables.current();const error=current&&mutation.error instanceof ResourceQueryError2?mutation.error:null;const failureStatus=()=>{if(error?.resourceError.outcome==="uncertain"){return"uncertain"}if(error?.resourceError.code==="conflict"){return"conflict"}return"rejected"};const status=mutation.isError?failureStatus():mutation.status;return{available:runtime.state.status==="authenticated"&&client.definition.capabilities[options.operation]===true,data:current?mutation.data?.value:undefined,error,pending,resubmit,status:current?status:"idle",submit,submitted:current?mutation.variables?.input:undefined}};var useEntityUpdate2=(contract,target)=>useEntityMutation(contract,{execute:(client,input)=>client.update(input),id:target.id,operation:"update",prepare:(input)=>({...input,id:target.id,idempotencyKey:globalThis.crypto.randomUUID()})});var useEntityCreate2=(contract)=>useEntityMutation(contract,{execute:(client,input)=>client.create(input),operation:"create",prepare:(input)=>({...input,idempotencyKey:globalThis.crypto.randomUUID()})});var useEntityDelete2=(contract,target)=>useEntityMutation(contract,{execute:(client,input)=>client.delete(input),id:target.id,operation:"delete",prepare:(input)=>({...input,id:target.id,idempotencyKey:globalThis.crypto.randomUUID()})});import{useQuery}from"@tanstack/react-query";import{useMemo as useMemo3}from"react";var authStateForError=(error,state)=>{if(error instanceof ResourceQueryError2){if(error.resourceError.code==="forbidden"){return{status:"forbidden"}}if(error.resourceError.code==="unauthenticated"){return{status:"unauthenticated"}}}return state};var useEntityList2=(contract,query)=>{const runtime=useEntityRuntime();const client=useMemo3(()=>runtime.source.bind(contract),[runtime,contract]);const result=useQuery({enabled:runtime.state.status==="authenticated"&&client.definition.capabilities.search,queryFn:async({signal})=>{if(!runtime.current()){throw entityScopeError()}const data=unwrapResourceResult2(await client.list(query,{signal}));if(!runtime.current()){throw entityScopeError()}return data},queryKey:["ci-entities",...runtime.key,contract.id,contract.version,"list",query],retry:false});return{authState:authStateForError(result.error,runtime.state),capabilities:client.definition.capabilities,data:result.data,empty:result.data?.items.length===0,error:result.error,loading:result.isLoading,refresh:async()=>{await result.refetch()},refreshing:result.isFetching}};var useEntityRecord2=(contract,input)=>{const runtime=useEntityRuntime();const client=useMemo3(()=>runtime.source.bind(contract),[runtime,contract]);const result=useQuery({enabled:runtime.state.status==="authenticated"&&client.definition.capabilities.get===true,queryFn:async({signal})=>{if(!runtime.current()){throw entityScopeError()}const data=unwrapResourceResult2(await client.get(input,{signal}));if(!runtime.current()){throw entityScopeError()}return data},queryKey:["ci-entities",...runtime.key,contract.id,contract.version,"record",input.id],retry:false});return{authState:authStateForError(result.error,runtime.state),capabilities:client.definition.capabilities,data:result.data,error:result.error,loading:result.isLoading,refresh:async()=>{await result.refetch()},refreshing:result.isFetching}};import{createEntityProtocol}from"@collective-intelligence/external-apps-client-sdk/resources";import{createParser,parseAsJson,useQueryStates}from"nuqs";import{useMemo as useMemo4,useState}from"react";var DEFAULT_PAGE_SIZE=25;var namespacePattern=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;var isObject=(value)=>typeof value==="object"&&value!==null&&!Array.isArray(value);var isFilter=(value)=>{if(!isObject(value)){return false}if((value.operator==="and"||value.operator==="or")&&"filters"in value){return Array.isArray(value.filters)&&value.filters.every(isFilter)}if(value.operator==="not"&&"filter"in value){return isFilter(value.filter)}return typeof value.field==="string"&&typeof value.operator==="string"&&(value.operator==="is_null"||value.operator==="is_not_null"||value.operator==="is_empty"||("value"in value))};var isSorts=(value)=>Array.isArray(value)&&value.every((sort)=>isObject(sort)&&typeof sort.field==="string"&&(sort.direction==="asc"||sort.direction==="desc"));var positiveInteger=(maximum=Number.MAX_SAFE_INTEGER)=>createParser({parse:(value)=>{const number=Number(value);return/^[1-9]\d*$/u.test(value)&&Number.isSafeInteger(number)&&number<=maximum?number:null},serialize:String});var pageSizeFor=(value=DEFAULT_PAGE_SIZE)=>Number.isSafeInteger(value)&&value>0&&value<=100?value:DEFAULT_PAGE_SIZE;var parsersFor=(pageSize,contract)=>{const protocol=contract===undefined?undefined:createEntityProtocol(contract);const accepts=(query)=>protocol===undefined||protocol.decodeRequest({operation:"list",query:{pagination:{mode:"page",page:1,pageSize},...query},requestId:"view-state",resourceId:contract?.id}).ok;return{filter:parseAsJson((value)=>isFilter(value)&&accepts({filter:value})?value:null),page:positiveInteger().withDefault(1),pageSize:positiveInteger(100).withDefault(pageSize),sort:parseAsJson((value)=>isSorts(value)&&accepts({sort:value})?value:null).withDefault([]),textSearch:createParser({parse:(value)=>accepts({textSearch:value})?value:null,serialize:(value)=>value}).withDefault("")}};var queryFor=(state)=>{const query={pagination:{mode:"page",page:state.page,pageSize:state.pageSize}};if(state.filter!==null){query.filter=state.filter}if(state.sort.length>0){query.sort=state.sort}if(state.textSearch.trim()!==""){query.textSearch=state.textSearch.trim()}return query};var controller=(state,update)=>({query:queryFor(state),setFilter:(filter)=>update({filter,page:1}),setPage:(page)=>update({page:Math.max(1,page)}),setPageSize:(pageSize)=>update({page:1,pageSize:Math.max(1,Math.min(100,pageSize))}),setSort:(sort)=>update({page:1,sort:[...sort]}),setTextSearch:(textSearch)=>update({page:1,textSearch}),state});var useEntityViewState2=(namespace,input={})=>{if(!namespacePattern.test(namespace)){throw new Error("URL-backed entity views require a stable alphanumeric namespace")}const parsers=useMemo4(()=>parsersFor(pageSizeFor(input.pageSize),input.contract),[input.pageSize,input.contract]);const[state,setState]=useQueryStates(parsers,{clearOnDefault:true,history:"replace",shallow:false,urlKeys:{filter:`${namespace}.filter`,page:`${namespace}.page`,pageSize:`${namespace}.pageSize`,sort:`${namespace}.sort`,textSearch:`${namespace}.textSearch`}});const normalized=useMemo4(()=>({filter:state.filter,page:state.page>0?state.page:1,pageSize:state.pageSize>0&&state.pageSize<=100?state.pageSize:input.pageSize??DEFAULT_PAGE_SIZE,sort:state.sort,textSearch:state.textSearch}),[input.pageSize,state]);return controller(normalized,(next)=>{const{sort,...values}=next;const update=values;if(sort!==undefined){update.sort=[...sort]}setState(update)})};var useMemoryEntityViewState2=(input={})=>{const[state,setState]=useState({filter:null,page:1,pageSize:pageSizeFor(input.pageSize),sort:[],textSearch:""});return controller(state,(next)=>{setState((current)=>({...current,...next}))})};import{jsx as jsx2,jsxs,Fragment}from"react/jsx-runtime";var isObject2=(value)=>value!==null&&typeof value==="object"&&!Array.isArray(value);var schemaProperties=(entity,mode="entity")=>{const properties=entity.contract.schemas[mode]?.properties;return isObject2(properties)?properties:{}};var requiredFields=(entity,mode)=>{const required=entity.contract.schemas[mode]?.required;return new Set(Array.isArray(required)?required.filter((value)=>typeof value==="string"):[])};var scalarTypes=(schema)=>{const{type}=schema;if(typeof type==="string"){return[type]}return Array.isArray(type)?type.filter((value)=>typeof value==="string"):[]};var presentationSchema=(schema)=>{const union=schema.anyOf??schema.oneOf;if(!Array.isArray(union)){return schema}const members=union.filter(isObject2);const values=members.filter((member)=>member.type!=="null");const[value]=values;if(members.length===2&&values.length===1&&value!==undefined){return{...schema,...value,type:[...scalarTypes(value),"null"]}}return schema};var defaultControl=(schema)=>{if(schema.$ref!==undefined||(schema.anyOf!==undefined||schema.oneOf!==undefined)&&schema.type===undefined){return"custom"}if(Array.isArray(schema.enum)){return"select"}const types=scalarTypes(schema);if(types.includes("boolean")){return"checkbox"}if(types.includes("number")||types.includes("integer")){return"number"}if(schema.format==="email"){return"email"}if(schema.format==="date"){return"date"}if(schema.format==="date-time"){return"datetime-local"}return types.some((type)=>type==="object"||type==="array")?"custom":"text"};var configuredControl=(config,schema)=>{switch(config.control){case"text":case"email":case"number":case"checkbox":case"date":case"datetime-local":case"select":case"custom":{return config.control}case undefined:{return defaultControl(schema)}default:{return"custom"}}};var formFields=(entity,mode)=>{const properties=schemaProperties(entity,mode);const entityProperties=schemaProperties(entity);const required=requiredFields(entity,mode);return entity.config.forms[mode].fields.map((name)=>{const config=entity.config.fields[name];if(config===undefined){throw new Error(`Missing entity field configuration: ${name}`)}const candidate=properties[name]??entityProperties[name];const schema=isObject2(candidate)?presentationSchema(candidate):{};const entityField=entityProperties[name];const enums=Array.isArray(schema.enum)?schema.enum.filter((value)=>typeof value==="string"||typeof value==="number"||typeof value==="boolean"):[];return{control:configuredControl(config,schema),label:config.label,name,nullable:scalarTypes(schema).includes("null")||Array.isArray(schema.enum)&&schema.enum.includes(null),options:enums.map((value)=>({label:String(value),value})),readOnly:config.readOnly===true||schema.readOnly===true||isObject2(entityField)&&entityField.readOnly===true,required:required.has(name)}})};var displayValue=(value)=>{if(value===null||value===undefined){return"—"}if(typeof value==="boolean"){return value?"Yes":"No"}return typeof value==="object"?JSON.stringify(value):String(value)};var renderField=(entity,record,name,renderers)=>{const renderer=entity.config.fields[name]?.renderer;return renderer===undefined||renderers?.[renderer]===undefined?displayValue(record.value[name]):renderers[renderer]({field:name,record})};var sortFor=(state,field)=>{const[current]=state.state.sort;return[{direction:current?.field===field&¤t.direction==="asc"?"desc":"asc",field}]};var QueryControls=({view})=>jsx2(ResourceFiltersBar,{controls:[{control:jsx2(ResourceFilterPrimitive,{label:"Search",onChange:(value)=>view.setTextSearch(value),value:view.state.textSearch}),fullWidth:true,key:"search"}]});var EntityStatus=({state})=>{if(state.authState.status!=="authenticated"){return jsx2("output",{children:state.authState.status})}if(state.loading){return jsx2("output",{children:"Loading"})}if(state.error!==null){return jsx2("output",{role:"alert",children:state.error.message})}return null};var EntityPagination=({result,view,pending})=>{if(result===undefined||result.pagination!=="page"){return null}return jsx2(ResourcePaginationPrimitive,{currentPage:result.page,totalPages:result.totalPages,hasPreviousPage:result.page>1,hasNextPage:result.page<result.totalPages,isPending:pending,onPageChange:(page)=>view.setPage(page),onPrevious:()=>view.setPage(result.page-1),onNext:()=>view.setPage(result.page+1)})};var ControlledEntityTable=({entity,empty,renderers,variant,view})=>{const result=useEntityList2(entity.contract,view.query);const names=variant===undefined?entity.config.table.columns:entity.config.table.variants?.[variant]?.columns??entity.config.table.columns;const columns=names.map((name)=>({id:name,label:entity.config.fields[name]?.label??name}));const hasStatus=result.authState.status!=="authenticated"||result.loading||result.error!==null;return jsxs("section",{children:[jsx2(QueryControls,{view}),hasStatus?jsx2(EntityStatus,{state:result}):jsxs(Fragment,{children:[jsx2(ResourceTablePrimitive,{columns:columns.map((column)=>({...column,label:entity.contract.query.sortableFields.includes(column.id)?jsx2("button",{type:"button",onClick:()=>view.setSort(sortFor(view,column.id)),children:column.label}):column.label})),empty,getRowKey:(record)=>record.id,renderCell:(record,column)=>{const name=names.find((field)=>field===column.id);return name===undefined?null:renderField(entity,record,name,renderers)},rows:result.data?.items??[]}),jsx2(EntityPagination,{pending:result.refreshing,result:result.data,view})]})]})};var UrlEntityTable=(props)=>jsx2(ControlledEntityTable,{...props,view:useEntityViewState2(props.namespace,{contract:props.entity.contract,pageSize:props.pageSize??25})});var MemoryEntityTable=(props)=>jsx2(ControlledEntityTable,{...props,view:useMemoryEntityViewState2(props.pageSize===undefined?{}:{pageSize:props.pageSize})});var EntityTable2=(props)=>props.viewState==="memory"?jsx2(MemoryEntityTable,{...props}):jsx2(UrlEntityTable,{...props,namespace:props.namespace});var ControlledEntityList=({entity,empty,renderers,view})=>{const result=useEntityList2(entity.contract,view.query);const hasStatus=result.authState.status!=="authenticated"||result.loading||result.error!==null;return jsxs("section",{children:[jsx2(QueryControls,{view}),hasStatus?jsx2(EntityStatus,{state:result}):jsxs(Fragment,{children:[jsx2(ResourceListPrimitive,{empty,getItemKey:(record)=>record.id,items:result.data?.items??[],renderItem:(record)=>jsxs("article",{children:[jsx2("strong",{children:renderField(entity,record,entity.config.list.title,renderers)}),entity.config.list.description===undefined?null:jsx2("p",{children:renderField(entity,record,entity.config.list.description,renderers)})]})}),jsx2(EntityPagination,{pending:result.refreshing,result:result.data,view})]})]})};var UrlEntityList=(props)=>jsx2(ControlledEntityList,{...props,view:useEntityViewState2(props.namespace,{contract:props.entity.contract,pageSize:props.pageSize??25})});var MemoryEntityList=(props)=>jsx2(ControlledEntityList,{...props,view:useMemoryEntityViewState2(props.pageSize===undefined?{}:{pageSize:props.pageSize})});var EntityList2=(props)=>props.viewState==="memory"?jsx2(MemoryEntityList,{...props}):jsx2(UrlEntityList,{...props,namespace:props.namespace});var initialFormValues=(entity,mode,supplied,record)=>{const values={};const properties=schemaProperties(entity,mode);const required=requiredFields(entity,mode);for(const name of entity.config.forms[mode].fields){const schema=properties[name];if(required.has(name)&&isObject2(schema)&&scalarTypes(presentationSchema(schema)).includes("boolean")){values[name]=false}if(isObject2(schema)&&schema.default!==undefined){values[name]=schema.default}if(record?.value[name]!==undefined){values[name]=record.value[name]}if(supplied?.[name]!==undefined){values[name]=supplied[name]}}return values};var controlRenderer=(entity,controls)=>function renderEntityControl(props){const fields=entity.config.fields;const key=fields[props.field.name]?.control;return key===undefined||controls?.[key]===undefined?jsxs("p",{role:"alert",children:["A custom control is required for ",props.field.label,"."]}):controls[key](props)};var mutationMessage=(mutation)=>{if(mutation.status==="conflict"){return"This entity changed. Your draft is preserved."}if(mutation.status==="uncertain"){return"The write outcome is unknown. Resubmit the same action to resolve it."}return mutation.error?.resourceError.message};var writableValues=(fields,values)=>{const writable=new Set(fields.filter((field)=>!field.readOnly).map((field)=>field.name));return Object.fromEntries(Object.entries(values).filter(([name])=>writable.has(name)))};var displayedValues=(fields,values)=>Object.fromEntries(Object.entries(values).map(([name,value])=>{if(typeof value!=="string"||fields.find((field)=>field.name===name)?.control!=="datetime-local"){return[name,value]}const date=new Date(value);if(Number.isNaN(date.getTime())){return[name,value]}return[name,new Date(date.getTime()-date.getTimezoneOffset()*60000).toISOString().slice(0,23)]}));var wireValue=(fields,name,value)=>{if(typeof value!=="string"||value===""||fields.find((field)=>field.name===name)?.control!=="datetime-local"){return value}const date=new Date(value);return Number.isNaN(date.getTime())?value:date.toISOString()};var CreateEntityForm=(props)=>{const mutation=useEntityCreate2(props.entity.contract);const[values,setValues]=useState2(()=>initialFormValues(props.entity,"create",props.initialValues));const fields=useMemo5(()=>formFields(props.entity,"create"),[props.entity]);return jsx2(ResourceFormPrimitive,{label:"Create entity",fields,values:displayedValues(fields,values),disabled:!mutation.available,pending:mutation.pending,error:mutationMessage(mutation),...props.onCancel===undefined?{}:{onCancel:props.onCancel},onChange:(name,value)=>setValues((current)=>({...current,[name]:wireValue(fields,name,value)})),onSubmit:async()=>{const value=writableValues(fields,values);const action=mutation.status==="uncertain"?mutation.resubmit():mutation.submit({value});const result=await action;if(result.ok){props.onSuccess?.(result.value)}},renderControl:controlRenderer(props.entity,props.controls),submitLabel:mutation.status==="uncertain"?"Resubmit":"Create"})};var UpdateEntityForm=(props)=>{const mutation=useEntityUpdate2(props.entity.contract,{id:props.record.id});const[draft,setDraft]=useState2(()=>({expectedRevision:props.record.revision,values:initialFormValues(props.entity,"update",props.initialValues,props.record)}));const{expectedRevision,values}=draft;const fields=useMemo5(()=>formFields(props.entity,"update"),[props.entity]);return jsx2(ResourceFormPrimitive,{label:"Update entity",fields,values:displayedValues(fields,values),disabled:!mutation.available,pending:mutation.pending,error:mutationMessage(mutation),...props.onCancel===undefined?{}:{onCancel:props.onCancel},onChange:(name,value)=>setDraft((current)=>({...current,values:{...current.values,[name]:wireValue(fields,name,value)}})),onSubmit:async()=>{const value=writableValues(fields,values);const result=await(mutation.status==="uncertain"?mutation.resubmit():mutation.submit({expectedRevision,value}));if(result.ok){props.onSuccess?.(result.value)}},renderControl:controlRenderer(props.entity,props.controls),submitLabel:mutation.status==="uncertain"?"Resubmit":"Save"})};var EntityForm2=(props)=>{const runtime=useEntityRuntime();if(runtime.state.status!=="authenticated"){return jsx2("output",{children:runtime.state.status})}return props.mode==="create"?jsx2(CreateEntityForm,{...props},runtime.formScope):jsx2(UpdateEntityForm,{...props,record:props.record},JSON.stringify([runtime.formScope,props.record.id]))};
|
|
2
|
+
export{resourceKeys,ResourceQueryError2,unwrapResourceResult2,createResourceListQueryOptions,mapResourceAuthState2,hasResourceCapability2,clearProtectedResourceQueries,EntityRuntimeProvider,createDelegatedEntitySource,defineEntity2,useEntityUpdate2,useEntityCreate2,useEntityDelete2,useEntityList2,useEntityRecord2,useEntityViewState2,useMemoryEntityViewState2,EntityTable2,EntityList2,EntityForm2};
|