@cfast/actions 0.1.3 → 0.3.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 +46 -4
- package/dist/client.js +113 -1
- package/dist/index.d.ts +39 -4
- package/dist/index.js +46 -0
- package/dist/{types-CJpjon5s.d.ts → types-C1PGA5l3.d.ts} +2 -2
- package/llms.txt +69 -2
- 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,41 @@ 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
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Client hook that replaces `useLoaderData()` for routes using `cfastJson()`.
|
|
37
|
+
*
|
|
38
|
+
* Returns the same data but wraps arrays and objects that have `_can` with
|
|
39
|
+
* permission helper methods:
|
|
40
|
+
*
|
|
41
|
+
* - **Collections** (arrays with items that have `_can`): array works normally
|
|
42
|
+
* (indexing, `.map()`, etc.) AND has `canAdd()`. Each item has `canEdit()`,
|
|
43
|
+
* `canDelete()`, `canRead()`, `canCreate()`.
|
|
44
|
+
* - **Items** (objects with `_can`): row properties directly accessible,
|
|
45
|
+
* plus `canEdit()`, `canDelete()`, `canRead()`, `canCreate()`.
|
|
46
|
+
* - **Other fields**: passed through unchanged.
|
|
47
|
+
*
|
|
48
|
+
* Each `can*()` method returns an {@link ActionHookResult}.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* const { documents } = useCfastLoader<typeof loader>();
|
|
53
|
+
*
|
|
54
|
+
* // Collection-level
|
|
55
|
+
* documents.canAdd() // -> ActionHookResult
|
|
56
|
+
*
|
|
57
|
+
* // Row fields accessible directly
|
|
58
|
+
* documents[0].title // "Hello World"
|
|
59
|
+
*
|
|
60
|
+
* // Row-level
|
|
61
|
+
* documents[0].canEdit() // -> ActionHookResult
|
|
62
|
+
* documents[0].canDelete() // -> ActionHookResult
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function useCfastLoader<T extends (...args: any[]) => any = () => Record<string, unknown>>(): ReturnType<T> extends Promise<infer R> ? R : ReturnType<T>;
|
|
24
66
|
|
|
25
67
|
type ActionFormProps = Omit<ComponentProps<typeof Form>, "children" | "action"> & {
|
|
26
68
|
/** Object with `_action` key and input fields to inject as hidden inputs. */
|
|
@@ -47,4 +89,4 @@ type ActionFormProps = Omit<ComponentProps<typeof Form>, "children" | "action">
|
|
|
47
89
|
*/
|
|
48
90
|
declare function ActionForm({ action, children, ...formProps }: ActionFormProps): react_jsx_runtime.JSX.Element;
|
|
49
91
|
|
|
50
|
-
export { ActionForm, type ActionHookResult, ClientDescriptor, clientDescriptor, useActions };
|
|
92
|
+
export { ActionForm, type ActionHookResult, ClientDescriptor, clientDescriptor, useActions, useCfastLoader };
|
package/dist/client.js
CHANGED
|
@@ -67,6 +67,117 @@ function useActions(descriptor) {
|
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// src/client/use-cfast-loader.ts
|
|
71
|
+
import { useMemo } from "react";
|
|
72
|
+
import { useLoaderData as useLoaderData2 } from "react-router";
|
|
73
|
+
function makeResult(permitted) {
|
|
74
|
+
return {
|
|
75
|
+
permitted,
|
|
76
|
+
invisible: false,
|
|
77
|
+
reason: null,
|
|
78
|
+
submit: () => {
|
|
79
|
+
},
|
|
80
|
+
pending: false,
|
|
81
|
+
data: void 0,
|
|
82
|
+
error: void 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function wrapItem(item) {
|
|
86
|
+
const can = item._can ?? {};
|
|
87
|
+
const wrapped = /* @__PURE__ */ Object.create(null);
|
|
88
|
+
for (const [key, value] of Object.entries(item)) {
|
|
89
|
+
if (key === "_can") continue;
|
|
90
|
+
Object.defineProperty(wrapped, key, {
|
|
91
|
+
value,
|
|
92
|
+
enumerable: true,
|
|
93
|
+
writable: true,
|
|
94
|
+
configurable: true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
Object.defineProperty(wrapped, "_can", {
|
|
98
|
+
value: item._can,
|
|
99
|
+
enumerable: false,
|
|
100
|
+
writable: false,
|
|
101
|
+
configurable: false
|
|
102
|
+
});
|
|
103
|
+
Object.defineProperty(wrapped, "canRead", {
|
|
104
|
+
value: () => makeResult(can.read ?? false),
|
|
105
|
+
enumerable: false,
|
|
106
|
+
writable: false,
|
|
107
|
+
configurable: false
|
|
108
|
+
});
|
|
109
|
+
Object.defineProperty(wrapped, "canCreate", {
|
|
110
|
+
value: () => makeResult(can.create ?? false),
|
|
111
|
+
enumerable: false,
|
|
112
|
+
writable: false,
|
|
113
|
+
configurable: false
|
|
114
|
+
});
|
|
115
|
+
Object.defineProperty(wrapped, "canEdit", {
|
|
116
|
+
value: () => makeResult(can.update ?? false),
|
|
117
|
+
enumerable: false,
|
|
118
|
+
writable: false,
|
|
119
|
+
configurable: false
|
|
120
|
+
});
|
|
121
|
+
Object.defineProperty(wrapped, "canDelete", {
|
|
122
|
+
value: () => makeResult(can.delete ?? false),
|
|
123
|
+
enumerable: false,
|
|
124
|
+
writable: false,
|
|
125
|
+
configurable: false
|
|
126
|
+
});
|
|
127
|
+
return wrapped;
|
|
128
|
+
}
|
|
129
|
+
function wrapCollection(arr, tablePerms) {
|
|
130
|
+
const wrappedItems = arr.map((item) => {
|
|
131
|
+
if (item && typeof item === "object" && "_can" in item) {
|
|
132
|
+
return wrapItem(item);
|
|
133
|
+
}
|
|
134
|
+
return item;
|
|
135
|
+
});
|
|
136
|
+
const tableName = arr._tableName;
|
|
137
|
+
const perms = tableName ? tablePerms[tableName] : void 0;
|
|
138
|
+
const result = [...wrappedItems];
|
|
139
|
+
if (tableName) {
|
|
140
|
+
Object.defineProperty(result, "_tableName", {
|
|
141
|
+
value: tableName,
|
|
142
|
+
enumerable: false,
|
|
143
|
+
writable: false,
|
|
144
|
+
configurable: false
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
Object.defineProperty(result, "canAdd", {
|
|
148
|
+
value: () => makeResult(perms?.create ?? false),
|
|
149
|
+
enumerable: false,
|
|
150
|
+
writable: false,
|
|
151
|
+
configurable: false
|
|
152
|
+
});
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
function useCfastLoader() {
|
|
156
|
+
const raw = useLoaderData2();
|
|
157
|
+
return useMemo(() => {
|
|
158
|
+
const tablePerms = raw._tablePerms ?? {};
|
|
159
|
+
const result = {};
|
|
160
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
161
|
+
if (key === "_tablePerms") {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
const hasCanItems = value.length > 0 && value[0] !== null && typeof value[0] === "object" && "_can" in value[0];
|
|
166
|
+
if (hasCanItems) {
|
|
167
|
+
result[key] = wrapCollection(value, tablePerms);
|
|
168
|
+
} else {
|
|
169
|
+
result[key] = value;
|
|
170
|
+
}
|
|
171
|
+
} else if (value !== null && typeof value === "object" && !Array.isArray(value) && "_can" in value) {
|
|
172
|
+
result[key] = wrapItem(value);
|
|
173
|
+
} else {
|
|
174
|
+
result[key] = value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}, [raw]);
|
|
179
|
+
}
|
|
180
|
+
|
|
70
181
|
// src/client/action-form.tsx
|
|
71
182
|
import { Form } from "react-router";
|
|
72
183
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -81,5 +192,6 @@ function ActionForm({ action, children, ...formProps }) {
|
|
|
81
192
|
export {
|
|
82
193
|
ActionForm,
|
|
83
194
|
clientDescriptor,
|
|
84
|
-
useActions
|
|
195
|
+
useActions,
|
|
196
|
+
useCfastLoader
|
|
85
197
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, D as DispatchArgs, R as RequestArgs, S as Serializable } from './types-
|
|
1
|
+
import { Grant, PermissionDescriptor, SchemaMap, CrudAction } 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-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
|
/**
|
|
@@ -241,6 +241,41 @@ declare function createActions<TUser = any, TServices extends ActionServices = A
|
|
|
241
241
|
composeActions: <TActions extends Record<string, ActionDefinition<any, any, any>>>(actions: TActions) => ComposedActions<TActions>;
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Server-side loader helper that wraps data with table-level permission
|
|
246
|
+
* metadata and serializes dates.
|
|
247
|
+
*
|
|
248
|
+
* Replaces the manual pattern of calling `can()` per table and `toJSON()`
|
|
249
|
+
* separately. The returned object contains:
|
|
250
|
+
*
|
|
251
|
+
* - All fields from `data`, with Dates converted to ISO strings.
|
|
252
|
+
* - `_tablePerms`: a `Record<tableName, Record<CrudAction, boolean>>` map
|
|
253
|
+
* covering every table in the schema.
|
|
254
|
+
* - Each array value in `data` is annotated with a non-enumerable
|
|
255
|
+
* `_tableName` property matching the first table whose SQL name appears
|
|
256
|
+
* as the data key. This allows the client-side `useCfastLoader` hook to
|
|
257
|
+
* look up `canAdd()` for the correct table.
|
|
258
|
+
*
|
|
259
|
+
* @param grants - The user's resolved permission grants.
|
|
260
|
+
* @param schema - A schema map (e.g. `import * as schema from "./schema"`).
|
|
261
|
+
* @param data - The loader data to wrap.
|
|
262
|
+
* @returns A serializable object with `_tablePerms` embedded.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* import { cfastJson } from "@cfast/actions";
|
|
267
|
+
* import * as schema from "../db/schema";
|
|
268
|
+
*
|
|
269
|
+
* export async function loader({ context }) {
|
|
270
|
+
* const documents = await db.query(documentsTable).findMany().run();
|
|
271
|
+
* return cfastJson(ctx.auth.grants, schema, { documents });
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
declare function cfastJson<TData extends Record<string, unknown>>(grants: Grant[], schema: SchemaMap, data: TData): TData & {
|
|
276
|
+
_tablePerms: Record<string, Record<CrudAction, boolean>>;
|
|
277
|
+
};
|
|
278
|
+
|
|
244
279
|
/**
|
|
245
280
|
* Options accepted by {@link forwardRequest}.
|
|
246
281
|
*
|
|
@@ -328,4 +363,4 @@ type ForwardRequestInit = {
|
|
|
328
363
|
*/
|
|
329
364
|
declare function forwardRequest(original: Request, init?: ForwardRequestInit): Request;
|
|
330
365
|
|
|
331
|
-
export { ActionDefinition, ActionPermissionStatus, ActionServices, ActionsConfig, ComposedActions, type ForwardRequestInit, type InferInput, type InputField, type InputParser, type InputSchema, InvalidInputError, OperationsFn, checkPermissionStatus, createActions, defineInput, forwardRequest, z };
|
|
366
|
+
export { ActionDefinition, ActionPermissionStatus, ActionServices, ActionsConfig, ComposedActions, type ForwardRequestInit, type InferInput, type InputField, type InputParser, type InputSchema, InvalidInputError, OperationsFn, cfastJson, checkPermissionStatus, createActions, defineInput, forwardRequest, z };
|
package/dist/index.js
CHANGED
|
@@ -154,6 +154,51 @@ function createActions(config) {
|
|
|
154
154
|
return { createAction, composeActions };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// src/cfast-json.ts
|
|
158
|
+
import { resolveTablePermissions } from "@cfast/permissions";
|
|
159
|
+
import { toJSON } from "@cfast/db";
|
|
160
|
+
function cfastJson(grants, schema, data) {
|
|
161
|
+
const tablePerms = resolveTablePermissions(grants, schema);
|
|
162
|
+
const serialized = toJSON(data);
|
|
163
|
+
const keyToTableName = {};
|
|
164
|
+
for (const [jsKey, table] of Object.entries(schema)) {
|
|
165
|
+
const DRIZZLE_NAME_SYMBOL = /* @__PURE__ */ Symbol.for("drizzle:Name");
|
|
166
|
+
if (typeof table === "object" && table !== null) {
|
|
167
|
+
const name = Reflect.get(table, DRIZZLE_NAME_SYMBOL);
|
|
168
|
+
if (typeof name === "string") {
|
|
169
|
+
keyToTableName[jsKey] = name;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const [key, value] of Object.entries(serialized)) {
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
let tableName;
|
|
176
|
+
if (keyToTableName[key]) {
|
|
177
|
+
tableName = keyToTableName[key];
|
|
178
|
+
} else {
|
|
179
|
+
for (const sqlName of Object.values(keyToTableName)) {
|
|
180
|
+
if (sqlName === key) {
|
|
181
|
+
tableName = sqlName;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (tableName) {
|
|
187
|
+
Object.defineProperty(value, "_tableName", {
|
|
188
|
+
value: tableName,
|
|
189
|
+
enumerable: false,
|
|
190
|
+
writable: false,
|
|
191
|
+
configurable: false
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
...serialized,
|
|
198
|
+
_tablePerms: tablePerms
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
157
202
|
// src/input-schema.ts
|
|
158
203
|
var InvalidInputError = class extends Error {
|
|
159
204
|
/** Field-keyed map of validation errors. */
|
|
@@ -340,6 +385,7 @@ function forwardRequest(original, init = {}) {
|
|
|
340
385
|
}
|
|
341
386
|
export {
|
|
342
387
|
InvalidInputError,
|
|
388
|
+
cfastJson,
|
|
343
389
|
checkPermissionStatus,
|
|
344
390
|
createActions,
|
|
345
391
|
defineInput,
|
|
@@ -223,11 +223,11 @@ type ActionPermissionsMap = Record<string, ActionPermissionStatus>;
|
|
|
223
223
|
* Created by {@link ActionDefinition.client} or {@link ComposedActions.client}.
|
|
224
224
|
* Contains the action names and the key used to read permission data from loader results.
|
|
225
225
|
*/
|
|
226
|
-
type ClientDescriptor = {
|
|
226
|
+
type ClientDescriptor<TNames extends readonly string[] = readonly string[]> = {
|
|
227
227
|
/** Brand field to distinguish this type at the type level. */
|
|
228
228
|
_brand: "ActionClientDescriptor";
|
|
229
229
|
/** The list of action names this descriptor covers. */
|
|
230
|
-
actionNames:
|
|
230
|
+
actionNames: TNames;
|
|
231
231
|
/** The loader-data key where {@link ActionPermissionsMap} is stored. */
|
|
232
232
|
permissionsKey: string;
|
|
233
233
|
};
|
package/llms.txt
CHANGED
|
@@ -16,6 +16,21 @@ Use this package when you need React Router route actions that automatically che
|
|
|
16
16
|
|
|
17
17
|
### Server (`@cfast/actions`)
|
|
18
18
|
|
|
19
|
+
#### `cfastJson(grants, schema, data)`
|
|
20
|
+
Server-side loader helper that wraps data with table-level permission metadata and serializes dates. Replaces the manual pattern of calling `can()` per table and `toJSON()` separately.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { cfastJson } from "@cfast/actions";
|
|
24
|
+
import * as schema from "../db/schema";
|
|
25
|
+
|
|
26
|
+
export async function loader({ context }) {
|
|
27
|
+
const documents = await db.query(documentsTable).findMany().run();
|
|
28
|
+
return cfastJson(ctx.auth.grants, schema, { documents });
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Returns the data with `_tablePerms` embedded and dates serialized. Arrays whose keys match schema table names are annotated with a non-enumerable `_tableName` property so `useCfastLoader` can resolve `canAdd()` for the correct table.
|
|
33
|
+
|
|
19
34
|
```typescript
|
|
20
35
|
function createActions<TUser>(config: ActionsConfig<TUser>): {
|
|
21
36
|
createAction: <TInput, TResult>(
|
|
@@ -68,9 +83,31 @@ type ActionPermissionsMap = Record<string, ActionPermissionStatus>;
|
|
|
68
83
|
|
|
69
84
|
### Client (`@cfast/actions/client`)
|
|
70
85
|
|
|
86
|
+
#### Type-safe `clientDescriptor()` and `useActions()`
|
|
87
|
+
|
|
88
|
+
`clientDescriptor()` now uses `const` type parameters to infer the literal tuple
|
|
89
|
+
type from the action names array. `useActions()` returns a mapped type keyed by
|
|
90
|
+
those literal names instead of `Record<string, ...>`, giving compile-time
|
|
91
|
+
autocomplete and error checking on action names.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Type-safe: actions.create and actions.delete are typed, typos are caught
|
|
95
|
+
const client = clientDescriptor(["create", "delete"]);
|
|
96
|
+
// client is ClientDescriptor<readonly ["create", "delete"]>
|
|
97
|
+
|
|
98
|
+
const actions = useActions(client);
|
|
99
|
+
// actions is { create: (input?) => ActionHookResult; delete: (input?) => ActionHookResult }
|
|
100
|
+
// actions.creat → TypeScript error (typo caught at compile time)
|
|
101
|
+
```
|
|
102
|
+
|
|
71
103
|
```typescript
|
|
72
|
-
function useActions
|
|
73
|
-
|
|
104
|
+
function useActions<const TNames extends readonly string[]>(
|
|
105
|
+
descriptor: ClientDescriptor<TNames>,
|
|
106
|
+
): { [K in TNames[number]]: (input?: Serializable) => ActionHookResult };
|
|
107
|
+
|
|
108
|
+
function clientDescriptor<const TNames extends readonly string[]>(
|
|
109
|
+
actionNames: TNames,
|
|
110
|
+
): ClientDescriptor<TNames>;
|
|
74
111
|
|
|
75
112
|
function ActionForm(props: ActionFormProps): JSX.Element;
|
|
76
113
|
|
|
@@ -90,6 +127,36 @@ type ActionFormProps = Omit<ComponentProps<typeof Form>, "children" | "action">
|
|
|
90
127
|
};
|
|
91
128
|
```
|
|
92
129
|
|
|
130
|
+
#### `useCfastLoader<T>()`
|
|
131
|
+
Client hook that replaces `useLoaderData()` for routes using `cfastJson()`. Returns the same data but wraps arrays and objects that have `_can` with permission helper methods.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { useCfastLoader } from "@cfast/actions/client";
|
|
135
|
+
|
|
136
|
+
const { documents } = useCfastLoader<typeof loader>();
|
|
137
|
+
|
|
138
|
+
// Collection-level: "can I add to this table?"
|
|
139
|
+
documents.canAdd() // -> ActionHookResult
|
|
140
|
+
|
|
141
|
+
// Row fields accessible directly (no .data wrapper)
|
|
142
|
+
documents[0].title // "Hello World"
|
|
143
|
+
|
|
144
|
+
// Row-level: "can I edit THIS item?"
|
|
145
|
+
documents[0].canEdit() // -> ActionHookResult
|
|
146
|
+
documents[0].canDelete() // -> ActionHookResult
|
|
147
|
+
|
|
148
|
+
// Single items work too
|
|
149
|
+
const { document } = useCfastLoader<typeof loader>();
|
|
150
|
+
document.canEdit() // -> ActionHookResult
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Wrapping rules:
|
|
154
|
+
- Array where items have `_can` -> collection: array + `canAdd()`, each item has `canEdit()`, `canDelete()`, `canRead()`, `canCreate()`
|
|
155
|
+
- Object with `_can` -> item: row properties directly accessible + `can*()` methods
|
|
156
|
+
- Otherwise -> pass through unchanged
|
|
157
|
+
|
|
158
|
+
All `can*()` methods return `ActionHookResult` with `permitted`, `invisible`, `reason`, `submit` (no-op), `pending` (false), `data`, `error`.
|
|
159
|
+
|
|
93
160
|
`ActionForm` is a form wrapper that auto-injects hidden fields from an action descriptor object. Replaces manual `<input type="hidden">` patterns when using `composeActions`.
|
|
94
161
|
|
|
95
162
|
```tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfast/actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.7.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.8.0",
|
|
63
|
+
"@cfast/permissions": "0.7.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsup src/index.ts src/client.ts --format esm --dts",
|