vite-plugin-server-actions 1.0.0 → 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.
@@ -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 be registered with openapi metadata
83
- // For simple conversion, we'll create a temporary registry
84
- const extendedSchema = schema.openapi ? schema : schema;
85
- registry.register(schemaName, extendedSchema);
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 { type: "object", description: "Schema conversion not supported" };
105
+ return this._basicZodToOpenAPI(schema);
97
106
  }
98
107
 
99
108
  return openAPISchema;
100
109
  } catch (error) {
101
- console.warn(`Failed to convert Zod schema to OpenAPI: ${error.message}`);
102
- return { type: "object", description: "Schema conversion failed" };
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
- const adapter = options.adapter || new ZodAdapter();
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.status(400).json({
258
- error: "Validation failed",
259
- details: "Request body must be a non-empty array of function arguments",
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.status(400).json({
277
- error: "Validation failed",
278
- details: result.errors,
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.status(500).json({
294
- error: "Internal validation error",
295
- details: error.message,
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
  }