nuxt-safe-action 0.1.1
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/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/module.d.mts +13 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +130 -0
- package/dist/runtime/composables/useAction.d.ts +15 -0
- package/dist/runtime/composables/useAction.js +76 -0
- package/dist/runtime/server/createSafeActionClient.d.ts +52 -0
- package/dist/runtime/server/createSafeActionClient.js +169 -0
- package/dist/runtime/server/errors.d.ts +31 -0
- package/dist/runtime/server/errors.js +17 -0
- package/dist/runtime/server/handler.d.ts +6 -0
- package/dist/runtime/server/handler.js +7 -0
- package/dist/runtime/server/index.d.ts +3 -0
- package/dist/runtime/server/index.js +2 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/types.d.ts +103 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +3 -0
- package/package.json +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philip Rutberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# nuxt-safe-action
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
7
|
+
|
|
8
|
+
Type-safe and validated server actions for Nuxt.
|
|
9
|
+
|
|
10
|
+
End-to-end type safety, input/output validation via Zod, composable middleware, and Vue composables with loading states, error handling, and optimistic updates.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// server/actions/create-post.ts
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { actionClient } from '../utils/action-client'
|
|
16
|
+
|
|
17
|
+
export default actionClient
|
|
18
|
+
.schema(z.object({
|
|
19
|
+
title: z.string().min(1).max(200),
|
|
20
|
+
body: z.string().min(1),
|
|
21
|
+
}))
|
|
22
|
+
.action(async ({ parsedInput }) => {
|
|
23
|
+
const post = await db.post.create({ data: parsedInput })
|
|
24
|
+
return { id: post.id, title: post.title }
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import { createPost } from '#safe-action/actions'
|
|
31
|
+
|
|
32
|
+
const { execute, data, status, validationErrors } = useAction(createPost)
|
|
33
|
+
</script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **End-to-end type safety** — Input and output types flow from server to client automatically
|
|
39
|
+
- **Input validation** — Zod schemas validate input before your handler runs
|
|
40
|
+
- **Composable middleware** — Chain auth checks, rate limiting, logging, and more
|
|
41
|
+
- **Vue composables** — `useAction` with reactive `status`, `data`, `error`, and `validationErrors`
|
|
42
|
+
- **Auto route generation** — Define actions in `server/actions/`, routes are created for you
|
|
43
|
+
- **H3Event access** — Full request context in middleware (headers, cookies, IP, sessions)
|
|
44
|
+
- **Nuxt-native** — Auto-imports, works with Nuxt DevTools, familiar conventions
|
|
45
|
+
|
|
46
|
+
## Quick Setup
|
|
47
|
+
|
|
48
|
+
Install the module:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx nuxi module add nuxt-safe-action
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or manually:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm add nuxt-safe-action zod
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// nuxt.config.ts
|
|
62
|
+
export default defineNuxtConfig({
|
|
63
|
+
modules: ['nuxt-safe-action'],
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### 1. Create an action client
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// server/utils/action-client.ts
|
|
73
|
+
import { createSafeActionClient } from '#safe-action'
|
|
74
|
+
|
|
75
|
+
export const actionClient = createSafeActionClient({
|
|
76
|
+
handleServerError: (error) => {
|
|
77
|
+
console.error('Action error:', error.message)
|
|
78
|
+
return error.message
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Optional: create an authenticated client
|
|
83
|
+
export const authActionClient = actionClient
|
|
84
|
+
.use(async ({ next, event }) => {
|
|
85
|
+
const session = await getUserSession(event)
|
|
86
|
+
if (!session) throw new Error('Unauthorized')
|
|
87
|
+
return next({ ctx: { userId: session.user.id } })
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Define actions
|
|
92
|
+
|
|
93
|
+
Create action files in `server/actions/`. Each file should export a default action:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// server/actions/greet.ts
|
|
97
|
+
import { z } from 'zod'
|
|
98
|
+
import { actionClient } from '../utils/action-client'
|
|
99
|
+
|
|
100
|
+
export default actionClient
|
|
101
|
+
.schema(z.object({
|
|
102
|
+
name: z.string().min(1, 'Name is required'),
|
|
103
|
+
}))
|
|
104
|
+
.action(async ({ parsedInput }) => {
|
|
105
|
+
return { greeting: `Hello, ${parsedInput.name}!` }
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. Use in components
|
|
110
|
+
|
|
111
|
+
```vue
|
|
112
|
+
<script setup lang="ts">
|
|
113
|
+
import { greet } from '#safe-action/actions'
|
|
114
|
+
|
|
115
|
+
const { execute, data, status, validationErrors, isExecuting, hasSucceeded } = useAction(greet, {
|
|
116
|
+
onSuccess({ data }) {
|
|
117
|
+
console.log(data.greeting) // fully typed!
|
|
118
|
+
},
|
|
119
|
+
onError({ error }) {
|
|
120
|
+
console.error(error)
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<template>
|
|
126
|
+
<form @submit.prevent="execute({ name: 'World' })">
|
|
127
|
+
<button :disabled="isExecuting">
|
|
128
|
+
{{ isExecuting ? 'Loading...' : 'Greet' }}
|
|
129
|
+
</button>
|
|
130
|
+
<p v-if="hasSucceeded">{{ data?.greeting }}</p>
|
|
131
|
+
</form>
|
|
132
|
+
</template>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## API
|
|
136
|
+
|
|
137
|
+
### `createSafeActionClient(opts?)`
|
|
138
|
+
|
|
139
|
+
Creates a new action client. Call this once and reuse it across actions.
|
|
140
|
+
|
|
141
|
+
| Option | Type | Description |
|
|
142
|
+
|--------|------|-------------|
|
|
143
|
+
| `handleServerError` | `(error: Error) => string` | Transform server errors before sending to client |
|
|
144
|
+
|
|
145
|
+
### Builder chain
|
|
146
|
+
|
|
147
|
+
| Method | Description |
|
|
148
|
+
|--------|-------------|
|
|
149
|
+
| `.schema(zodSchema)` | Set input validation schema |
|
|
150
|
+
| `.outputSchema(zodSchema)` | Set output validation schema |
|
|
151
|
+
| `.use(middleware)` | Add middleware to the chain |
|
|
152
|
+
| `.metadata(meta)` | Attach metadata (accessible in middleware) |
|
|
153
|
+
| `.action(handler)` | Define the action handler (terminal) |
|
|
154
|
+
|
|
155
|
+
### `useAction(action, callbacks?)`
|
|
156
|
+
|
|
157
|
+
Vue composable for executing actions.
|
|
158
|
+
|
|
159
|
+
**Returns:**
|
|
160
|
+
|
|
161
|
+
| Property | Type | Description |
|
|
162
|
+
|----------|------|-------------|
|
|
163
|
+
| `execute(input)` | `(input: TInput) => void` | Fire-and-forget execution |
|
|
164
|
+
| `executeAsync(input)` | `(input: TInput) => Promise<ActionResult>` | Awaitable execution |
|
|
165
|
+
| `data` | `Ref<TOutput \| undefined>` | Success data |
|
|
166
|
+
| `serverError` | `Ref<string \| undefined>` | Server error message |
|
|
167
|
+
| `validationErrors` | `Ref<Record<string, string[]> \| undefined>` | Per-field validation errors |
|
|
168
|
+
| `status` | `Ref<ActionStatus>` | `'idle' \| 'executing' \| 'hasSucceeded' \| 'hasErrored'` |
|
|
169
|
+
| `isIdle` | `ComputedRef<boolean>` | Status shortcut |
|
|
170
|
+
| `isExecuting` | `ComputedRef<boolean>` | Status shortcut |
|
|
171
|
+
| `hasSucceeded` | `ComputedRef<boolean>` | Status shortcut |
|
|
172
|
+
| `hasErrored` | `ComputedRef<boolean>` | Status shortcut |
|
|
173
|
+
| `reset()` | `() => void` | Reset all state to initial |
|
|
174
|
+
|
|
175
|
+
**Callbacks:**
|
|
176
|
+
|
|
177
|
+
| Callback | Description |
|
|
178
|
+
|----------|-------------|
|
|
179
|
+
| `onSuccess({ data, input })` | Called when the action succeeds |
|
|
180
|
+
| `onError({ error, input })` | Called on server or validation error |
|
|
181
|
+
| `onSettled({ result, input })` | Called after every execution |
|
|
182
|
+
| `onExecute({ input })` | Called when execution starts |
|
|
183
|
+
|
|
184
|
+
### Middleware
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
actionClient.use(async ({ ctx, next, event, metadata, clientInput }) => {
|
|
188
|
+
// ctx: context from previous middleware
|
|
189
|
+
// event: H3Event with full request access
|
|
190
|
+
// metadata: action metadata
|
|
191
|
+
// clientInput: raw input before validation
|
|
192
|
+
return next({ ctx: { ...ctx, myData: 'value' } })
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Error handling
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { ActionError, returnValidationErrors } from '#safe-action'
|
|
200
|
+
|
|
201
|
+
// Throw a server error
|
|
202
|
+
throw new ActionError('Not enough credits')
|
|
203
|
+
|
|
204
|
+
// Return per-field validation errors
|
|
205
|
+
returnValidationErrors({
|
|
206
|
+
email: ['This email is already taken'],
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Configuration
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// nuxt.config.ts
|
|
214
|
+
export default defineNuxtConfig({
|
|
215
|
+
modules: ['nuxt-safe-action'],
|
|
216
|
+
safeAction: {
|
|
217
|
+
actionsDir: 'actions', // relative to server/ directory (default: 'actions')
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## How it works
|
|
223
|
+
|
|
224
|
+
1. You define actions in `server/actions/` using the builder chain
|
|
225
|
+
2. The module scans this directory and auto-generates Nitro API routes at `/api/_actions/<name>`
|
|
226
|
+
3. A typed virtual module `#safe-action/actions` provides client-side references with full type inference
|
|
227
|
+
4. `useAction()` calls the generated route via `$fetch` and returns reactive state
|
|
228
|
+
|
|
229
|
+
## Inspiration
|
|
230
|
+
|
|
231
|
+
Inspired by [next-safe-action](https://github.com/TheEdoRan/next-safe-action) — adapted for the Nuxt ecosystem with H3Event access, auto route generation, and Vue reactivity.
|
|
232
|
+
|
|
233
|
+
## Contributing
|
|
234
|
+
|
|
235
|
+
<details>
|
|
236
|
+
<summary>Local development</summary>
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Install dependencies
|
|
240
|
+
pnpm install
|
|
241
|
+
|
|
242
|
+
# Generate type stubs
|
|
243
|
+
pnpm run dev:prepare
|
|
244
|
+
|
|
245
|
+
# Develop with the playground
|
|
246
|
+
pnpm run dev
|
|
247
|
+
|
|
248
|
+
# Build the playground
|
|
249
|
+
pnpm run dev:build
|
|
250
|
+
|
|
251
|
+
# Run ESLint
|
|
252
|
+
pnpm run lint
|
|
253
|
+
|
|
254
|
+
# Run Vitest
|
|
255
|
+
pnpm run test
|
|
256
|
+
pnpm run test:watch
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
</details>
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
[MIT](./LICENSE)
|
|
264
|
+
|
|
265
|
+
<!-- Badges -->
|
|
266
|
+
[npm-version-src]: https://img.shields.io/npm/v/nuxt-safe-action/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
|
267
|
+
[npm-version-href]: https://npmjs.com/package/nuxt-safe-action
|
|
268
|
+
|
|
269
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-safe-action.svg?style=flat&colorA=020420&colorB=00DC82
|
|
270
|
+
[npm-downloads-href]: https://npm.chart.dev/nuxt-safe-action
|
|
271
|
+
|
|
272
|
+
[license-src]: https://img.shields.io/npm/l/nuxt-safe-action.svg?style=flat&colorA=020420&colorB=00DC82
|
|
273
|
+
[license-href]: https://npmjs.com/package/nuxt-safe-action
|
|
274
|
+
|
|
275
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
|
|
276
|
+
[nuxt-href]: https://nuxt.com
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Directory where action files are located, relative to the server directory.
|
|
6
|
+
* @default 'actions'
|
|
7
|
+
*/
|
|
8
|
+
actionsDir?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
11
|
+
|
|
12
|
+
export { _default as default };
|
|
13
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, posix, basename } from 'node:path';
|
|
3
|
+
import { useLogger, defineNuxtModule, createResolver, addImportsDir, addServerImports, addTemplate } from '@nuxt/kit';
|
|
4
|
+
|
|
5
|
+
const logger = useLogger("nuxt-safe-action");
|
|
6
|
+
const module$1 = defineNuxtModule({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "nuxt-safe-action",
|
|
9
|
+
configKey: "safeAction",
|
|
10
|
+
compatibility: {
|
|
11
|
+
nuxt: ">=4.0.0"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
defaults: {
|
|
15
|
+
actionsDir: "actions"
|
|
16
|
+
},
|
|
17
|
+
setup(options, nuxt) {
|
|
18
|
+
const { resolve } = createResolver(import.meta.url);
|
|
19
|
+
addImportsDir(resolve("./runtime/composables"));
|
|
20
|
+
addServerImports([
|
|
21
|
+
{ name: "createSafeActionClient", from: resolve("./runtime/server/createSafeActionClient") },
|
|
22
|
+
{ name: "ActionError", from: resolve("./runtime/server/errors") },
|
|
23
|
+
{ name: "ActionValidationError", from: resolve("./runtime/server/errors") },
|
|
24
|
+
{ name: "returnValidationErrors", from: resolve("./runtime/server/errors") }
|
|
25
|
+
]);
|
|
26
|
+
nuxt.options.alias["#safe-action"] = resolve("./runtime/server/index");
|
|
27
|
+
const actionsDir = options.actionsDir || "actions";
|
|
28
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
29
|
+
const serverDir2 = nuxt.options.serverDir;
|
|
30
|
+
const fullActionsDir2 = join(serverDir2, actionsDir);
|
|
31
|
+
if (!existsSync(fullActionsDir2)) {
|
|
32
|
+
logger.info(
|
|
33
|
+
`No actions directory found at ${fullActionsDir2} \u2014 skipping action route generation.`
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const actionFiles = scanActionFiles(fullActionsDir2);
|
|
38
|
+
if (actionFiles.length === 0) {
|
|
39
|
+
logger.info("No action files found.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
logger.info(
|
|
43
|
+
`Found ${actionFiles.length} action file(s): ${actionFiles.map((a) => a.name).join(", ")}`
|
|
44
|
+
);
|
|
45
|
+
nitroConfig.virtual = nitroConfig.virtual || {};
|
|
46
|
+
nitroConfig.handlers = nitroConfig.handlers || [];
|
|
47
|
+
for (const action of actionFiles) {
|
|
48
|
+
const virtualKey = `#safe-action-handler/${action.name}`;
|
|
49
|
+
nitroConfig.virtual[virtualKey] = generateHandlerCode(action);
|
|
50
|
+
nitroConfig.handlers.push({
|
|
51
|
+
route: `/api/_actions/${action.name}`,
|
|
52
|
+
method: "post",
|
|
53
|
+
handler: virtualKey
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const serverDir = nuxt.options.serverDir;
|
|
58
|
+
const fullActionsDir = join(serverDir, actionsDir);
|
|
59
|
+
addTemplate({
|
|
60
|
+
filename: "safe-action/actions.ts",
|
|
61
|
+
write: true,
|
|
62
|
+
getContents: () => {
|
|
63
|
+
if (!existsSync(fullActionsDir)) {
|
|
64
|
+
return "export {}\n";
|
|
65
|
+
}
|
|
66
|
+
const actionFiles = scanActionFiles(fullActionsDir);
|
|
67
|
+
if (actionFiles.length === 0) {
|
|
68
|
+
return "export {}\n";
|
|
69
|
+
}
|
|
70
|
+
const lines = [
|
|
71
|
+
`import type { SafeActionReference } from '#safe-action'`,
|
|
72
|
+
""
|
|
73
|
+
];
|
|
74
|
+
for (const action of actionFiles) {
|
|
75
|
+
const exportName = toCamelCase(action.name);
|
|
76
|
+
const relativePath = posix.join("../..", "server", actionsDir, action.name);
|
|
77
|
+
lines.push(`import type _action_${exportName} from '${relativePath}'`);
|
|
78
|
+
}
|
|
79
|
+
lines.push("");
|
|
80
|
+
for (const action of actionFiles) {
|
|
81
|
+
const exportName = toCamelCase(action.name);
|
|
82
|
+
lines.push(
|
|
83
|
+
`export const ${exportName}: SafeActionReference<(typeof _action_${exportName})['_types']['input'], (typeof _action_${exportName})['_types']['output'], (typeof _action_${exportName})['_types']['serverError']> = Object.freeze({ __safeActionPath: '${action.name}' }) as any`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n") + "\n";
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
nuxt.hook("prepare:types", ({ tsConfig }) => {
|
|
90
|
+
tsConfig.compilerOptions = tsConfig.compilerOptions || {};
|
|
91
|
+
tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {};
|
|
92
|
+
tsConfig.compilerOptions.paths["#safe-action/actions"] = ["./safe-action/actions"];
|
|
93
|
+
tsConfig.compilerOptions.paths["#safe-action"] = [
|
|
94
|
+
resolve("./runtime/server/index").replace(/\.ts$/, "")
|
|
95
|
+
];
|
|
96
|
+
});
|
|
97
|
+
nuxt.options.alias["#safe-action/actions"] = join(nuxt.options.buildDir, "safe-action/actions");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
function scanActionFiles(dir, prefix = "") {
|
|
101
|
+
const results = [];
|
|
102
|
+
if (!existsSync(dir)) return results;
|
|
103
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const fullPath = join(dir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
results.push(...scanActionFiles(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name));
|
|
108
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && entry.name !== "index.ts") {
|
|
109
|
+
const name = prefix ? `${prefix}/${basename(entry.name, ".ts")}` : basename(entry.name, ".ts");
|
|
110
|
+
results.push({ name, filePath: fullPath });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
function generateHandlerCode(action) {
|
|
116
|
+
return `
|
|
117
|
+
import { defineEventHandler, readBody } from 'h3'
|
|
118
|
+
import action from '${action.filePath}'
|
|
119
|
+
|
|
120
|
+
export default defineEventHandler(async (event) => {
|
|
121
|
+
const body = await readBody(event).catch(() => undefined)
|
|
122
|
+
return action._execute(body, event)
|
|
123
|
+
})
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
function toCamelCase(name) {
|
|
127
|
+
return name.replace(/[/-](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toLowerCase());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SafeAction, SafeActionReference, UseActionReturn, UseActionCallbacks } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vue composable for executing a safe action with reactive status tracking.
|
|
4
|
+
*
|
|
5
|
+
* ```vue
|
|
6
|
+
* <script setup lang="ts">
|
|
7
|
+
* import { createPost } from '#safe-action/actions'
|
|
8
|
+
*
|
|
9
|
+
* const { execute, data, status, error } = useAction(createPost, {
|
|
10
|
+
* onSuccess({ data }) { console.log('Created:', data) },
|
|
11
|
+
* })
|
|
12
|
+
* </script>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function useAction<TInput, TOutput, TServerError = string>(action: SafeAction<TInput, TOutput, TServerError> | SafeActionReference<TInput, TOutput, TServerError>, callbacks?: UseActionCallbacks<TInput, TOutput, TServerError>): UseActionReturn<TInput, TOutput, TServerError>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ref, computed, readonly } from "vue";
|
|
2
|
+
export function useAction(action, callbacks) {
|
|
3
|
+
const actionPath = action.__safeActionPath;
|
|
4
|
+
const data = ref();
|
|
5
|
+
const serverError = ref();
|
|
6
|
+
const validationErrors = ref();
|
|
7
|
+
const status = ref("idle");
|
|
8
|
+
async function executeAsync(input) {
|
|
9
|
+
serverError.value = void 0;
|
|
10
|
+
validationErrors.value = void 0;
|
|
11
|
+
status.value = "executing";
|
|
12
|
+
callbacks?.onExecute?.({ input });
|
|
13
|
+
try {
|
|
14
|
+
const result = await $fetch(
|
|
15
|
+
`/api/_actions/${actionPath}`,
|
|
16
|
+
{
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: input
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
if (result.data !== void 0) {
|
|
22
|
+
data.value = result.data;
|
|
23
|
+
status.value = "hasSucceeded";
|
|
24
|
+
callbacks?.onSuccess?.({ data: result.data, input });
|
|
25
|
+
} else if (result.serverError !== void 0) {
|
|
26
|
+
serverError.value = result.serverError;
|
|
27
|
+
status.value = "hasErrored";
|
|
28
|
+
callbacks?.onError?.({
|
|
29
|
+
error: { serverError: result.serverError },
|
|
30
|
+
input
|
|
31
|
+
});
|
|
32
|
+
} else if (result.validationErrors !== void 0) {
|
|
33
|
+
validationErrors.value = result.validationErrors;
|
|
34
|
+
status.value = "hasErrored";
|
|
35
|
+
callbacks?.onError?.({
|
|
36
|
+
error: { validationErrors: result.validationErrors },
|
|
37
|
+
input
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
callbacks?.onSettled?.({ result, input });
|
|
41
|
+
return result;
|
|
42
|
+
} catch (fetchError) {
|
|
43
|
+
const message = fetchError instanceof Error ? fetchError.message : "An unexpected error occurred";
|
|
44
|
+
serverError.value = message;
|
|
45
|
+
status.value = "hasErrored";
|
|
46
|
+
const result = {
|
|
47
|
+
serverError: message
|
|
48
|
+
};
|
|
49
|
+
callbacks?.onError?.({ error: { serverError: message }, input });
|
|
50
|
+
callbacks?.onSettled?.({ result, input });
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function execute(input) {
|
|
55
|
+
executeAsync(input);
|
|
56
|
+
}
|
|
57
|
+
function reset() {
|
|
58
|
+
data.value = void 0;
|
|
59
|
+
serverError.value = void 0;
|
|
60
|
+
validationErrors.value = void 0;
|
|
61
|
+
status.value = "idle";
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
execute,
|
|
65
|
+
executeAsync,
|
|
66
|
+
data: readonly(data),
|
|
67
|
+
serverError: readonly(serverError),
|
|
68
|
+
validationErrors: readonly(validationErrors),
|
|
69
|
+
status: readonly(status),
|
|
70
|
+
isIdle: computed(() => status.value === "idle"),
|
|
71
|
+
isExecuting: computed(() => status.value === "executing"),
|
|
72
|
+
hasSucceeded: computed(() => status.value === "hasSucceeded"),
|
|
73
|
+
hasErrored: computed(() => status.value === "hasErrored"),
|
|
74
|
+
reset
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import type { SafeAction, SafeActionClientOpts, ActionMetadata, MiddlewareFn, ActionHandler } from '../types.js';
|
|
3
|
+
declare class SafeActionBuilder<TCtx, TInput, TServerError> {
|
|
4
|
+
private _middlewares;
|
|
5
|
+
private _inputSchema?;
|
|
6
|
+
private _outputSchema?;
|
|
7
|
+
private _metadata;
|
|
8
|
+
private _handleServerError?;
|
|
9
|
+
constructor(opts: {
|
|
10
|
+
middlewares: MiddlewareFn<any, any>[];
|
|
11
|
+
inputSchema?: ZodType;
|
|
12
|
+
outputSchema?: ZodType;
|
|
13
|
+
metadata: ActionMetadata;
|
|
14
|
+
handleServerError?: (error: Error) => TServerError;
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Add a Zod schema for input validation.
|
|
18
|
+
*/
|
|
19
|
+
schema<TSchema extends ZodType>(schema: TSchema): SafeActionBuilder<TCtx, TSchema extends ZodType<infer T> ? T : never, TServerError>;
|
|
20
|
+
/**
|
|
21
|
+
* Add a Zod schema for output validation.
|
|
22
|
+
*/
|
|
23
|
+
outputSchema<TSchema extends ZodType>(schema: TSchema): SafeActionBuilder<TCtx, TInput, TServerError>;
|
|
24
|
+
/**
|
|
25
|
+
* Add middleware to the action chain.
|
|
26
|
+
* Middleware runs in order, and each can extend the context.
|
|
27
|
+
*/
|
|
28
|
+
use<TNewCtx>(middleware: MiddlewareFn<TCtx, TNewCtx>): SafeActionBuilder<TNewCtx, TInput, TServerError>;
|
|
29
|
+
/**
|
|
30
|
+
* Attach metadata to the action (e.g. action name, tags).
|
|
31
|
+
* Accessible in middleware via `args.metadata`.
|
|
32
|
+
*/
|
|
33
|
+
metadata(meta: ActionMetadata): SafeActionBuilder<TCtx, TInput, TServerError>;
|
|
34
|
+
/**
|
|
35
|
+
* Define the action handler. This is the terminal method of the chain.
|
|
36
|
+
* Returns a `SafeAction` object that can be exported from `server/actions/`.
|
|
37
|
+
*/
|
|
38
|
+
action<TOutput>(handler: ActionHandler<TCtx, TInput, TOutput>): SafeAction<TInput, TOutput, TServerError>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a safe action client with optional global configuration.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { createSafeActionClient } from '#safe-action'
|
|
45
|
+
*
|
|
46
|
+
* export const actionClient = createSafeActionClient({
|
|
47
|
+
* handleServerError: (e) => e.message,
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function createSafeActionClient<TServerError = string>(opts?: SafeActionClientOpts<TServerError>): SafeActionBuilder<Record<string, never>, undefined, TServerError>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ActionError, ActionValidationError } from "./errors.js";
|
|
2
|
+
function formatZodErrors(zodError) {
|
|
3
|
+
const errors = {};
|
|
4
|
+
for (const issue of zodError.issues) {
|
|
5
|
+
const path = issue.path.join(".") || "_root";
|
|
6
|
+
if (!errors[path]) {
|
|
7
|
+
errors[path] = [];
|
|
8
|
+
}
|
|
9
|
+
errors[path].push(issue.message);
|
|
10
|
+
}
|
|
11
|
+
return errors;
|
|
12
|
+
}
|
|
13
|
+
async function executeMiddlewareChain(middlewares, initialCtx, clientInput, metadata, event, innerFn) {
|
|
14
|
+
let execute = innerFn;
|
|
15
|
+
for (let i = middlewares.length - 1; i >= 0; i--) {
|
|
16
|
+
const nextExecute = execute;
|
|
17
|
+
const middleware = middlewares[i];
|
|
18
|
+
execute = async (ctx) => {
|
|
19
|
+
let result;
|
|
20
|
+
await middleware({
|
|
21
|
+
ctx,
|
|
22
|
+
clientInput,
|
|
23
|
+
metadata,
|
|
24
|
+
event,
|
|
25
|
+
next: async ({ ctx: newCtx }) => {
|
|
26
|
+
result = await nextExecute(newCtx);
|
|
27
|
+
return { ctx: newCtx };
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return execute(initialCtx);
|
|
34
|
+
}
|
|
35
|
+
class SafeActionBuilder {
|
|
36
|
+
_middlewares;
|
|
37
|
+
_inputSchema;
|
|
38
|
+
_outputSchema;
|
|
39
|
+
_metadata;
|
|
40
|
+
_handleServerError;
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
this._middlewares = opts.middlewares;
|
|
43
|
+
this._inputSchema = opts.inputSchema;
|
|
44
|
+
this._outputSchema = opts.outputSchema;
|
|
45
|
+
this._metadata = opts.metadata;
|
|
46
|
+
this._handleServerError = opts.handleServerError;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Add a Zod schema for input validation.
|
|
50
|
+
*/
|
|
51
|
+
schema(schema) {
|
|
52
|
+
return new SafeActionBuilder({
|
|
53
|
+
middlewares: this._middlewares,
|
|
54
|
+
inputSchema: schema,
|
|
55
|
+
outputSchema: this._outputSchema,
|
|
56
|
+
metadata: this._metadata,
|
|
57
|
+
handleServerError: this._handleServerError
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Add a Zod schema for output validation.
|
|
62
|
+
*/
|
|
63
|
+
outputSchema(schema) {
|
|
64
|
+
return new SafeActionBuilder({
|
|
65
|
+
middlewares: this._middlewares,
|
|
66
|
+
inputSchema: this._inputSchema,
|
|
67
|
+
outputSchema: schema,
|
|
68
|
+
metadata: this._metadata,
|
|
69
|
+
handleServerError: this._handleServerError
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Add middleware to the action chain.
|
|
74
|
+
* Middleware runs in order, and each can extend the context.
|
|
75
|
+
*/
|
|
76
|
+
use(middleware) {
|
|
77
|
+
return new SafeActionBuilder({
|
|
78
|
+
middlewares: [...this._middlewares, middleware],
|
|
79
|
+
inputSchema: this._inputSchema,
|
|
80
|
+
outputSchema: this._outputSchema,
|
|
81
|
+
metadata: this._metadata,
|
|
82
|
+
handleServerError: this._handleServerError
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Attach metadata to the action (e.g. action name, tags).
|
|
87
|
+
* Accessible in middleware via `args.metadata`.
|
|
88
|
+
*/
|
|
89
|
+
metadata(meta) {
|
|
90
|
+
return new SafeActionBuilder({
|
|
91
|
+
middlewares: this._middlewares,
|
|
92
|
+
inputSchema: this._inputSchema,
|
|
93
|
+
outputSchema: this._outputSchema,
|
|
94
|
+
metadata: { ...this._metadata, ...meta },
|
|
95
|
+
handleServerError: this._handleServerError
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Define the action handler. This is the terminal method of the chain.
|
|
100
|
+
* Returns a `SafeAction` object that can be exported from `server/actions/`.
|
|
101
|
+
*/
|
|
102
|
+
action(handler) {
|
|
103
|
+
const config = {
|
|
104
|
+
middlewares: this._middlewares,
|
|
105
|
+
inputSchema: this._inputSchema,
|
|
106
|
+
outputSchema: this._outputSchema,
|
|
107
|
+
metadata: this._metadata,
|
|
108
|
+
handler,
|
|
109
|
+
handleServerError: this._handleServerError
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
_execute: (rawInput, event) => executeAction(rawInput, event, config)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function executeAction(rawInput, event, config) {
|
|
117
|
+
try {
|
|
118
|
+
return await executeMiddlewareChain(
|
|
119
|
+
config.middlewares,
|
|
120
|
+
{},
|
|
121
|
+
rawInput,
|
|
122
|
+
config.metadata,
|
|
123
|
+
event,
|
|
124
|
+
async (ctx) => {
|
|
125
|
+
let parsedInput = rawInput;
|
|
126
|
+
if (config.inputSchema) {
|
|
127
|
+
const result = config.inputSchema.safeParse(rawInput);
|
|
128
|
+
if (!result.success) {
|
|
129
|
+
return { validationErrors: formatZodErrors(result.error) };
|
|
130
|
+
}
|
|
131
|
+
parsedInput = result.data;
|
|
132
|
+
}
|
|
133
|
+
const data = await config.handler({
|
|
134
|
+
parsedInput,
|
|
135
|
+
ctx,
|
|
136
|
+
event
|
|
137
|
+
});
|
|
138
|
+
if (config.outputSchema) {
|
|
139
|
+
const result = config.outputSchema.safeParse(data);
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Output validation failed: ${JSON.stringify(formatZodErrors(result.error))}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return { data: result.data };
|
|
146
|
+
}
|
|
147
|
+
return { data };
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof ActionValidationError) {
|
|
152
|
+
return { validationErrors: error.validationErrors };
|
|
153
|
+
}
|
|
154
|
+
if (error instanceof ActionError) {
|
|
155
|
+
return { serverError: error.message };
|
|
156
|
+
}
|
|
157
|
+
if (config.handleServerError) {
|
|
158
|
+
return { serverError: config.handleServerError(error) };
|
|
159
|
+
}
|
|
160
|
+
return { serverError: "An unexpected error occurred" };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function createSafeActionClient(opts = {}) {
|
|
164
|
+
return new SafeActionBuilder({
|
|
165
|
+
middlewares: [],
|
|
166
|
+
metadata: {},
|
|
167
|
+
handleServerError: opts.handleServerError
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ValidationErrors } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Throw this inside an action handler or middleware to return a typed
|
|
4
|
+
* server error to the client.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* throw new ActionError('Not enough credits')
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare class ActionError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Throw this inside an action handler to return per-field validation
|
|
15
|
+
* errors to the client (useful when Zod alone isn't enough).
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* throw new ActionValidationError({
|
|
19
|
+
* email: ['This email is already taken'],
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class ActionValidationError extends Error {
|
|
24
|
+
readonly validationErrors: ValidationErrors;
|
|
25
|
+
constructor(validationErrors: ValidationErrors);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Utility to throw validation errors from within an action handler.
|
|
29
|
+
* Shorthand for `throw new ActionValidationError(errors)`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function returnValidationErrors(errors: ValidationErrors): never;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class ActionError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "ActionError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ActionValidationError extends Error {
|
|
8
|
+
validationErrors;
|
|
9
|
+
constructor(validationErrors) {
|
|
10
|
+
super("Validation failed");
|
|
11
|
+
this.name = "ActionValidationError";
|
|
12
|
+
this.validationErrors = validationErrors;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function returnValidationErrors(errors) {
|
|
16
|
+
throw new ActionValidationError(errors);
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SafeAction } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Nitro event handler for a given safe action.
|
|
4
|
+
* Used internally by the module to generate route handlers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createActionHandler(action: SafeAction<any, any, any>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../types.js").ActionResult<any, any>>>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createSafeActionClient } from './createSafeActionClient.js';
|
|
2
|
+
export { ActionError, ActionValidationError, returnValidationErrors } from './errors.js';
|
|
3
|
+
export type { SafeAction, SafeActionReference, InferSafeActionInput, InferSafeActionOutput, InferSafeActionServerError, } from '../types.js';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { ZodType } from 'zod';
|
|
3
|
+
export interface ActionResult<TOutput, TServerError = string> {
|
|
4
|
+
data?: TOutput;
|
|
5
|
+
serverError?: TServerError;
|
|
6
|
+
validationErrors?: ValidationErrors;
|
|
7
|
+
}
|
|
8
|
+
export type ValidationErrors = Record<string, string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* A safe action carries phantom types for input/output inference plus
|
|
11
|
+
* an internal `_execute` method used by the generated Nitro handler.
|
|
12
|
+
*
|
|
13
|
+
* On the **client** side the runtime value is a lightweight reference
|
|
14
|
+
* `{ __safeActionPath: string }` — the types are applied via a generated
|
|
15
|
+
* declaration file so that `useAction` can infer `TInput` and `TOutput`.
|
|
16
|
+
*/
|
|
17
|
+
export interface SafeAction<TInput = unknown, TOutput = unknown, TServerError = string> {
|
|
18
|
+
/** phantom field — carries generic types for inference, never set at runtime */
|
|
19
|
+
readonly _types: {
|
|
20
|
+
input: TInput;
|
|
21
|
+
output: TOutput;
|
|
22
|
+
serverError: TServerError;
|
|
23
|
+
};
|
|
24
|
+
/** used by the generated Nitro handler */
|
|
25
|
+
_execute: (rawInput: unknown, event: H3Event) => Promise<ActionResult<TOutput, TServerError>>;
|
|
26
|
+
}
|
|
27
|
+
export interface SafeActionReference<TInput = unknown, TOutput = unknown, TServerError = string> {
|
|
28
|
+
readonly __safeActionPath: string;
|
|
29
|
+
/** phantom field — carries generic types for inference, never set at runtime */
|
|
30
|
+
readonly _types: {
|
|
31
|
+
input: TInput;
|
|
32
|
+
output: TOutput;
|
|
33
|
+
serverError: TServerError;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface MiddlewareArgs<TCtx> {
|
|
37
|
+
ctx: TCtx;
|
|
38
|
+
clientInput: unknown;
|
|
39
|
+
metadata: ActionMetadata;
|
|
40
|
+
event: H3Event;
|
|
41
|
+
next: <TNewCtx>(opts: {
|
|
42
|
+
ctx: TNewCtx;
|
|
43
|
+
}) => Promise<MiddlewareResult<TNewCtx>>;
|
|
44
|
+
}
|
|
45
|
+
export interface MiddlewareResult<TCtx> {
|
|
46
|
+
ctx: TCtx;
|
|
47
|
+
}
|
|
48
|
+
export type MiddlewareFn<TCtxIn = Record<string, unknown>, TCtxOut = TCtxIn> = (args: MiddlewareArgs<TCtxIn>) => Promise<MiddlewareResult<TCtxOut>>;
|
|
49
|
+
export interface ActionHandlerArgs<TCtx, TInput> {
|
|
50
|
+
parsedInput: TInput;
|
|
51
|
+
ctx: TCtx;
|
|
52
|
+
event: H3Event;
|
|
53
|
+
}
|
|
54
|
+
export type ActionHandler<TCtx, TInput, TOutput> = (args: ActionHandlerArgs<TCtx, TInput>) => Promise<TOutput> | TOutput;
|
|
55
|
+
export type ActionMetadata = Record<string, unknown>;
|
|
56
|
+
export interface SafeActionClientOpts<TServerError = string> {
|
|
57
|
+
handleServerError?: (error: Error) => TServerError;
|
|
58
|
+
}
|
|
59
|
+
export interface ActionConfig<TServerError = string> {
|
|
60
|
+
middlewares: MiddlewareFn<any, any>[];
|
|
61
|
+
inputSchema?: ZodType;
|
|
62
|
+
outputSchema?: ZodType;
|
|
63
|
+
metadata: ActionMetadata;
|
|
64
|
+
handler: ActionHandler<any, any, any>;
|
|
65
|
+
handleServerError?: (error: Error) => TServerError;
|
|
66
|
+
}
|
|
67
|
+
export type ActionStatus = 'idle' | 'executing' | 'hasSucceeded' | 'hasErrored';
|
|
68
|
+
export type InferSafeActionInput<T> = T extends SafeAction<infer I, any, any> ? I : T extends SafeActionReference<infer I, any, any> ? I : never;
|
|
69
|
+
export type InferSafeActionOutput<T> = T extends SafeAction<any, infer O, any> ? O : T extends SafeActionReference<any, infer O, any> ? O : never;
|
|
70
|
+
export type InferSafeActionServerError<T> = T extends SafeAction<any, any, infer E> ? E : T extends SafeActionReference<any, any, infer E> ? E : string;
|
|
71
|
+
export interface UseActionReturn<TInput, TOutput, TServerError = string> {
|
|
72
|
+
execute: (input: TInput) => void;
|
|
73
|
+
executeAsync: (input: TInput) => Promise<ActionResult<TOutput, TServerError>>;
|
|
74
|
+
data: Readonly<import('vue').Ref<TOutput | undefined>>;
|
|
75
|
+
serverError: Readonly<import('vue').Ref<TServerError | undefined>>;
|
|
76
|
+
validationErrors: Readonly<import('vue').Ref<ValidationErrors | undefined>>;
|
|
77
|
+
status: Readonly<import('vue').Ref<ActionStatus>>;
|
|
78
|
+
isIdle: import('vue').ComputedRef<boolean>;
|
|
79
|
+
isExecuting: import('vue').ComputedRef<boolean>;
|
|
80
|
+
hasSucceeded: import('vue').ComputedRef<boolean>;
|
|
81
|
+
hasErrored: import('vue').ComputedRef<boolean>;
|
|
82
|
+
reset: () => void;
|
|
83
|
+
}
|
|
84
|
+
export interface UseActionCallbacks<TInput, TOutput, TServerError = string> {
|
|
85
|
+
onSuccess?: (args: {
|
|
86
|
+
data: TOutput;
|
|
87
|
+
input: TInput;
|
|
88
|
+
}) => void;
|
|
89
|
+
onError?: (args: {
|
|
90
|
+
error: {
|
|
91
|
+
serverError?: TServerError;
|
|
92
|
+
validationErrors?: ValidationErrors;
|
|
93
|
+
};
|
|
94
|
+
input: TInput;
|
|
95
|
+
}) => void;
|
|
96
|
+
onSettled?: (args: {
|
|
97
|
+
result: ActionResult<TOutput, TServerError>;
|
|
98
|
+
input: TInput;
|
|
99
|
+
}) => void;
|
|
100
|
+
onExecute?: (args: {
|
|
101
|
+
input: TInput;
|
|
102
|
+
}) => void;
|
|
103
|
+
}
|
|
File without changes
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-safe-action",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Type-safe and validated server actions for Nuxt",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/rutbergphilip/nuxt-safe-action.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/rutbergphilip/nuxt-safe-action",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/rutbergphilip/nuxt-safe-action/issues"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"nuxt",
|
|
15
|
+
"nuxt-module",
|
|
16
|
+
"server-actions",
|
|
17
|
+
"type-safe",
|
|
18
|
+
"validation",
|
|
19
|
+
"zod",
|
|
20
|
+
"middleware",
|
|
21
|
+
"vue"
|
|
22
|
+
],
|
|
23
|
+
"author": "Philip Rutberg",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/types.d.mts",
|
|
29
|
+
"import": "./dist/module.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/module.mjs",
|
|
33
|
+
"typesVersions": {
|
|
34
|
+
"*": {
|
|
35
|
+
".": [
|
|
36
|
+
"./dist/types.d.mts"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"prepack": "nuxt-module-build build",
|
|
45
|
+
"dev": "npm run dev:prepare && nuxt dev playground",
|
|
46
|
+
"dev:build": "nuxt build playground",
|
|
47
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
48
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
49
|
+
"lint": "eslint .",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest watch",
|
|
52
|
+
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@nuxt/kit": "^4.3.1",
|
|
56
|
+
"defu": "^6.1.4"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@nuxt/devtools": "^3.1.1",
|
|
60
|
+
"@nuxt/eslint-config": "^1.14.0",
|
|
61
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
62
|
+
"@nuxt/schema": "^4.3.1",
|
|
63
|
+
"@nuxt/test-utils": "^4.0.0",
|
|
64
|
+
"@types/node": "latest",
|
|
65
|
+
"changelogen": "^0.6.2",
|
|
66
|
+
"eslint": "^10.0.0",
|
|
67
|
+
"eslint-config-prettier": "^10.1.8",
|
|
68
|
+
"nuxt": "^4.3.1",
|
|
69
|
+
"typescript": "~5.9.3",
|
|
70
|
+
"vitest": "^4.0.18",
|
|
71
|
+
"vue-tsc": "^3.2.4",
|
|
72
|
+
"zod": "^3.24.0"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"zod": "^3.0.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"zod": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|