@collective-intelligence/external-apps-client-sdk-react 0.1.35 → 0.1.36

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 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. TanStack Query is a normal
6
- SDK dependency; applications do not install it as a peer or configure a QueryClient for SDK CRUD.
7
- React and React DOM remain peers.
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
@@ -116,8 +123,14 @@ Drafts and operation keys never enter the URL.
116
123
 
117
124
  ## Realtime topics
118
125
 
119
- `RealtimeProvider` shares one `createRealtimeClient` instance, and so one
120
- connection, with every component of the tab.
126
+ `RealtimeProvider` takes the options of `createRealtimeClient` as props. It
127
+ creates one client, and so one connection, for its tree, and closes it when the
128
+ tree unmounts. Its effects and the hooks below it hold the client by reference
129
+ count. Thus a Strict Mode double mount opens one stream, and a server render
130
+ creates no client. Without a `document` prop the client follows the page of
131
+ the browser. The client reads the other props, such as `ticketUrl` and
132
+ `fetch`, once, when the provider creates it; a later change has no effect. To
133
+ use new values, remount the provider, for example with a React `key`.
121
134
  `useRealtimeTopic(topic, onChanges)` subscribes while the component is mounted
122
135
  and unsubscribes on unmount. A new topic object for the same topic keeps the
123
136
  subscription; `null` subscribes to nothing. The latest `onChanges` is called
@@ -125,22 +138,29 @@ without resubscribing. It receives the changes of the topic in batches: one
125
138
  delivery per tab, 250 to 1000 ms after the first pending change, with the
126
139
  latest change of each record.
127
140
 
128
- Pass `onReset` to the client and refetch every realtime-driven query in it.
129
- The client calls it when the server cannot replay a gap and once after a
130
- stream opens without a last event id, the first one of each page included.
131
- Without `onReset` the client cannot recover from such a gap, so a change can
132
- be lost.
141
+ Pass `onReset` and refetch every realtime-driven query in it. The client calls
142
+ it when the server cannot replay a gap and once after a stream opens without a
143
+ last event id, the first one of each page included. Without `onReset` the
144
+ client cannot recover from such a gap, so a change can be lost. The client
145
+ calls `onAccessChanged` when the user's access changes. Both stay callbacks of
146
+ the application; the client calls the ones of the latest render, also when a
147
+ render adds or removes one. Only a client with `onReset` resyncs, so a render
148
+ that adds or removes `onReset` replaces the client and its stream. The new
149
+ stream opens without a last event id, so with `onReset` it resyncs once.
133
150
 
134
151
  ```tsx
135
- // `queryClient` is the QueryClient of your realtime-driven queries.
136
- const realtime = createRealtimeClient({
137
- ticketUrl: "/api/realtime/ticket",
138
- onReset: () => queryClient.invalidateQueries()
139
- })
140
-
141
- <RealtimeProvider client={realtime}>
142
- <LeadBoard />
143
- </RealtimeProvider>
152
+ const AppRealtime = ({ children }: { children: ReactNode }) => {
153
+ const queryClient = useQueryClient()
154
+ return (
155
+ <RealtimeProvider
156
+ ticketUrl="/api/realtime/ticket"
157
+ onReset={() => queryClient.invalidateQueries()}
158
+ onAccessChanged={() => queryClient.invalidateQueries()}
159
+ >
160
+ {children}
161
+ </RealtimeProvider>
162
+ )
163
+ }
144
164
 
145
165
  const LeadBoard = () => {
146
166
  const queryClient = useQueryClient()
@@ -152,6 +172,68 @@ const LeadBoard = () => {
152
172
  }
153
173
  ```
154
174
 
175
+ ## Effect bridge
176
+
177
+ `./effect` connects XState machines to Effect and to the application's
178
+ QueryClient. It has no Effect Atom bindings.
179
+
180
+ - `fromEffect(run)` makes actor logic from an Effect. The actor runs the effect
181
+ on `input.runtime`, so annotate `input` with a `runtime: EffectRunner<R>`
182
+ that provides every service of the effect. The actor's output is the
183
+ effect's value; on a failure, `onError` gets the typed error or the defect.
184
+ A stop of the actor interrupts the fiber, and the run ends. After the stop,
185
+ no output, error or interruption of that run gets to the actor, also when
186
+ React Strict Mode starts the actor of `useActorRef` again.
187
+ - `EffectRuntimeProvider` gives its tree a runtime with `QueryCache` over the
188
+ nearest QueryClient; `useEffectRuntime()` returns it. Put it inside
189
+ `CollectiveIntelligenceProvider`. The runtime holds no resource, so the
190
+ provider never disposes it.
191
+ - `QueryCache` is the one way for machine actors to read and write remote data;
192
+ machine context holds none. `fetch({ queryKey, query, staleTime? })` returns
193
+ a fresh cached value or runs `query` once, with no automatic retry, and caches
194
+ its value. `CiError` and `CiTransportError` stay typed errors; another failure
195
+ is a defect. While a `fetch` waits, it keeps the key in use, as a mounted
196
+ view does. When the last view or `fetch` of the key stops waiting, for
197
+ example because its actor stops, TanStack Query cancels the fetch and
198
+ `query` is interrupted. Thus in React Strict Mode, the actor that
199
+ `useActorRef` starts again can run `query` a second time. A cancel of the
200
+ key, for example `queryClient.cancelQueries({ queryKey })`, also interrupts
201
+ `query`. A `fetch` that waits then returns the value from before the fetch,
202
+ or is interrupted when the key has no value. `invalidate(queryKey)` marks
203
+ values stale and refetches the ones that views show.
204
+ `setData(queryKey, value)` writes a value that views show at once. Views
205
+ read the same keys with `useQuery`.
206
+
207
+ ```tsx
208
+ import type { EffectRunner } from "@collective-intelligence/external-apps-client-sdk-react/effect"
209
+ import {
210
+ EffectRuntimeProvider,
211
+ QueryCache,
212
+ fromEffect,
213
+ useEffectRuntime
214
+ } from "@collective-intelligence/external-apps-client-sdk-react/effect"
215
+
216
+ const loadLead = fromEffect(
217
+ (input: Readonly<{ id: string; runtime: EffectRunner<QueryCache> }>) =>
218
+ QueryCache.use((cache) =>
219
+ cache.fetch({ queryKey: ["crm", "lead", input.id], query: readLead(input.id) })
220
+ )
221
+ )
222
+
223
+ <CollectiveIntelligenceProvider client={appSdk} queryClient={queryClient}>
224
+ <EffectRuntimeProvider>
225
+ <LeadEditor id={leadId} />
226
+ </EffectRuntimeProvider>
227
+ </CollectiveIntelligenceProvider>
228
+
229
+ const LeadEditor = ({ id }: { id: string }) => {
230
+ const runtime = useEffectRuntime()
231
+ // `leadMachine` invokes `loadLead` with `{ id, runtime }` from its input.
232
+ const lead = useActorRef(leadMachine, { input: { id, runtime } })
233
+ // …
234
+ }
235
+ ```
236
+
155
237
  ## Workspace development
156
238
 
157
239
  See the repository's [Effect demo](../../../demo/external-app-effect/README.md)
@@ -164,9 +246,10 @@ are consumed without an intermediate rebuild. Release builds disable custom cond
164
246
  consumers resolve generated `dist` JavaScript and declarations. Shared runtime chunks keep provider
165
247
  contexts identical across the root and `/resources` entrypoints.
166
248
 
167
- Run `bunx vitest run src`, `bun run type-check`, `bun run lint`, and `bun run test:pack`. The pack check
168
- executes a real root-provider/resources-hook consumer without caller TanStack setup and removes its
169
- temporary installation on success or failure.
249
+ Run `bun run test:unit`, `bun run type-check`, `bun run lint`, and `bun run test:pack`. The pack check
250
+ executes a real root-provider/resources-hook consumer without caller TanStack setup, and a `./effect`
251
+ consumer that shares the application's QueryClient. It removes its temporary installation on success
252
+ or failure. The repository root runs `bun run lint:effect` for the Effect rules.
170
253
 
171
254
  ## Enum value compatibility
172
255
 
@@ -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&&current.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};