@cfast/actions 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +13 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -1
- package/dist/{types-ogCcbQm3.d.ts → types-C1PGA5l3.d.ts} +58 -3
- package/llms.txt +55 -3
- package/package.json +4 -4
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as ClientDescriptor, S as Serializable } from './types-
|
|
1
|
+
import { C as ClientDescriptor, S as Serializable } from './types-C1PGA5l3.js';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
import { Form } from 'react-router';
|
|
4
4
|
import { ComponentProps, ReactNode } from 'react';
|
|
@@ -9,8 +9,16 @@ import '@cfast/permissions';
|
|
|
9
9
|
* Creates a ClientDescriptor for use in client code without importing
|
|
10
10
|
* server modules. The action names must match the keys passed to
|
|
11
11
|
* `composeActions` or the single action name from `createAction`.
|
|
12
|
+
*
|
|
13
|
+
* Pass a `readonly` tuple (`as const`) to get compile-time type-checking
|
|
14
|
+
* of action names throughout the client code:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const client = clientDescriptor(["create", "delete"] as const);
|
|
18
|
+
* // client is ClientDescriptor<readonly ["create", "delete"]>
|
|
19
|
+
* ```
|
|
12
20
|
*/
|
|
13
|
-
declare function clientDescriptor
|
|
21
|
+
declare function clientDescriptor<const TNames extends readonly string[]>(actionNames: TNames): ClientDescriptor<TNames>;
|
|
14
22
|
type ActionHookResult = {
|
|
15
23
|
permitted: boolean;
|
|
16
24
|
invisible: boolean;
|
|
@@ -20,7 +28,9 @@ type ActionHookResult = {
|
|
|
20
28
|
data: unknown | undefined;
|
|
21
29
|
error: unknown | undefined;
|
|
22
30
|
};
|
|
23
|
-
declare function useActions(descriptor: ClientDescriptor):
|
|
31
|
+
declare function useActions<const TNames extends readonly string[]>(descriptor: ClientDescriptor<TNames>): {
|
|
32
|
+
[K in TNames[number]]: (input?: Serializable) => ActionHookResult;
|
|
33
|
+
};
|
|
24
34
|
|
|
25
35
|
type ActionFormProps = Omit<ComponentProps<typeof Form>, "children" | "action"> & {
|
|
26
36
|
/** Object with `_action` key and input fields to inject as hidden inputs. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Grant, PermissionDescriptor } from '@cfast/permissions';
|
|
2
|
-
import { A as ActionPermissionStatus, a as ActionServices, b as ActionsConfig, O as OperationsFn, c as ActionDefinition, d as ComposedActions } from './types-
|
|
3
|
-
export { e as ActionContext, f as ActionPermissionsMap, C as ClientDescriptor, R as RequestArgs, S as Serializable } from './types-
|
|
2
|
+
import { A as ActionPermissionStatus, a as ActionServices, b as ActionsConfig, O as OperationsFn, c as ActionDefinition, d as ComposedActions } from './types-C1PGA5l3.js';
|
|
3
|
+
export { e as ActionContext, f as ActionPermissionsMap, C as ClientDescriptor, D as DispatchArgs, R as RequestArgs, S as Serializable } from './types-C1PGA5l3.js';
|
|
4
4
|
import '@cfast/db';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/index.js
CHANGED
|
@@ -104,7 +104,12 @@ function createActions(config) {
|
|
|
104
104
|
const buildOperation = (db, input, ctx) => {
|
|
105
105
|
return handler(db, input, ctx);
|
|
106
106
|
};
|
|
107
|
-
|
|
107
|
+
const dispatch = async (args) => {
|
|
108
|
+
const { ctx, input } = args;
|
|
109
|
+
const operation = handler(ctx.db, input, ctx);
|
|
110
|
+
return operation.run();
|
|
111
|
+
};
|
|
112
|
+
return { action, dispatch, loader, client, buildOperation };
|
|
108
113
|
}
|
|
109
114
|
function composeActions(actions) {
|
|
110
115
|
const actionNames = Object.keys(actions);
|
|
@@ -80,6 +80,37 @@ type RequestArgs = {
|
|
|
80
80
|
/** Optional context object (e.g., Cloudflare Workers env via `context.cloudflare.env`). */
|
|
81
81
|
context?: unknown;
|
|
82
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Arguments for {@link ActionDefinition.dispatch}, which lets a parent action
|
|
85
|
+
* invoke a sub-action **without** constructing a new `Request`.
|
|
86
|
+
*
|
|
87
|
+
* The parent passes its already-resolved {@link ActionContext} and the typed
|
|
88
|
+
* input directly. This avoids re-running `getContext()` (and thus the
|
|
89
|
+
* cookie-based session lookup) for the sub-action, which is both faster and
|
|
90
|
+
* eliminates the class of bugs described in issue #185 where a manually-built
|
|
91
|
+
* `Request` forgets to forward the `Cookie` header.
|
|
92
|
+
*
|
|
93
|
+
* @typeParam TInput - The expected input shape for the target action.
|
|
94
|
+
* @typeParam TUser - The shape of the authenticated user object.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const parent = createAction((db, input, ctx) => ({
|
|
99
|
+
* permissions: [],
|
|
100
|
+
* async run() {
|
|
101
|
+
* // No Request needed — ctx is reused directly.
|
|
102
|
+
* const result = await child.dispatch({ ctx, input: { title: "Hello" } });
|
|
103
|
+
* return result;
|
|
104
|
+
* },
|
|
105
|
+
* }));
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
type DispatchArgs<TInput, TUser> = {
|
|
109
|
+
/** The parent action's already-resolved context, reused as-is. */
|
|
110
|
+
ctx: ActionContext<TUser>;
|
|
111
|
+
/** Typed input for the sub-action. */
|
|
112
|
+
input: TInput;
|
|
113
|
+
};
|
|
83
114
|
/**
|
|
84
115
|
* Configuration for the {@link createActions} factory.
|
|
85
116
|
*
|
|
@@ -192,11 +223,11 @@ type ActionPermissionsMap = Record<string, ActionPermissionStatus>;
|
|
|
192
223
|
* Created by {@link ActionDefinition.client} or {@link ComposedActions.client}.
|
|
193
224
|
* Contains the action names and the key used to read permission data from loader results.
|
|
194
225
|
*/
|
|
195
|
-
type ClientDescriptor = {
|
|
226
|
+
type ClientDescriptor<TNames extends readonly string[] = readonly string[]> = {
|
|
196
227
|
/** Brand field to distinguish this type at the type level. */
|
|
197
228
|
_brand: "ActionClientDescriptor";
|
|
198
229
|
/** The list of action names this descriptor covers. */
|
|
199
|
-
actionNames:
|
|
230
|
+
actionNames: TNames;
|
|
200
231
|
/** The loader-data key where {@link ActionPermissionsMap} is stored. */
|
|
201
232
|
permissionsKey: string;
|
|
202
233
|
};
|
|
@@ -230,6 +261,30 @@ type ClientDescriptor = {
|
|
|
230
261
|
type ActionDefinition<TInput, TResult, TUser> = {
|
|
231
262
|
/** React Router action handler. Parses input, resolves context, and runs the operation. */
|
|
232
263
|
action: (args: RequestArgs) => Promise<TResult>;
|
|
264
|
+
/**
|
|
265
|
+
* Dispatches the action using an already-resolved {@link ActionContext},
|
|
266
|
+
* bypassing `getContext()` entirely.
|
|
267
|
+
*
|
|
268
|
+
* Use this when a parent action handler needs to invoke a sub-action.
|
|
269
|
+
* Because the parent's `ctx` is reused directly, cookies, user session,
|
|
270
|
+
* and grants are inherited without constructing a new `Request` — which
|
|
271
|
+
* eliminates the cookie-forwarding bug described in issue #185.
|
|
272
|
+
*
|
|
273
|
+
* @param args - The {@link DispatchArgs} containing the parent's `ctx`
|
|
274
|
+
* and the typed input for this action.
|
|
275
|
+
* @returns The action's result, same as calling `.action()`.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* const parent = createAction((db, input, ctx) => ({
|
|
280
|
+
* permissions: [],
|
|
281
|
+
* async run() {
|
|
282
|
+
* return child.dispatch({ ctx, input: { title: "Hello" } });
|
|
283
|
+
* },
|
|
284
|
+
* }));
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
dispatch: (args: DispatchArgs<TInput, TUser>) => Promise<TResult>;
|
|
233
288
|
/**
|
|
234
289
|
* Wraps a loader function to inject {@link ActionPermissionsMap} into its return value.
|
|
235
290
|
*
|
|
@@ -285,4 +340,4 @@ type ComposedActions<TActions extends Record<string, ActionDefinition<any, any,
|
|
|
285
340
|
actions: TActions;
|
|
286
341
|
};
|
|
287
342
|
|
|
288
|
-
export type { ActionPermissionStatus as A, ClientDescriptor as C, OperationsFn as O, RequestArgs as R, Serializable as S, ActionServices as a, ActionsConfig as b, ActionDefinition as c, ComposedActions as d, ActionContext as e, ActionPermissionsMap as f };
|
|
343
|
+
export type { ActionPermissionStatus as A, ClientDescriptor as C, DispatchArgs as D, OperationsFn as O, RequestArgs as R, Serializable as S, ActionServices as a, ActionsConfig as b, ActionDefinition as c, ComposedActions as d, ActionContext as e, ActionPermissionsMap as f };
|
package/llms.txt
CHANGED
|
@@ -68,9 +68,31 @@ type ActionPermissionsMap = Record<string, ActionPermissionStatus>;
|
|
|
68
68
|
|
|
69
69
|
### Client (`@cfast/actions/client`)
|
|
70
70
|
|
|
71
|
+
#### Type-safe `clientDescriptor()` and `useActions()`
|
|
72
|
+
|
|
73
|
+
`clientDescriptor()` now uses `const` type parameters to infer the literal tuple
|
|
74
|
+
type from the action names array. `useActions()` returns a mapped type keyed by
|
|
75
|
+
those literal names instead of `Record<string, ...>`, giving compile-time
|
|
76
|
+
autocomplete and error checking on action names.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Type-safe: actions.create and actions.delete are typed, typos are caught
|
|
80
|
+
const client = clientDescriptor(["create", "delete"]);
|
|
81
|
+
// client is ClientDescriptor<readonly ["create", "delete"]>
|
|
82
|
+
|
|
83
|
+
const actions = useActions(client);
|
|
84
|
+
// actions is { create: (input?) => ActionHookResult; delete: (input?) => ActionHookResult }
|
|
85
|
+
// actions.creat → TypeScript error (typo caught at compile time)
|
|
86
|
+
```
|
|
87
|
+
|
|
71
88
|
```typescript
|
|
72
|
-
function useActions
|
|
73
|
-
|
|
89
|
+
function useActions<const TNames extends readonly string[]>(
|
|
90
|
+
descriptor: ClientDescriptor<TNames>,
|
|
91
|
+
): { [K in TNames[number]]: (input?: Serializable) => ActionHookResult };
|
|
92
|
+
|
|
93
|
+
function clientDescriptor<const TNames extends readonly string[]>(
|
|
94
|
+
actionNames: TNames,
|
|
95
|
+
): ClientDescriptor<TNames>;
|
|
74
96
|
|
|
75
97
|
function ActionForm(props: ActionFormProps): JSX.Element;
|
|
76
98
|
|
|
@@ -187,7 +209,37 @@ new body is `FormData` or `URLSearchParams`, so the platform can attach the
|
|
|
187
209
|
correct multipart boundary. Pass an explicit `headers: { "content-type": ... }`
|
|
188
210
|
to override.
|
|
189
211
|
|
|
190
|
-
### 5.
|
|
212
|
+
### 5. Dispatch sub-actions with `action.dispatch()` (preferred)
|
|
213
|
+
|
|
214
|
+
`action.dispatch({ ctx, input })` calls an action's operation directly,
|
|
215
|
+
bypassing Request construction entirely. No cookie forwarding, no FormData
|
|
216
|
+
serialisation, no `forwardRequest()` boilerplate -- just pass the context
|
|
217
|
+
and input you already have:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { createRow } from "~/actions/rows";
|
|
221
|
+
|
|
222
|
+
export const importCsv = createAction(async (db, input, ctx) => ({
|
|
223
|
+
permissions: createRow.buildOperation(db, {} as never, ctx).permissions,
|
|
224
|
+
async run() {
|
|
225
|
+
for (const row of input.rows) {
|
|
226
|
+
await createRow.dispatch({
|
|
227
|
+
ctx: { db, user: ctx.user, grants: ctx.grants, services: {} },
|
|
228
|
+
input: { title: row.title },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return { ok: true };
|
|
232
|
+
},
|
|
233
|
+
}));
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`dispatch()` reuses the caller's `db`, `user`, and `grants` directly, so
|
|
237
|
+
permission checks still run on the sub-action's operation. Use `dispatch()`
|
|
238
|
+
for all server-to-server sub-action calls. `forwardRequest()` still works
|
|
239
|
+
but is only needed when you must go through the full HTTP action handler
|
|
240
|
+
(e.g., calling an action in a different Worker).
|
|
241
|
+
|
|
242
|
+
### 6. Use on the client
|
|
191
243
|
```typescript
|
|
192
244
|
import { useActions } from "@cfast/actions/client";
|
|
193
245
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfast/actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Multi-action routes and permission-aware action definitions for React Router",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cfast",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@cfast/db": ">=0.3.0 <0.5.0",
|
|
42
|
-
"@cfast/permissions": ">=0.3.0 <0.
|
|
42
|
+
"@cfast/permissions": ">=0.3.0 <0.6.0",
|
|
43
43
|
"react": "^19.0.0",
|
|
44
44
|
"react-router": "^7.0.0"
|
|
45
45
|
},
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"tsup": "^8",
|
|
60
60
|
"typescript": "^5.7",
|
|
61
61
|
"vitest": "^4.1.0",
|
|
62
|
-
"@cfast/db": "0.
|
|
63
|
-
"@cfast/permissions": "0.
|
|
62
|
+
"@cfast/db": "0.5.0",
|
|
63
|
+
"@cfast/permissions": "0.5.1"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsup src/index.ts src/client.ts --format esm --dts",
|