@kellanjs/actioncraft 0.1.0 → 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/README.md +411 -302
- package/dist/actioncraft-error.d.ts +23 -0
- package/dist/actioncraft-error.js +60 -0
- package/dist/actioncraft-error.js.map +1 -0
- package/dist/actioncraft-prev.d.ts +93 -0
- package/dist/actioncraft-prev.js +387 -0
- package/dist/actioncraft-prev.js.map +1 -0
- package/dist/actioncraft.d.ts +94 -44
- package/dist/actioncraft.js +281 -55
- package/dist/actioncraft.js.map +1 -1
- package/dist/api.d.ts +49 -0
- package/dist/api.js +84 -0
- package/dist/api.js.map +1 -0
- package/dist/classes/action-builder.d.ts +59 -0
- package/dist/classes/action-builder.js +95 -0
- package/dist/classes/action-builder.js.map +1 -0
- package/dist/classes/craft-builder.d.ts +66 -0
- package/dist/classes/craft-builder.js +129 -0
- package/dist/classes/craft-builder.js.map +1 -0
- package/dist/classes/crafter.d.ts +66 -0
- package/dist/classes/crafter.js +129 -0
- package/dist/classes/crafter.js.map +1 -0
- package/dist/classes/error.d.ts +23 -0
- package/dist/classes/error.js +60 -0
- package/dist/classes/error.js.map +1 -0
- package/dist/classes/executor/callbacks.d.ts +6 -0
- package/dist/classes/executor/callbacks.js +20 -0
- package/dist/classes/executor/callbacks.js.map +1 -0
- package/dist/classes/executor/errors.d.ts +29 -0
- package/dist/classes/executor/errors.js +114 -0
- package/dist/classes/executor/errors.js.map +1 -0
- package/dist/classes/executor/executor.d.ts +68 -0
- package/dist/classes/executor/executor.js +391 -0
- package/dist/classes/executor/executor.js.map +1 -0
- package/dist/classes/executor/logging.d.ts +2 -0
- package/dist/classes/executor/logging.js +8 -0
- package/dist/classes/executor/logging.js.map +1 -0
- package/dist/classes/executor/transformation.d.ts +17 -0
- package/dist/classes/executor/transformation.js +43 -0
- package/dist/classes/executor/transformation.js.map +1 -0
- package/dist/classes/executor/validation.d.ts +16 -0
- package/dist/classes/executor/validation.js +70 -0
- package/dist/classes/executor/validation.js.map +1 -0
- package/dist/classes/executor.d.ts +64 -0
- package/dist/classes/executor.js +354 -0
- package/dist/classes/executor.js.map +1 -0
- package/dist/classes/internal.d.ts +10 -0
- package/dist/classes/internal.js +5 -0
- package/dist/classes/internal.js.map +1 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/errors.js +5 -5
- package/dist/core/errors.js.map +1 -1
- package/dist/core/logging.d.ts +1 -1
- package/dist/core/transformation.d.ts +2 -2
- package/dist/core/validation.d.ts +4 -4
- package/dist/core/validation.js +14 -14
- package/dist/core/validation.js.map +1 -1
- package/dist/craft.d.ts +29 -0
- package/dist/craft.js +62 -0
- package/dist/craft.js.map +1 -0
- package/dist/error.d.ts +21 -6
- package/dist/error.js +59 -10
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/initial.d.ts +14 -0
- package/dist/initial.js +47 -0
- package/dist/initial.js.map +1 -0
- package/dist/types/actions.d.ts +67 -25
- package/dist/types/builder.d.ts +92 -0
- package/dist/types/builder.js +2 -0
- package/dist/types/builder.js.map +1 -0
- package/dist/types/crafter.d.ts +87 -0
- package/dist/types/crafter.js +2 -0
- package/dist/types/crafter.js.map +1 -0
- package/dist/types/errors.d.ts +25 -17
- package/dist/types/inference.d.ts +41 -8
- package/dist/types/result.d.ts +8 -14
- package/dist/types/result.js +36 -4
- package/dist/types/result.js.map +1 -1
- package/dist/types/schemas.d.ts +7 -7
- package/dist/types/shared.d.ts +14 -6
- package/dist/utils.d.ts +30 -6
- package/dist/utils.js +68 -8
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
⚠️🚧 The library hasn't reached a stable release yet. Expect bugs and potentially breaking API changes until then.
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Actioncraft
|
|
4
4
|
|
|
5
5
|
Streamline your server actions.
|
|
6
6
|
|
|
@@ -21,24 +21,25 @@ Streamline your server actions.
|
|
|
21
21
|
- [Example](#example)
|
|
22
22
|
- [Result Format](#result-format)
|
|
23
23
|
- [Walkthrough](#walkthrough)
|
|
24
|
-
- [.
|
|
24
|
+
- [.config() - Configure Your Action](#config)
|
|
25
25
|
- [.schemas() - Add Validation](#schemas)
|
|
26
26
|
- [.errors() - Define Custom Errors](#errors)
|
|
27
|
-
- [.
|
|
27
|
+
- [.handler() - Implement Business Logic](#handler)
|
|
28
28
|
- [.callbacks() - Add Lifecycle Hooks](#callbacks)
|
|
29
|
-
- [.craft() - Build Your Action](#craft)
|
|
30
29
|
- [Using Your Actions](#using-your-actions)
|
|
31
30
|
- [Basic Usage](#basic-usage)
|
|
32
31
|
- [Error Handling](#error-handling)
|
|
33
32
|
- [React Forms with useActionState](#react-forms-with-useactionstate)
|
|
34
33
|
- [Progressive Enhancement](#progressive-enhancement)
|
|
35
34
|
- [Complete Example](#complete-example)
|
|
36
|
-
- [Integrations](#integrations)
|
|
37
|
-
- [Utilities](#utilities)
|
|
38
|
-
- [React Query](#react-query)
|
|
39
35
|
- [Advanced Features](#advanced-features)
|
|
40
36
|
- [Bind Arguments](#bind-arguments)
|
|
41
|
-
- [
|
|
37
|
+
- [Utilities](#utilities)
|
|
38
|
+
- [Type Inference](#type-inference)
|
|
39
|
+
- [Input Validation](#input-validation)
|
|
40
|
+
- [Integration Utilities](#integration-utilities)
|
|
41
|
+
- [Actioncraft Errors](#actioncraft-errors)
|
|
42
|
+
- [React Query](#react-query)
|
|
42
43
|
|
|
43
44
|
## Quick Start
|
|
44
45
|
|
|
@@ -50,18 +51,40 @@ npm install @kellanjs/actioncraft
|
|
|
50
51
|
|
|
51
52
|
### Overview
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Actioncraft makes it easy to create type-safe server actions with first-class error-handling support.
|
|
55
|
+
|
|
56
|
+
The library supports two different syntax patterns, aptly referred to as the `action() api` and the `craft() api`. Both are functionally the same, so use whichever you prefer!
|
|
57
|
+
|
|
58
|
+
For the sake of simplicity, this document will use the same pattern (the `action() api`) for all usage examples, but either pattern would produce the exact same result.
|
|
59
|
+
|
|
60
|
+
#### action() api
|
|
54
61
|
|
|
55
62
|
```typescript
|
|
56
|
-
const
|
|
63
|
+
export const example = action() // We call action() first to create a builder to use
|
|
64
|
+
.config(...)
|
|
57
65
|
.schemas(...)
|
|
58
66
|
.errors(...)
|
|
59
|
-
.
|
|
67
|
+
.handler(...)
|
|
60
68
|
.callbacks(...)
|
|
61
|
-
.craft();
|
|
69
|
+
.craft(); // And we call craft() last to build and return your type-safe server action
|
|
62
70
|
```
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
#### craft() api
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// We call craft() first, and it provides us with a builder to use
|
|
76
|
+
export const example = craft(async (action) =>
|
|
77
|
+
action
|
|
78
|
+
.config(...)
|
|
79
|
+
.schemas(...)
|
|
80
|
+
.errors(...)
|
|
81
|
+
.handler(...)
|
|
82
|
+
.callbacks(...)
|
|
83
|
+
// No craft() needed here, because it's already wrapping everything!
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Actioncraft uses a fluent builder design, making it simple to chain one method after the next to create a full-featured server action. Regardless of which syntax pattern you use, the order in which the methods are defined is important for type inference to work properly, so you'll see the same structure repeated often throughout the documentation. Always make sure to chain your methods together like this for the best experience!
|
|
65
88
|
|
|
66
89
|
### Example
|
|
67
90
|
|
|
@@ -70,7 +93,7 @@ With this basic structure in mind, let's see what a more detailed example looks
|
|
|
70
93
|
```typescript
|
|
71
94
|
"use server";
|
|
72
95
|
|
|
73
|
-
import {
|
|
96
|
+
import { action } from "@kellanjs/actioncraft";
|
|
74
97
|
import { z } from "zod";
|
|
75
98
|
|
|
76
99
|
const newUserInputSchema = z.object({
|
|
@@ -79,7 +102,11 @@ const newUserInputSchema = z.object({
|
|
|
79
102
|
age: z.number(),
|
|
80
103
|
});
|
|
81
104
|
|
|
82
|
-
export const createNewUser =
|
|
105
|
+
export const createNewUser = action()
|
|
106
|
+
// Define configuration settings
|
|
107
|
+
.config({
|
|
108
|
+
validationErrorFormat: "nested",
|
|
109
|
+
})
|
|
83
110
|
// Define the validation schema
|
|
84
111
|
.schemas({
|
|
85
112
|
inputSchema: newUserInputSchema,
|
|
@@ -99,7 +126,7 @@ export const createNewUser = create()
|
|
|
99
126
|
}) as const,
|
|
100
127
|
})
|
|
101
128
|
// Define your server action logic
|
|
102
|
-
.
|
|
129
|
+
.handler(async ({ input, errors }) => {
|
|
103
130
|
// These are your validated input values
|
|
104
131
|
const { name, email, age } = input;
|
|
105
132
|
|
|
@@ -112,13 +139,12 @@ export const createNewUser = create()
|
|
|
112
139
|
|
|
113
140
|
return { newUser };
|
|
114
141
|
})
|
|
115
|
-
// Define lifecycle callbacks
|
|
142
|
+
// Define lifecycle callbacks
|
|
116
143
|
.callbacks({
|
|
117
|
-
onSettled: (result) => {
|
|
144
|
+
onSettled: ({ result }) => {
|
|
118
145
|
// Log what happened if you want
|
|
119
146
|
},
|
|
120
147
|
})
|
|
121
|
-
// Finally, build the full type-safe action
|
|
122
148
|
.craft();
|
|
123
149
|
```
|
|
124
150
|
|
|
@@ -126,7 +152,7 @@ export const createNewUser = create()
|
|
|
126
152
|
|
|
127
153
|
Server actions work best when you're returning serializable data. Throwing errors is less effective in this context, because Next.js will sanitize Error class objects that are thrown in your action, leaving you without useful error information on the client. You might see something in development, but in production, if you try to display `error.message`, you'll likely see something along the lines of: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details."
|
|
128
154
|
|
|
129
|
-
|
|
155
|
+
Actioncraft was designed with this fundamental behavior in mind! Instead of throwing errors, we're working exclusively with structured, serializable objects every step of the way, so errors in your action will always return the data you need.
|
|
130
156
|
|
|
131
157
|
The default result format should feel pretty familiar: `{ success: true, data: T } | { success: false, error: E }`
|
|
132
158
|
|
|
@@ -141,7 +167,7 @@ const handleCreateNewUser = async (userData) => {
|
|
|
141
167
|
// If the action was successful, then you get back typed return data
|
|
142
168
|
toast.success("User created:", result.data.newUser);
|
|
143
169
|
} else {
|
|
144
|
-
// If the action was unsuccessful, then you get
|
|
170
|
+
// If the action was unsuccessful, then you get fully typed error handling
|
|
145
171
|
switch (result.error.type) {
|
|
146
172
|
case "INPUT_VALIDATION":
|
|
147
173
|
handleInputValidationErrorLogic();
|
|
@@ -162,71 +188,81 @@ const handleCreateNewUser = async (userData) => {
|
|
|
162
188
|
|
|
163
189
|
## Walkthrough
|
|
164
190
|
|
|
165
|
-
Now that we've covered the basic structure of an action and looked at a simple example, let's take a more detailed look at
|
|
191
|
+
Now that we've covered the basic structure of an action and looked at a simple example, let's take a more detailed look at how Actioncraft works and what you can do with it.
|
|
166
192
|
|
|
167
|
-
### .
|
|
193
|
+
### .config()
|
|
168
194
|
|
|
169
|
-
|
|
195
|
+
Actioncraft provides several configuration options to customize your action. Sensible defaults are provided, so you only need to define the `config` if you specifically want to override something. When you want to customize a certain behavior, just pass a configuration object:
|
|
170
196
|
|
|
171
197
|
```typescript
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
export const getUser = action()
|
|
199
|
+
.config({
|
|
200
|
+
name: "getUser",
|
|
201
|
+
useActionState: true,
|
|
202
|
+
resultFormat: "functional",
|
|
203
|
+
validationErrorFormat: "nested",
|
|
204
|
+
handleThrownError: (error) => ({
|
|
205
|
+
type: "CUSTOM_ERROR",
|
|
206
|
+
message: error.message,
|
|
207
|
+
}) as const,
|
|
208
|
+
})
|
|
174
209
|
.schemas(...)
|
|
175
210
|
.errors(...)
|
|
176
|
-
.
|
|
211
|
+
.handler(...)
|
|
177
212
|
.callbacks(...)
|
|
178
213
|
.craft();
|
|
179
214
|
```
|
|
180
215
|
|
|
181
|
-
####
|
|
216
|
+
#### `name: string`
|
|
182
217
|
|
|
183
|
-
|
|
218
|
+
**Default:** `undefined`
|
|
219
|
+
|
|
220
|
+
An optional identifier for your action that will be included in error messages to help with debugging:
|
|
184
221
|
|
|
185
222
|
```typescript
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
type: "CUSTOM_ERROR",
|
|
192
|
-
message: error.message,
|
|
193
|
-
}) as const,
|
|
223
|
+
export const updateUserProfile = action()
|
|
224
|
+
.config({ name: "updateUserProfile" })
|
|
225
|
+
.schemas({ inputSchema: userSchema })
|
|
226
|
+
.handler(async ({ input }) => {
|
|
227
|
+
// Your handler logic
|
|
194
228
|
})
|
|
195
|
-
.schemas(...)
|
|
196
|
-
.errors(...)
|
|
197
|
-
.action(...)
|
|
198
|
-
.callbacks(...)
|
|
199
229
|
.craft();
|
|
230
|
+
|
|
231
|
+
// If validation fails, the error message will be:
|
|
232
|
+
// "Input validation failed in action \"updateUserProfile\""
|
|
233
|
+
// instead of just:
|
|
234
|
+
// "Input validation failed"
|
|
200
235
|
```
|
|
201
236
|
|
|
202
|
-
|
|
237
|
+
#### `useActionState: boolean`
|
|
203
238
|
|
|
204
239
|
**Default:** `false`
|
|
205
240
|
|
|
206
241
|
Set to `true` to make your action compatible with React's `useActionState` hook:
|
|
207
242
|
|
|
208
243
|
```typescript
|
|
209
|
-
const
|
|
244
|
+
export const getUser = action()
|
|
245
|
+
.config({ useActionState: true })
|
|
210
246
|
.schemas(...)
|
|
211
247
|
.errors(...)
|
|
212
|
-
.
|
|
248
|
+
.handler(...)
|
|
213
249
|
.callbacks(...)
|
|
214
250
|
.craft();
|
|
215
251
|
|
|
216
|
-
// Now you can use it with useActionState like this:
|
|
217
|
-
const [state,
|
|
252
|
+
// Now you can use it with useActionState in your client components like this:
|
|
253
|
+
const [state, action] = useActionState(getUser, initial(getUser));
|
|
218
254
|
```
|
|
219
255
|
|
|
220
|
-
|
|
256
|
+
#### `resultFormat: "api" | "functional"`
|
|
221
257
|
|
|
222
258
|
**Default:** `"api"`
|
|
223
259
|
|
|
224
|
-
|
|
260
|
+
Actioncraft supports two different return formats:
|
|
225
261
|
|
|
226
262
|
- **`"api"`**: `{ success: true, data: T } | { success: false, error: E }`
|
|
227
263
|
- **`"functional"`**: `{ type: "ok", value: T } | { type: "err", error: E }`
|
|
228
264
|
|
|
229
|
-
|
|
265
|
+
#### `validationErrorFormat: "flattened" | "nested"`
|
|
230
266
|
|
|
231
267
|
**Default:** `"flattened"`
|
|
232
268
|
|
|
@@ -235,19 +271,26 @@ Controls how validation errors are structured:
|
|
|
235
271
|
- **`"flattened"`**: Returns a flat array of error messages
|
|
236
272
|
- **`"nested"`**: Returns a nested object matching your schema structure
|
|
237
273
|
|
|
238
|
-
|
|
274
|
+
#### `handleThrownError: (error: unknown) => UserDefinedError`
|
|
239
275
|
|
|
240
|
-
By default,
|
|
276
|
+
By default, Actioncraft catches thrown errors and returns a structured error with type `"UNHANDLED"`. You can customize this behavior by passing an error handler function of your own:
|
|
241
277
|
|
|
242
278
|
```typescript
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
279
|
+
export const getUser = action()
|
|
280
|
+
.config({
|
|
281
|
+
handleThrownError: (error) =>
|
|
282
|
+
({
|
|
283
|
+
type: "CUSTOM_ERROR",
|
|
284
|
+
message:
|
|
285
|
+
error instanceof Error ? error.message : "Something went wrong",
|
|
286
|
+
timestamp: new Date().toISOString(),
|
|
287
|
+
}) as const,
|
|
288
|
+
})
|
|
289
|
+
.schemas(...)
|
|
290
|
+
.errors(...)
|
|
291
|
+
.handler(...)
|
|
292
|
+
.callbacks(...)
|
|
293
|
+
.craft();
|
|
251
294
|
```
|
|
252
295
|
|
|
253
296
|
You can even implement more complex logic if you want:
|
|
@@ -296,7 +339,7 @@ handleThrownError: (error: unknown) => {
|
|
|
296
339
|
};
|
|
297
340
|
```
|
|
298
341
|
|
|
299
|
-
|
|
342
|
+
Actioncraft's types are smart enough to infer all of these possibilities back on the client:
|
|
300
343
|
|
|
301
344
|
```typescript
|
|
302
345
|
if (!result.success) {
|
|
@@ -309,17 +352,18 @@ Pretty cool!
|
|
|
309
352
|
|
|
310
353
|
### .schemas()
|
|
311
354
|
|
|
312
|
-
With our action configured, let's add validation using schemas.
|
|
355
|
+
With our action configured, let's add validation using schemas. Actioncraft supports any library that implements the **Standard Schema V1** interface. Validation is handled automatically - you just need to provide the schemas:
|
|
313
356
|
|
|
314
357
|
```typescript
|
|
315
|
-
const
|
|
358
|
+
export const getUser = action()
|
|
359
|
+
.config(...)
|
|
316
360
|
.schemas({
|
|
317
361
|
inputSchema,
|
|
318
362
|
outputSchema,
|
|
319
363
|
bindSchemas,
|
|
320
364
|
})
|
|
321
365
|
.errors(...)
|
|
322
|
-
.
|
|
366
|
+
.handler(...)
|
|
323
367
|
.callbacks(...)
|
|
324
368
|
.craft();
|
|
325
369
|
```
|
|
@@ -340,10 +384,11 @@ Validates arguments bound to the action with `.bind()`. If validation fails, a "
|
|
|
340
384
|
|
|
341
385
|
### .errors()
|
|
342
386
|
|
|
343
|
-
Now that we have validation set up, let's define custom errors that our action can return.
|
|
387
|
+
Now that we have validation set up, let's define custom errors that our action can return. Actioncraft makes error handling really easy by letting you define structured error types:
|
|
344
388
|
|
|
345
389
|
```typescript
|
|
346
|
-
const
|
|
390
|
+
export const errorExamples = action()
|
|
391
|
+
.config(...)
|
|
347
392
|
.schemas(...)
|
|
348
393
|
.errors({
|
|
349
394
|
unauthorized: () =>
|
|
@@ -364,7 +409,7 @@ const action = create()
|
|
|
364
409
|
email,
|
|
365
410
|
}) as const,
|
|
366
411
|
})
|
|
367
|
-
.
|
|
412
|
+
.handler(...)
|
|
368
413
|
.callbacks(...)
|
|
369
414
|
.craft();
|
|
370
415
|
```
|
|
@@ -384,14 +429,16 @@ Each error is defined as a function called an **ErrorDefinition**:
|
|
|
384
429
|
The `as const` assertion is **required** for proper TypeScript inference. It ensures your error types are treated as literal types rather than generic:
|
|
385
430
|
|
|
386
431
|
```typescript
|
|
387
|
-
// ❌ Without 'as const' - TypeScript infers { type: string, message: string }
|
|
432
|
+
// ❌ Without 'as const' - TypeScript infers { type: string, message: string } :(
|
|
388
433
|
badErrorDefinition: () => ({ type: "ERROR", message: "Something went wrong" });
|
|
389
434
|
|
|
390
|
-
// ✅ With 'as const' - TypeScript infers { type: "ERROR", message: "Something went wrong" }
|
|
435
|
+
// ✅ With 'as const' - TypeScript infers { type: "ERROR", message: "Something went wrong" } :D
|
|
391
436
|
goodErrorDefinition: () =>
|
|
392
437
|
({ type: "ERROR", message: "Something went wrong" }) as const;
|
|
393
438
|
```
|
|
394
439
|
|
|
440
|
+
Always remember the `as const` assertion when you define your errors!
|
|
441
|
+
|
|
395
442
|
#### Reusing Common Errors
|
|
396
443
|
|
|
397
444
|
Since error definitions are just functions, you can easily share common errors between actions:
|
|
@@ -420,32 +467,34 @@ export const notFound = (resource: string, id: string) =>
|
|
|
420
467
|
```
|
|
421
468
|
|
|
422
469
|
```typescript
|
|
423
|
-
//
|
|
424
|
-
export const
|
|
470
|
+
// get-user.ts
|
|
471
|
+
export const getUser = action()
|
|
472
|
+
.config(...)
|
|
425
473
|
.schemas(...)
|
|
426
474
|
.errors({
|
|
427
475
|
// Easily use common shared errors
|
|
428
476
|
unauthorized,
|
|
429
477
|
rateLimited,
|
|
430
478
|
notFound,
|
|
431
|
-
// Plus any action-specific errors
|
|
479
|
+
// Plus any action-specific errors you need
|
|
432
480
|
emailTaken: (email: string) =>
|
|
433
481
|
({ type: "EMAIL_TAKEN", email }) as const,
|
|
434
482
|
})
|
|
435
|
-
.
|
|
483
|
+
.handler(...)
|
|
436
484
|
.callbacks(...)
|
|
437
485
|
.craft();
|
|
438
486
|
```
|
|
439
487
|
|
|
440
|
-
#### Using Errors in Your Action
|
|
488
|
+
#### Using Errors in Your Action Handler
|
|
441
489
|
|
|
442
|
-
Once defined, you can use these errors in your
|
|
490
|
+
Once defined, you can use these errors in your handler logic. When an error occurs, just call and return that particular error function:
|
|
443
491
|
|
|
444
492
|
```typescript
|
|
445
|
-
const
|
|
493
|
+
export const getUser = action()
|
|
494
|
+
.config(...)
|
|
446
495
|
.schemas(...)
|
|
447
496
|
.errors(...)
|
|
448
|
-
.
|
|
497
|
+
.handler(async ({ input, errors }) => {
|
|
449
498
|
// Check permissions
|
|
450
499
|
if (!hasPermission(input.userId)) {
|
|
451
500
|
return errors.unauthorized();
|
|
@@ -464,22 +513,23 @@ const action = create()
|
|
|
464
513
|
.craft();
|
|
465
514
|
```
|
|
466
515
|
|
|
467
|
-
### .
|
|
516
|
+
### .handler()
|
|
468
517
|
|
|
469
|
-
The `
|
|
518
|
+
The `handler` method is where you implement the core functionality of your server action. Actioncraft provides several helpful parameters to make things quick and easy for you:
|
|
470
519
|
|
|
471
520
|
```typescript
|
|
472
|
-
const
|
|
521
|
+
export const getUser = action()
|
|
522
|
+
.config(...)
|
|
473
523
|
.schemas(...)
|
|
474
524
|
.errors(...)
|
|
475
|
-
.
|
|
476
|
-
//
|
|
525
|
+
.handler(async ({ input, bindArgs, errors, metadata }) => {
|
|
526
|
+
// Server action logic here
|
|
477
527
|
})
|
|
478
528
|
.callbacks(...)
|
|
479
529
|
.craft();
|
|
480
530
|
```
|
|
481
531
|
|
|
482
|
-
####
|
|
532
|
+
#### Handler Parameters
|
|
483
533
|
|
|
484
534
|
##### `input`
|
|
485
535
|
|
|
@@ -500,16 +550,18 @@ Contains additional request information:
|
|
|
500
550
|
- `rawInput`: The original, unvalidated input data
|
|
501
551
|
- `rawBindArgs`: The original, unvalidated bound arguments array
|
|
502
552
|
- `prevState`: Previous state (when using `useActionState`)
|
|
553
|
+
- `actionId`: A unique identifier for the action instance
|
|
503
554
|
|
|
504
555
|
### .callbacks()
|
|
505
556
|
|
|
506
557
|
Sometimes you need to hook into the action lifecycle for logging, analytics, or other side effects. The `callbacks` method lets you define functions that run at key moments:
|
|
507
558
|
|
|
508
559
|
```typescript
|
|
509
|
-
const
|
|
560
|
+
export const getUser = action()
|
|
561
|
+
.config(...)
|
|
510
562
|
.schemas(...)
|
|
511
563
|
.errors(...)
|
|
512
|
-
.
|
|
564
|
+
.handler(...)
|
|
513
565
|
.callbacks({
|
|
514
566
|
onStart: ({metadata}) => { ... },
|
|
515
567
|
onSuccess: ({data}) => { ... },
|
|
@@ -527,7 +579,7 @@ Executes first, before any validation or action logic has occurred.
|
|
|
527
579
|
|
|
528
580
|
##### `onSuccess?: (params: { data, metadata }) => Promise<void> | void`
|
|
529
581
|
|
|
530
|
-
Executes when your action completes successfully. The `data` parameter contains your action's return value.
|
|
582
|
+
Executes when your action completes successfully. The `data` parameter contains your action's typed return value.
|
|
531
583
|
|
|
532
584
|
##### `onError?: (params: { error, metadata }) => Promise<void> | void`
|
|
533
585
|
|
|
@@ -539,44 +591,32 @@ Executes after your action completes, regardless of success or failure. Useful f
|
|
|
539
591
|
|
|
540
592
|
Note: All callback methods support async operations and won't affect your action's result, even if they throw errors.
|
|
541
593
|
|
|
542
|
-
### .craft()
|
|
543
|
-
|
|
544
|
-
Every action should end with the `craft` method. It doesn't take any arguments or anything like that. Its only purpose is to build the final action based on everything that has been defined in the previous methods.
|
|
545
|
-
|
|
546
|
-
```typescript
|
|
547
|
-
const action = create()
|
|
548
|
-
.schemas(...)
|
|
549
|
-
.errors(...)
|
|
550
|
-
.action(...)
|
|
551
|
-
.callbacks(...)
|
|
552
|
-
.craft(); // Returns your fully-typed server action
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
This is the final step in the builder chain. Once you call `craft()`, you have a complete, type-safe server action ready to export and use in your application.
|
|
556
|
-
|
|
557
594
|
## Using Your Actions
|
|
558
595
|
|
|
559
|
-
Now that you know how to build actions with
|
|
596
|
+
Now that you know how to build actions with Actioncraft, let's see how you can use them in your application.
|
|
560
597
|
|
|
561
598
|
### Basic Usage
|
|
562
599
|
|
|
563
600
|
You can call your action like any async function:
|
|
564
601
|
|
|
565
602
|
```typescript
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
603
|
+
// client-component.ts
|
|
604
|
+
const handleClick = async () => {
|
|
605
|
+
const result = await createNewUser({
|
|
606
|
+
name: "John",
|
|
607
|
+
email: "john@example.com",
|
|
608
|
+
age: 25,
|
|
609
|
+
});
|
|
571
610
|
|
|
572
|
-
if (result.success) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
} else {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
611
|
+
if (result.success) {
|
|
612
|
+
// Action succeeded
|
|
613
|
+
console.log("User created:", result.data.newUser);
|
|
614
|
+
} else {
|
|
615
|
+
// Action failed
|
|
616
|
+
console.log("Error:", result.error.type);
|
|
617
|
+
console.log("Message:", result.error.message);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
580
620
|
```
|
|
581
621
|
|
|
582
622
|
### Error Handling
|
|
@@ -609,10 +649,11 @@ if (!result.success) {
|
|
|
609
649
|
For React forms, you can use actions configured for `useActionState`:
|
|
610
650
|
|
|
611
651
|
```typescript
|
|
612
|
-
const updateUser =
|
|
652
|
+
export const updateUser = action()
|
|
653
|
+
.config({ useActionState: true })
|
|
613
654
|
.schemas(...)
|
|
614
655
|
.errors(...)
|
|
615
|
-
.
|
|
656
|
+
.handler(...)
|
|
616
657
|
.callbacks(...)
|
|
617
658
|
.craft();
|
|
618
659
|
```
|
|
@@ -621,7 +662,7 @@ When `useActionState: true` is set, your action's return type changes to include
|
|
|
621
662
|
|
|
622
663
|
#### The `initial()` Helper
|
|
623
664
|
|
|
624
|
-
When using `useActionState`, you have to provide the hook with a proper initial state that matches the return type of your action. That's where
|
|
665
|
+
When using `useActionState`, you have to provide the hook with a proper initial state that matches the return type of your action. That's where Actioncraft's `initial` function comes in. It returns a special error object with type `"INITIAL_STATE"` that you can use to detect when the form hasn't been submitted yet:
|
|
625
666
|
|
|
626
667
|
```typescript
|
|
627
668
|
function UserForm() {
|
|
@@ -652,14 +693,15 @@ By providing a schema which supports FormData, your action can work with or with
|
|
|
652
693
|
|
|
653
694
|
```typescript
|
|
654
695
|
// This action handles FormData from server-side form submissions
|
|
655
|
-
const
|
|
696
|
+
export const createNewUser = action()
|
|
697
|
+
.config({ useActionState: true })
|
|
656
698
|
.schemas({
|
|
657
699
|
inputSchema: zfd.formData({
|
|
658
700
|
name: zfd.text(),
|
|
659
701
|
email: zfd.text(z.string().email()),
|
|
660
702
|
}),
|
|
661
703
|
})
|
|
662
|
-
.
|
|
704
|
+
.handler(async ({ input }) => {
|
|
663
705
|
// Save the validated user data to database
|
|
664
706
|
const user = await db.user.create({
|
|
665
707
|
data: {
|
|
@@ -678,12 +720,12 @@ const serverAction = create({ useActionState: true })
|
|
|
678
720
|
|
|
679
721
|
## Complete Example
|
|
680
722
|
|
|
681
|
-
Now that we've gone over how to create actions and how to use them on the client, let's check out
|
|
723
|
+
Now that we've gone over how to create actions and how to use them on the client, let's check out a more thorough example that puts a lot of these ideas together:
|
|
682
724
|
|
|
683
725
|
```typescript
|
|
684
726
|
"use server";
|
|
685
727
|
|
|
686
|
-
import {
|
|
728
|
+
import { action } from "@kellanjs/actioncraft";
|
|
687
729
|
import { revalidatePath } from "next/cache";
|
|
688
730
|
import { z } from "zod";
|
|
689
731
|
|
|
@@ -693,7 +735,8 @@ const updateProfileSchema = z.object({
|
|
|
693
735
|
bio: z.string().max(500, "Bio must be under 500 characters"),
|
|
694
736
|
});
|
|
695
737
|
|
|
696
|
-
export const updateProfile =
|
|
738
|
+
export const updateProfile = action()
|
|
739
|
+
.config({ useActionState: true })
|
|
697
740
|
.schemas({ inputSchema: updateProfileSchema })
|
|
698
741
|
.errors({
|
|
699
742
|
unauthorized: () =>
|
|
@@ -710,7 +753,7 @@ export const updateProfile = create({ useActionState: true })
|
|
|
710
753
|
message: "Too many requests. Please try again later.",
|
|
711
754
|
}) as const,
|
|
712
755
|
})
|
|
713
|
-
.
|
|
756
|
+
.handler(async ({ input, errors }) => {
|
|
714
757
|
// Check authentication
|
|
715
758
|
const session = await getSession();
|
|
716
759
|
if (!session) return errors.unauthorized();
|
|
@@ -823,64 +866,282 @@ export default function ProfileForm() {
|
|
|
823
866
|
}
|
|
824
867
|
```
|
|
825
868
|
|
|
826
|
-
##
|
|
869
|
+
## Advanced Features
|
|
870
|
+
|
|
871
|
+
### Bind Arguments
|
|
872
|
+
|
|
873
|
+
Actioncraft supports binding arguments to actions. Just provide schemas, and you'll get the validated bindArgs values to use in the action handler.
|
|
874
|
+
|
|
875
|
+
If validation fails, an error with type `BIND_ARGS_VALIDATION` is returned to the client.
|
|
876
|
+
|
|
877
|
+
#### Example: Multi-Tenant Action
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
export const createPost = action()
|
|
881
|
+
.schemas({
|
|
882
|
+
bindSchemas: [z.string()], // Organization ID
|
|
883
|
+
inputSchema: z.object({
|
|
884
|
+
title: z.string(),
|
|
885
|
+
content: z.string(),
|
|
886
|
+
}),
|
|
887
|
+
})
|
|
888
|
+
.handler(async ({ bindArgs, input }) => {
|
|
889
|
+
const [organizationId] = bindArgs;
|
|
890
|
+
|
|
891
|
+
const post = await db.post.create({
|
|
892
|
+
data: {
|
|
893
|
+
...input,
|
|
894
|
+
organizationId,
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
return { post };
|
|
899
|
+
})
|
|
900
|
+
.craft();
|
|
901
|
+
|
|
902
|
+
// Create organization-specific actions
|
|
903
|
+
const createPostForOrgA = createPost.bind(null, "org-a-id");
|
|
904
|
+
const createPostForOrgB = createPost.bind(null, "org-b-id");
|
|
905
|
+
|
|
906
|
+
// Each bound action automatically includes the correct org ID
|
|
907
|
+
const result = await createPostForOrgA({
|
|
908
|
+
title: "My Post",
|
|
909
|
+
content: "Post content...",
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
#### Example: Configuration Binding
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
export const sendEmail = action()
|
|
917
|
+
.schemas({
|
|
918
|
+
bindSchemas: [
|
|
919
|
+
z.object({
|
|
920
|
+
apiKey: z.string(),
|
|
921
|
+
fromEmail: z.string(),
|
|
922
|
+
}),
|
|
923
|
+
],
|
|
924
|
+
inputSchema: z.object({
|
|
925
|
+
to: z.string().email(),
|
|
926
|
+
subject: z.string(),
|
|
927
|
+
body: z.string(),
|
|
928
|
+
}),
|
|
929
|
+
})
|
|
930
|
+
.handler(async ({ bindArgs, input }) => {
|
|
931
|
+
const [config] = bindArgs;
|
|
932
|
+
|
|
933
|
+
// Use the bound configuration
|
|
934
|
+
const emailService = new EmailService(config.apiKey);
|
|
935
|
+
const result = await emailService.send({
|
|
936
|
+
from: config.fromEmail,
|
|
937
|
+
to: input.to,
|
|
938
|
+
subject: input.subject,
|
|
939
|
+
body: input.body,
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
return { messageId: result.id };
|
|
943
|
+
})
|
|
944
|
+
.craft();
|
|
945
|
+
|
|
946
|
+
// Create environment-specific email actions
|
|
947
|
+
const sendProductionEmail = sendEmail.bind(null, {
|
|
948
|
+
apiKey: process.env.PROD_EMAIL_API_KEY,
|
|
949
|
+
fromEmail: "noreply@company.com",
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
const sendDevelopmentEmail = sendEmail.bind(null, {
|
|
953
|
+
apiKey: process.env.DEV_EMAIL_API_KEY,
|
|
954
|
+
fromEmail: "dev@company.com",
|
|
955
|
+
});
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
## Utilities
|
|
959
|
+
|
|
960
|
+
Actioncraft provides several utilities to help you work with your actions more effectively.
|
|
827
961
|
|
|
828
|
-
###
|
|
962
|
+
### Type Inference
|
|
829
963
|
|
|
830
|
-
|
|
964
|
+
These utilities extract useful type information from your actions.
|
|
831
965
|
|
|
832
|
-
#### `
|
|
966
|
+
#### Using `$Infer`
|
|
833
967
|
|
|
834
|
-
|
|
968
|
+
Every crafted action includes an `$Infer` property that provides direct access to all inferred types:
|
|
969
|
+
|
|
970
|
+
- **`$Infer.Input`** - The input type that the action expects
|
|
971
|
+
- **`$Infer.Data`** - The success data type from your action's return value
|
|
972
|
+
- **`$Infer.Errors`** - All possible error types your action can return
|
|
973
|
+
- **`$Infer.Result`** - The complete result type (success and error cases)
|
|
974
|
+
|
|
975
|
+
#### Type Extraction Example
|
|
976
|
+
|
|
977
|
+
```typescript
|
|
978
|
+
import { action } from "@kellanjs/actioncraft";
|
|
979
|
+
import { z } from "zod";
|
|
980
|
+
|
|
981
|
+
export const updateUser = action()
|
|
982
|
+
.schemas({
|
|
983
|
+
inputSchema: z.object({
|
|
984
|
+
id: z.string(),
|
|
985
|
+
name: z.string(),
|
|
986
|
+
email: z.string().email(),
|
|
987
|
+
}),
|
|
988
|
+
})
|
|
989
|
+
.errors({
|
|
990
|
+
notFound: (id: string) => ({ type: "NOT_FOUND", id }) as const,
|
|
991
|
+
unauthorized: () => ({ type: "UNAUTHORIZED" }) as const,
|
|
992
|
+
})
|
|
993
|
+
.handler(async ({ input, errors }) => {
|
|
994
|
+
// ... implementation
|
|
995
|
+
return { user: input, updatedAt: new Date() };
|
|
996
|
+
})
|
|
997
|
+
.craft();
|
|
998
|
+
|
|
999
|
+
// Extracted types using $Infer:
|
|
1000
|
+
type ActionInput = typeof updateUser.$Infer.Input;
|
|
1001
|
+
// { id: string, name: string, email: string }
|
|
1002
|
+
|
|
1003
|
+
type ActionData = typeof updateUser.$Infer.Data;
|
|
1004
|
+
// { user: { id: string, name: string, email: string }, updatedAt: Date }
|
|
1005
|
+
|
|
1006
|
+
type ActionErrors = typeof updateUser.$Infer.Errors;
|
|
1007
|
+
// { type: "NOT_FOUND", id: string } | { type: "UNAUTHORIZED" } |
|
|
1008
|
+
// { type: "INPUT_VALIDATION", issues: ... } | { type: "UNHANDLED", message: string }
|
|
1009
|
+
|
|
1010
|
+
type ActionResult = typeof updateUser.$Infer.Result;
|
|
1011
|
+
// { success: true, data: { user: UserInput, updatedAt: Date } } |
|
|
1012
|
+
// { success: false, error: { type: "NOT_FOUND", id: string } | ... }
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
#### Using `Infer` Types
|
|
1016
|
+
|
|
1017
|
+
You can also use these alternative type inference utilities if you prefer:
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import type {
|
|
1021
|
+
InferInput,
|
|
1022
|
+
InferResult,
|
|
1023
|
+
InferData,
|
|
1024
|
+
InferErrors,
|
|
1025
|
+
} from "@kellanjs/actioncraft";
|
|
1026
|
+
|
|
1027
|
+
type ActionInput = InferInput<typeof updateUser>;
|
|
1028
|
+
type ActionResult = InferResult<typeof updateUser>;
|
|
1029
|
+
type ActionData = InferData<typeof updateUser>;
|
|
1030
|
+
type ActionErrors = InferErrors<typeof updateUser>;
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
These provide the exact same type information as the `$Infer` utility.
|
|
1034
|
+
|
|
1035
|
+
### Input Validation
|
|
1036
|
+
|
|
1037
|
+
#### Using `$validate`
|
|
1038
|
+
|
|
1039
|
+
Actioncraft provides a utility to help you easily validate data against a particular action's input schema. The `$validate` method is available on every crafted action by default, and runs the same validation logic used during action execution. This is especially useful when you want to perform client-side validation before calling an action:
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
// On the server...
|
|
1043
|
+
export const createUser = action()
|
|
1044
|
+
.schemas({ inputSchema: userSchema })
|
|
1045
|
+
.handler(async ({ input }) => ({ user: input }))
|
|
1046
|
+
.craft();
|
|
1047
|
+
|
|
1048
|
+
// On the client...
|
|
1049
|
+
// Validate input without executing the action
|
|
1050
|
+
const result = await createUser.$validate({
|
|
1051
|
+
name: "John",
|
|
1052
|
+
email: "john@example.com",
|
|
1053
|
+
age: 25,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
if (result.success) {
|
|
1057
|
+
console.log("Valid input:", result.data);
|
|
1058
|
+
// Now we can call the action, knowing that input validation will succeed
|
|
1059
|
+
} else {
|
|
1060
|
+
console.log("Validation failed:", result.error);
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
#### Validation Results
|
|
1065
|
+
|
|
1066
|
+
Returns `{ success: true, data: ValidatedInput }` on success, or `{ success: false, error: ValidationError }` on failure.
|
|
1067
|
+
|
|
1068
|
+
## Integration Utilities
|
|
1069
|
+
|
|
1070
|
+
Actioncraft comes with several utilities intended to make it easier to integrate with libraries like React Query.
|
|
1071
|
+
|
|
1072
|
+
### Actioncraft Errors
|
|
1073
|
+
|
|
1074
|
+
#### `ActioncraftError`
|
|
1075
|
+
|
|
1076
|
+
A standard Error class that wraps Actioncraft error data while preserving type information:
|
|
835
1077
|
|
|
836
1078
|
```typescript
|
|
837
1079
|
// The error preserves all your action's error data in the `cause` property
|
|
838
|
-
if (error instanceof
|
|
839
|
-
console.log(error.message); // "
|
|
1080
|
+
if (error instanceof ActioncraftError) {
|
|
1081
|
+
console.log(error.message); // "Actioncraft Error: EMAIL_TAKEN - Email already exists"
|
|
840
1082
|
console.log(error.cause); // { type: "EMAIL_TAKEN", message: "Email already exists", email: "user@example.com" }
|
|
841
1083
|
}
|
|
842
1084
|
```
|
|
843
1085
|
|
|
844
1086
|
#### `unwrap(result)`
|
|
845
1087
|
|
|
846
|
-
Extracts the data from a successful result or throws an `
|
|
1088
|
+
Extracts the data from a successful result or throws an `ActioncraftError`:
|
|
847
1089
|
|
|
848
1090
|
```typescript
|
|
849
|
-
const result = await
|
|
1091
|
+
const result = await createNewUser(data);
|
|
850
1092
|
const userData = unwrap(result); // Throws if result.success === false
|
|
851
1093
|
```
|
|
852
1094
|
|
|
853
1095
|
#### `throwable(action)`
|
|
854
1096
|
|
|
855
|
-
Wraps an action to automatically throw errors as `
|
|
1097
|
+
Wraps an action to automatically throw errors as `ActioncraftError` instances instead of returning them as objects:
|
|
856
1098
|
|
|
857
1099
|
```typescript
|
|
858
1100
|
const throwingAction = throwable(myAction);
|
|
859
1101
|
const userData = await throwingAction(data); // Throws on error
|
|
860
1102
|
```
|
|
861
1103
|
|
|
862
|
-
#### `
|
|
1104
|
+
#### `isActioncraftError(error, action)`
|
|
863
1105
|
|
|
864
|
-
Type guard that
|
|
1106
|
+
Type guard that checks if an error is an `ActioncraftError`. When called with just an error object, it performs basic structural validation. When called with both error and action, it additionally verifies that the error originated from that specific action, providing full type inference for that action's error types.
|
|
865
1107
|
|
|
866
1108
|
```typescript
|
|
867
1109
|
try {
|
|
868
1110
|
const data = await throwable(updateUser)(userData);
|
|
1111
|
+
console.log("Updated user data", data); // We know data exists at this point
|
|
869
1112
|
} catch (error) {
|
|
870
|
-
if
|
|
871
|
-
|
|
1113
|
+
// Basic usage - checks if error is any ActioncraftError
|
|
1114
|
+
if (isActioncraftError(error)) {
|
|
1115
|
+
console.log("This is an ActioncraftError:", error.cause.type);
|
|
1116
|
+
// error.cause has generic typing here
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Advanced usage - verifies error came from the given action
|
|
1120
|
+
if (isActioncraftError(error, updateUser)) {
|
|
1121
|
+
// error.cause is now typed with updateUser's specific error types
|
|
872
1122
|
switch (error.cause.type) {
|
|
873
|
-
case "EMAIL_TAKEN":
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1123
|
+
case "EMAIL_TAKEN":
|
|
1124
|
+
showError(`Email ${error.cause.email} is already taken`);
|
|
1125
|
+
break;
|
|
1126
|
+
case "UNAUTHORIZED":
|
|
1127
|
+
redirectToLogin();
|
|
1128
|
+
break;
|
|
1129
|
+
case "INPUT_VALIDATION":
|
|
1130
|
+
showValidationErrors(error.cause.issues);
|
|
877
1131
|
break;
|
|
878
1132
|
}
|
|
879
1133
|
}
|
|
880
1134
|
}
|
|
881
1135
|
```
|
|
882
1136
|
|
|
883
|
-
**
|
|
1137
|
+
**Key Differences:**
|
|
1138
|
+
|
|
1139
|
+
- **Without action parameter**: Performs basic structural validation, returns `true` for any `ActioncraftError`
|
|
1140
|
+
- **With action parameter**: Additionally verifies the error originated from that specific action and provides full type inference for that action's error types
|
|
1141
|
+
|
|
1142
|
+
#### `getActionId(action)`
|
|
1143
|
+
|
|
1144
|
+
Utility to extract the unique ID from a crafted action. Useful for debugging and logging purposes.
|
|
884
1145
|
|
|
885
1146
|
### React Query
|
|
886
1147
|
|
|
@@ -900,14 +1161,14 @@ function UserProfile({ userId }: { userId: string }) {
|
|
|
900
1161
|
queryKey: ["user", userId],
|
|
901
1162
|
queryFn: async () => {
|
|
902
1163
|
const result = await fetchUserProfile({ userId });
|
|
903
|
-
return unwrap(result); // Throws
|
|
1164
|
+
return unwrap(result); // Throws ActioncraftError on failure
|
|
904
1165
|
},
|
|
905
1166
|
});
|
|
906
1167
|
|
|
907
1168
|
if (isLoading) return <div>Loading...</div>;
|
|
908
1169
|
|
|
909
1170
|
if (error) {
|
|
910
|
-
if (
|
|
1171
|
+
if (isActioncraftError(error, fetchUserProfile)) {
|
|
911
1172
|
// Full type inference for your action's specific error types
|
|
912
1173
|
switch (error.cause.type) {
|
|
913
1174
|
case "USER_NOT_FOUND":
|
|
@@ -945,19 +1206,19 @@ Use the `throwable()` utility for mutations:
|
|
|
945
1206
|
```typescript
|
|
946
1207
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
947
1208
|
import { updateUserProfile } from "./actions";
|
|
948
|
-
import { throwable,
|
|
1209
|
+
import { throwable, isActioncraftError } from "@kellanjs/actioncraft";
|
|
949
1210
|
|
|
950
1211
|
function EditProfileForm() {
|
|
951
1212
|
const queryClient = useQueryClient();
|
|
952
1213
|
|
|
953
1214
|
const mutation = useMutation({
|
|
954
|
-
mutationFn: throwable(updateUserProfile), // Throws
|
|
1215
|
+
mutationFn: throwable(updateUserProfile), // Throws ActioncraftError on failure
|
|
955
1216
|
onSuccess: (data) => {
|
|
956
1217
|
// data is properly typed as your action's success data
|
|
957
1218
|
queryClient.invalidateQueries({ queryKey: ["user", data.user.id] });
|
|
958
1219
|
},
|
|
959
1220
|
onError: (error) => {
|
|
960
|
-
if (
|
|
1221
|
+
if (isActioncraftError(error, updateUserProfile)) {
|
|
961
1222
|
// Handle specific error types with full type safety
|
|
962
1223
|
switch (error.cause.type) {
|
|
963
1224
|
case "UNAUTHORIZED":
|
|
@@ -993,162 +1254,10 @@ function EditProfileForm() {
|
|
|
993
1254
|
}
|
|
994
1255
|
```
|
|
995
1256
|
|
|
996
|
-
##
|
|
997
|
-
|
|
998
|
-
### Bind Arguments
|
|
999
|
-
|
|
1000
|
-
ActionCraft supports binding arguments to actions. Just provide schemas, and you'll get the validated bindArgs values to use in the action.
|
|
1001
|
-
|
|
1002
|
-
If validation fails, an error with type "BIND_ARGS_VALIDATION" is returned to the client.
|
|
1257
|
+
## Thanks
|
|
1003
1258
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
```typescript
|
|
1007
|
-
const createPost = create()
|
|
1008
|
-
.schemas({
|
|
1009
|
-
bindSchemas: [z.string()], // Organization ID
|
|
1010
|
-
inputSchema: z.object({
|
|
1011
|
-
title: z.string(),
|
|
1012
|
-
content: z.string(),
|
|
1013
|
-
}),
|
|
1014
|
-
})
|
|
1015
|
-
.action(async ({ bindArgs, input }) => {
|
|
1016
|
-
const [organizationId] = bindArgs;
|
|
1017
|
-
|
|
1018
|
-
const post = await db.post.create({
|
|
1019
|
-
data: {
|
|
1020
|
-
...input,
|
|
1021
|
-
organizationId,
|
|
1022
|
-
},
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
return { post };
|
|
1026
|
-
})
|
|
1027
|
-
.craft();
|
|
1028
|
-
|
|
1029
|
-
// Create organization-specific actions
|
|
1030
|
-
const createPostForOrgA = createPost.bind(null, "org-a-id");
|
|
1031
|
-
const createPostForOrgB = createPost.bind(null, "org-b-id");
|
|
1032
|
-
|
|
1033
|
-
// Each bound action automatically includes the correct org ID
|
|
1034
|
-
const result = await createPostForOrgA({
|
|
1035
|
-
title: "My Post",
|
|
1036
|
-
content: "Post content...",
|
|
1037
|
-
});
|
|
1038
|
-
```
|
|
1039
|
-
|
|
1040
|
-
#### Example: Configuration Binding
|
|
1041
|
-
|
|
1042
|
-
```typescript
|
|
1043
|
-
const sendEmail = create()
|
|
1044
|
-
.schemas({
|
|
1045
|
-
bindSchemas: [
|
|
1046
|
-
z.object({
|
|
1047
|
-
apiKey: z.string(),
|
|
1048
|
-
fromEmail: z.string(),
|
|
1049
|
-
}),
|
|
1050
|
-
],
|
|
1051
|
-
inputSchema: z.object({
|
|
1052
|
-
to: z.string().email(),
|
|
1053
|
-
subject: z.string(),
|
|
1054
|
-
body: z.string(),
|
|
1055
|
-
}),
|
|
1056
|
-
})
|
|
1057
|
-
.action(async ({ bindArgs, input }) => {
|
|
1058
|
-
const [config] = bindArgs;
|
|
1059
|
-
|
|
1060
|
-
// Use the bound configuration
|
|
1061
|
-
const emailService = new EmailService(config.apiKey);
|
|
1062
|
-
const result = await emailService.send({
|
|
1063
|
-
from: config.fromEmail,
|
|
1064
|
-
to: input.to,
|
|
1065
|
-
subject: input.subject,
|
|
1066
|
-
body: input.body,
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
return { messageId: result.id };
|
|
1070
|
-
})
|
|
1071
|
-
.craft();
|
|
1072
|
-
|
|
1073
|
-
// Create environment-specific email actions
|
|
1074
|
-
const sendProductionEmail = sendEmail.bind(null, {
|
|
1075
|
-
apiKey: process.env.PROD_EMAIL_API_KEY,
|
|
1076
|
-
fromEmail: "noreply@company.com",
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
const sendDevelopmentEmail = sendEmail.bind(null, {
|
|
1080
|
-
apiKey: process.env.DEV_EMAIL_API_KEY,
|
|
1081
|
-
fromEmail: "dev@company.com",
|
|
1082
|
-
});
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
## Type Inference Utilities
|
|
1086
|
-
|
|
1087
|
-
ActionCraft provides several type inference utilities to extract types from your actions for use elsewhere in your application:
|
|
1088
|
-
|
|
1089
|
-
### Available Utilities
|
|
1090
|
-
|
|
1091
|
-
#### `InferInput<Action>`
|
|
1092
|
-
|
|
1093
|
-
Infers the input type that the action expects. Returns `unknown` if no input schema is defined.
|
|
1094
|
-
|
|
1095
|
-
#### `InferResult<Action>`
|
|
1096
|
-
|
|
1097
|
-
Infers the complete result type, including both success and error cases. Respects your action's configuration (api/functional format, useActionState, etc.).
|
|
1098
|
-
|
|
1099
|
-
#### `InferData<Action>`
|
|
1100
|
-
|
|
1101
|
-
Infers the success data type from your action's return value.
|
|
1102
|
-
|
|
1103
|
-
#### `InferErrors<Action>`
|
|
1104
|
-
|
|
1105
|
-
Infers all possible error types your action can return, including custom errors, validation errors, and thrown errors.
|
|
1106
|
-
|
|
1107
|
-
### Type Extraction Example
|
|
1108
|
-
|
|
1109
|
-
```typescript
|
|
1110
|
-
import { create } from "@kellanjs/actioncraft";
|
|
1111
|
-
import type {
|
|
1112
|
-
InferInput,
|
|
1113
|
-
InferResult,
|
|
1114
|
-
InferData,
|
|
1115
|
-
InferErrors,
|
|
1116
|
-
} from "@kellanjs/actioncraft";
|
|
1117
|
-
import { z } from "zod";
|
|
1118
|
-
|
|
1119
|
-
const updateUser = create()
|
|
1120
|
-
.schemas({
|
|
1121
|
-
inputSchema: z.object({
|
|
1122
|
-
id: z.string(),
|
|
1123
|
-
name: z.string(),
|
|
1124
|
-
email: z.string().email(),
|
|
1125
|
-
}),
|
|
1126
|
-
})
|
|
1127
|
-
.errors({
|
|
1128
|
-
notFound: (id: string) => ({ type: "NOT_FOUND", id }) as const,
|
|
1129
|
-
unauthorized: () => ({ type: "UNAUTHORIZED" }) as const,
|
|
1130
|
-
})
|
|
1131
|
-
.action(async ({ input, errors }) => {
|
|
1132
|
-
// ... implementation
|
|
1133
|
-
return { user: input, updatedAt: new Date() };
|
|
1134
|
-
})
|
|
1135
|
-
.craft();
|
|
1136
|
-
|
|
1137
|
-
type ActionInput = InferInput<typeof updateUser>;
|
|
1138
|
-
// { id: string, name: string, email: string }
|
|
1139
|
-
|
|
1140
|
-
type ActionResult = InferResult<typeof updateUser>;
|
|
1141
|
-
// { success: true, data: { user: UserInput, updatedAt: Date } } |
|
|
1142
|
-
// { success: false, error: { type: "NOT_FOUND", id: string } | ... }
|
|
1143
|
-
|
|
1144
|
-
type ActionData = InferData<typeof updateUser>;
|
|
1145
|
-
// { user: { id: string, name: string, email: string }, updatedAt: Date }
|
|
1146
|
-
|
|
1147
|
-
type ActionErrors = InferErrors<typeof updateUser>;
|
|
1148
|
-
// { type: "NOT_FOUND", id: string } | { type: "UNAUTHORIZED" } |
|
|
1149
|
-
// { type: "INPUT_VALIDATION", issues: ... } | { type: "UNHANDLED", message: string }
|
|
1150
|
-
```
|
|
1259
|
+
If you made it this far, thanks for checking out the library, and I hope you find it useful in your projects!
|
|
1151
1260
|
|
|
1152
1261
|
## License
|
|
1153
1262
|
|
|
1154
|
-
|
|
1263
|
+
Actioncraft is open source under the terms of the [MIT license](https://github.com/kellanjs/actioncraft/blob/main/LICENSE).
|