vite-plugin-server-actions 1.0.1 → 1.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 +162 -52
- package/index.d.ts +76 -32
- package/package.json +23 -14
- package/src/ast-parser.js +535 -0
- package/src/build-utils.js +10 -12
- package/src/dev-validator.js +272 -0
- package/src/error-enhancer.js +283 -0
- package/src/index.js +428 -80
- package/src/openapi.js +67 -29
- package/src/security.js +118 -0
- package/src/type-generator.js +378 -0
- package/src/types.ts +1 -1
- package/src/validation-runtime.js +100 -0
- package/src/validation.js +126 -21
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation code that gets bundled with the production server
|
|
3
|
+
* This avoids the need for relative imports from src/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simple schema discovery for production
|
|
8
|
+
*/
|
|
9
|
+
export class SchemaDiscovery {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.schemas = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerSchema(moduleName, functionName, schema) {
|
|
15
|
+
const key = `${moduleName}.${functionName}`;
|
|
16
|
+
this.schemas.set(key, schema);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getSchema(moduleName, functionName) {
|
|
20
|
+
const key = `${moduleName}.${functionName}`;
|
|
21
|
+
return this.schemas.get(key) || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getAllSchemas() {
|
|
25
|
+
return new Map(this.schemas);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validation middleware for production
|
|
31
|
+
*/
|
|
32
|
+
export function createValidationMiddleware(options = {}) {
|
|
33
|
+
const schemaDiscovery = options.schemaDiscovery || new SchemaDiscovery();
|
|
34
|
+
|
|
35
|
+
return async function validationMiddleware(req, res, next) {
|
|
36
|
+
let moduleName, functionName, schema;
|
|
37
|
+
|
|
38
|
+
// Check for context from route setup
|
|
39
|
+
if (req.validationContext) {
|
|
40
|
+
moduleName = req.validationContext.moduleName;
|
|
41
|
+
functionName = req.validationContext.functionName;
|
|
42
|
+
schema = req.validationContext.schema;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!schema) {
|
|
46
|
+
// No schema defined, skip validation
|
|
47
|
+
return next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Request body should be an array of arguments for server functions
|
|
52
|
+
if (!Array.isArray(req.body) || req.body.length === 0) {
|
|
53
|
+
return res.status(400).json({
|
|
54
|
+
error: "Validation failed",
|
|
55
|
+
message: "Request body must be a non-empty array of function arguments",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate based on schema type
|
|
60
|
+
let validationData;
|
|
61
|
+
if (schema._def?.typeName === "ZodTuple") {
|
|
62
|
+
// Schema expects multiple arguments (tuple)
|
|
63
|
+
validationData = req.body;
|
|
64
|
+
} else {
|
|
65
|
+
// Schema expects single argument (first element of array)
|
|
66
|
+
validationData = req.body[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate request body using Zod
|
|
70
|
+
if (schema.parse) {
|
|
71
|
+
// It's a Zod schema
|
|
72
|
+
const validatedData = await schema.parseAsync(validationData);
|
|
73
|
+
|
|
74
|
+
// Replace request body with validated data
|
|
75
|
+
if (schema._def?.typeName === "ZodTuple") {
|
|
76
|
+
req.body = validatedData;
|
|
77
|
+
} else {
|
|
78
|
+
req.body = [validatedData];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
next();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Validation failed
|
|
84
|
+
if (error.errors) {
|
|
85
|
+
// Zod validation error
|
|
86
|
+
return res.status(400).json({
|
|
87
|
+
error: "Validation failed",
|
|
88
|
+
details: error.errors,
|
|
89
|
+
message: error.message,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Other validation error
|
|
94
|
+
return res.status(400).json({
|
|
95
|
+
error: "Validation failed",
|
|
96
|
+
message: error.message,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/validation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { createErrorResponse } from "./security.js";
|
|
2
3
|
import { extendZodWithOpenApi, OpenAPIRegistry, OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
|
|
3
4
|
|
|
4
5
|
// Extend Zod with OpenAPI support
|
|
@@ -79,10 +80,18 @@ export class ZodAdapter extends ValidationAdapter {
|
|
|
79
80
|
const registry = new OpenAPIRegistry();
|
|
80
81
|
const schemaName = "_TempSchema";
|
|
81
82
|
|
|
82
|
-
// The library requires schemas to
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// The library requires schemas to have .openapi() metadata
|
|
84
|
+
// If the schema doesn't have it, add it dynamically
|
|
85
|
+
let schemaToRegister = schema;
|
|
86
|
+
if (typeof schema.openapi === "function") {
|
|
87
|
+
// Add openapi metadata with the temp schema name
|
|
88
|
+
schemaToRegister = schema.openapi(schemaName);
|
|
89
|
+
} else {
|
|
90
|
+
// Schema wasn't created with extendZodWithOpenApi - use basic fallback
|
|
91
|
+
return this._basicZodToOpenAPI(schema);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
registry.register(schemaName, schemaToRegister);
|
|
86
95
|
|
|
87
96
|
// Generate the OpenAPI components
|
|
88
97
|
const generator = new OpenApiGeneratorV3(registry.definitions);
|
|
@@ -93,13 +102,79 @@ export class ZodAdapter extends ValidationAdapter {
|
|
|
93
102
|
|
|
94
103
|
if (!openAPISchema) {
|
|
95
104
|
// Fallback for schemas that couldn't be converted
|
|
96
|
-
return
|
|
105
|
+
return this._basicZodToOpenAPI(schema);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
return openAPISchema;
|
|
100
109
|
} catch (error) {
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
// Only log warning for unexpected errors
|
|
111
|
+
if (process.env.NODE_ENV === "development") {
|
|
112
|
+
console.warn(`Failed to convert Zod schema to OpenAPI: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
return this._basicZodToOpenAPI(schema);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Basic Zod to OpenAPI conversion for schemas without .openapi() extension
|
|
120
|
+
* @param {any} schema - Zod schema
|
|
121
|
+
* @returns {object} OpenAPI schema
|
|
122
|
+
*/
|
|
123
|
+
_basicZodToOpenAPI(schema) {
|
|
124
|
+
if (!schema || !schema._def) {
|
|
125
|
+
return { type: "object", description: "Unknown schema" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const typeName = schema._def.typeName;
|
|
129
|
+
|
|
130
|
+
switch (typeName) {
|
|
131
|
+
case "ZodString":
|
|
132
|
+
return { type: "string" };
|
|
133
|
+
case "ZodNumber":
|
|
134
|
+
return { type: "number" };
|
|
135
|
+
case "ZodBoolean":
|
|
136
|
+
return { type: "boolean" };
|
|
137
|
+
case "ZodArray":
|
|
138
|
+
return {
|
|
139
|
+
type: "array",
|
|
140
|
+
items: this._basicZodToOpenAPI(schema._def.type),
|
|
141
|
+
};
|
|
142
|
+
case "ZodObject": {
|
|
143
|
+
const shape = schema._def.shape ? schema._def.shape() : {};
|
|
144
|
+
const properties = {};
|
|
145
|
+
const required = [];
|
|
146
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
147
|
+
properties[key] = this._basicZodToOpenAPI(value);
|
|
148
|
+
if (!value.isOptional?.()) {
|
|
149
|
+
required.push(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties,
|
|
155
|
+
...(required.length > 0 ? { required } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
case "ZodOptional":
|
|
159
|
+
return this._basicZodToOpenAPI(schema._def.innerType);
|
|
160
|
+
case "ZodDefault":
|
|
161
|
+
return this._basicZodToOpenAPI(schema._def.innerType);
|
|
162
|
+
case "ZodEnum":
|
|
163
|
+
return {
|
|
164
|
+
type: "string",
|
|
165
|
+
enum: schema._def.values,
|
|
166
|
+
};
|
|
167
|
+
case "ZodTuple": {
|
|
168
|
+
const items = schema._def.items.map((item) => this._basicZodToOpenAPI(item));
|
|
169
|
+
return {
|
|
170
|
+
type: "array",
|
|
171
|
+
items: items.length === 1 ? items[0] : { oneOf: items },
|
|
172
|
+
minItems: items.length,
|
|
173
|
+
maxItems: items.length,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
default:
|
|
177
|
+
return { type: "object", description: `Zod type: ${typeName}` };
|
|
103
178
|
}
|
|
104
179
|
}
|
|
105
180
|
|
|
@@ -188,6 +263,17 @@ export class SchemaDiscovery {
|
|
|
188
263
|
return this.schemas.get(key) || null;
|
|
189
264
|
}
|
|
190
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Check if schema exists for a function
|
|
268
|
+
* @param {string} moduleName - Module name
|
|
269
|
+
* @param {string} functionName - Function name
|
|
270
|
+
* @returns {boolean} True if schema exists
|
|
271
|
+
*/
|
|
272
|
+
hasSchema(moduleName, functionName) {
|
|
273
|
+
const key = `${moduleName}.${functionName}`;
|
|
274
|
+
return this.schemas.has(key);
|
|
275
|
+
}
|
|
276
|
+
|
|
191
277
|
/**
|
|
192
278
|
* Get all schemas
|
|
193
279
|
* @returns {Map} All registered schemas
|
|
@@ -227,7 +313,17 @@ export class SchemaDiscovery {
|
|
|
227
313
|
* Validation middleware factory
|
|
228
314
|
*/
|
|
229
315
|
export function createValidationMiddleware(options = {}) {
|
|
230
|
-
|
|
316
|
+
// Handle adapter as string key (e.g., "zod") or instance
|
|
317
|
+
let adapter;
|
|
318
|
+
if (typeof options.adapter === "string") {
|
|
319
|
+
const AdapterClass = adapters[options.adapter];
|
|
320
|
+
if (!AdapterClass) {
|
|
321
|
+
throw new Error(`Unknown validation adapter: ${options.adapter}. Available: ${Object.keys(adapters).join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
adapter = new AdapterClass();
|
|
324
|
+
} else {
|
|
325
|
+
adapter = options.adapter || new ZodAdapter();
|
|
326
|
+
}
|
|
231
327
|
const schemaDiscovery = options.schemaDiscovery || new SchemaDiscovery(adapter);
|
|
232
328
|
|
|
233
329
|
return async function validationMiddleware(req, res, next) {
|
|
@@ -254,10 +350,15 @@ export function createValidationMiddleware(options = {}) {
|
|
|
254
350
|
try {
|
|
255
351
|
// Request body should be an array of arguments for server functions
|
|
256
352
|
if (!Array.isArray(req.body) || req.body.length === 0) {
|
|
257
|
-
return res
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
353
|
+
return res
|
|
354
|
+
.status(400)
|
|
355
|
+
.json(
|
|
356
|
+
createErrorResponse(
|
|
357
|
+
400,
|
|
358
|
+
"Request body must be a non-empty array of function arguments",
|
|
359
|
+
"INVALID_REQUEST_BODY",
|
|
360
|
+
),
|
|
361
|
+
);
|
|
261
362
|
}
|
|
262
363
|
|
|
263
364
|
// Validate based on schema type
|
|
@@ -273,11 +374,9 @@ export function createValidationMiddleware(options = {}) {
|
|
|
273
374
|
const result = await adapter.validate(schema, validationData);
|
|
274
375
|
|
|
275
376
|
if (!result.success) {
|
|
276
|
-
return res
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
validationErrors: result.errors,
|
|
280
|
-
});
|
|
377
|
+
return res
|
|
378
|
+
.status(400)
|
|
379
|
+
.json(createErrorResponse(400, "Validation failed", "VALIDATION_ERROR", { validationErrors: result.errors }));
|
|
281
380
|
}
|
|
282
381
|
|
|
283
382
|
// Replace request body with validated data
|
|
@@ -290,10 +389,16 @@ export function createValidationMiddleware(options = {}) {
|
|
|
290
389
|
next();
|
|
291
390
|
} catch (error) {
|
|
292
391
|
console.error("Validation middleware error:", error);
|
|
293
|
-
res
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
392
|
+
res
|
|
393
|
+
.status(500)
|
|
394
|
+
.json(
|
|
395
|
+
createErrorResponse(
|
|
396
|
+
500,
|
|
397
|
+
"Internal validation error",
|
|
398
|
+
"VALIDATION_INTERNAL_ERROR",
|
|
399
|
+
process.env.NODE_ENV !== "production" ? { message: error.message, stack: error.stack } : null,
|
|
400
|
+
),
|
|
401
|
+
);
|
|
297
402
|
}
|
|
298
403
|
};
|
|
299
404
|
}
|