@superdangerous/app-framework 4.9.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/LICENSE +21 -0
- package/README.md +652 -0
- package/dist/api/logsRouter.d.ts +20 -0
- package/dist/api/logsRouter.d.ts.map +1 -0
- package/dist/api/logsRouter.js +515 -0
- package/dist/api/logsRouter.js.map +1 -0
- package/dist/cli/dev-server.d.ts +7 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +640 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/StandardServer.d.ts +129 -0
- package/dist/core/StandardServer.d.ts.map +1 -0
- package/dist/core/StandardServer.js +453 -0
- package/dist/core/StandardServer.js.map +1 -0
- package/dist/core/apiResponse.d.ts +69 -0
- package/dist/core/apiResponse.d.ts.map +1 -0
- package/dist/core/apiResponse.js +127 -0
- package/dist/core/apiResponse.js.map +1 -0
- package/dist/core/healthCheck.d.ts +160 -0
- package/dist/core/healthCheck.d.ts.map +1 -0
- package/dist/core/healthCheck.js +398 -0
- package/dist/core/healthCheck.js.map +1 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +40 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +117 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +826 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/portUtils.d.ts +71 -0
- package/dist/core/portUtils.d.ts.map +1 -0
- package/dist/core/portUtils.js +240 -0
- package/dist/core/portUtils.js.map +1 -0
- package/dist/core/storageService.d.ts +119 -0
- package/dist/core/storageService.d.ts.map +1 -0
- package/dist/core/storageService.js +405 -0
- package/dist/core/storageService.js.map +1 -0
- package/dist/desktop/bundler.d.ts +40 -0
- package/dist/desktop/bundler.d.ts.map +1 -0
- package/dist/desktop/bundler.js +176 -0
- package/dist/desktop/bundler.js.map +1 -0
- package/dist/desktop/index.d.ts +25 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/index.js +15 -0
- package/dist/desktop/index.js.map +1 -0
- package/dist/desktop/native-modules.d.ts +66 -0
- package/dist/desktop/native-modules.d.ts.map +1 -0
- package/dist/desktop/native-modules.js +200 -0
- package/dist/desktop/native-modules.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/LogCategories.d.ts +87 -0
- package/dist/logging/LogCategories.d.ts.map +1 -0
- package/dist/logging/LogCategories.js +205 -0
- package/dist/logging/LogCategories.js.map +1 -0
- package/dist/middleware/aiErrorHandler.d.ts +31 -0
- package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
- package/dist/middleware/aiErrorHandler.js +181 -0
- package/dist/middleware/aiErrorHandler.js.map +1 -0
- package/dist/middleware/auth.d.ts +101 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +230 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +56 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +123 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +13 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +85 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/fileUpload.d.ts +62 -0
- package/dist/middleware/fileUpload.d.ts.map +1 -0
- package/dist/middleware/fileUpload.js +175 -0
- package/dist/middleware/fileUpload.js.map +1 -0
- package/dist/middleware/health.d.ts +48 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +143 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/index.d.ts +20 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/openapi.d.ts +64 -0
- package/dist/middleware/openapi.d.ts.map +1 -0
- package/dist/middleware/openapi.js +258 -0
- package/dist/middleware/openapi.js.map +1 -0
- package/dist/middleware/requestLogging.d.ts +22 -0
- package/dist/middleware/requestLogging.d.ts.map +1 -0
- package/dist/middleware/requestLogging.js +61 -0
- package/dist/middleware/requestLogging.js.map +1 -0
- package/dist/middleware/session.d.ts +84 -0
- package/dist/middleware/session.d.ts.map +1 -0
- package/dist/middleware/session.js +189 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/validation.d.ts +1337 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +483 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/services/aiService.d.ts +180 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +547 -0
- package/dist/services/aiService.js.map +1 -0
- package/dist/services/conversationStorage.d.ts +38 -0
- package/dist/services/conversationStorage.d.ts.map +1 -0
- package/dist/services/conversationStorage.js +158 -0
- package/dist/services/conversationStorage.js.map +1 -0
- package/dist/services/crossPlatformBuffer.d.ts +84 -0
- package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
- package/dist/services/crossPlatformBuffer.js +246 -0
- package/dist/services/crossPlatformBuffer.js.map +1 -0
- package/dist/services/index.d.ts +17 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/networkService.d.ts +81 -0
- package/dist/services/networkService.d.ts.map +1 -0
- package/dist/services/networkService.js +268 -0
- package/dist/services/networkService.js.map +1 -0
- package/dist/services/queueService.d.ts +112 -0
- package/dist/services/queueService.d.ts.map +1 -0
- package/dist/services/queueService.js +338 -0
- package/dist/services/queueService.js.map +1 -0
- package/dist/services/settingsService.d.ts +135 -0
- package/dist/services/settingsService.d.ts.map +1 -0
- package/dist/services/settingsService.js +425 -0
- package/dist/services/settingsService.js.map +1 -0
- package/dist/services/systemMonitor.d.ts +208 -0
- package/dist/services/systemMonitor.d.ts.map +1 -0
- package/dist/services/systemMonitor.js +693 -0
- package/dist/services/systemMonitor.js.map +1 -0
- package/dist/services/updateService.d.ts +78 -0
- package/dist/services/updateService.d.ts.map +1 -0
- package/dist/services/updateService.js +252 -0
- package/dist/services/updateService.js.map +1 -0
- package/dist/services/websocketEvents.d.ts +372 -0
- package/dist/services/websocketEvents.d.ts.map +1 -0
- package/dist/services/websocketEvents.js +338 -0
- package/dist/services/websocketEvents.js.map +1 -0
- package/dist/services/websocketServer.d.ts +80 -0
- package/dist/services/websocketServer.d.ts.map +1 -0
- package/dist/services/websocketServer.js +299 -0
- package/dist/services/websocketServer.js.map +1 -0
- package/dist/settings/SettingsSchema.d.ts +151 -0
- package/dist/settings/SettingsSchema.d.ts.map +1 -0
- package/dist/settings/SettingsSchema.js +424 -0
- package/dist/settings/SettingsSchema.js.map +1 -0
- package/dist/testing/TestServer.d.ts +69 -0
- package/dist/testing/TestServer.d.ts.map +1 -0
- package/dist/testing/TestServer.js +250 -0
- package/dist/testing/TestServer.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/appPaths.d.ts +74 -0
- package/dist/utils/appPaths.d.ts.map +1 -0
- package/dist/utils/appPaths.js +162 -0
- package/dist/utils/appPaths.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +50 -0
- package/dist/utils/fs-utils.d.ts.map +1 -0
- package/dist/utils/fs-utils.js +114 -0
- package/dist/utils/fs-utils.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/standardConfig.d.ts +61 -0
- package/dist/utils/standardConfig.d.ts.map +1 -0
- package/dist/utils/standardConfig.js +109 -0
- package/dist/utils/standardConfig.js.map +1 -0
- package/dist/utils/startupBanner.d.ts +34 -0
- package/dist/utils/startupBanner.d.ts.map +1 -0
- package/dist/utils/startupBanner.js +169 -0
- package/dist/utils/startupBanner.js.map +1 -0
- package/dist/utils/startupLogger.d.ts +45 -0
- package/dist/utils/startupLogger.d.ts.map +1 -0
- package/dist/utils/startupLogger.js +200 -0
- package/dist/utils/startupLogger.js.map +1 -0
- package/package.json +151 -0
- package/src/api/logsRouter.ts +600 -0
- package/src/cli/dev-server.ts +803 -0
- package/src/cli/index.ts +31 -0
- package/src/core/StandardServer.ts +587 -0
- package/src/core/apiResponse.ts +202 -0
- package/src/core/healthCheck.ts +565 -0
- package/src/core/index.ts +80 -0
- package/src/core/logger.ts +1092 -0
- package/src/core/portUtils.ts +319 -0
- package/src/core/storageService.ts +595 -0
- package/src/desktop/bundler.ts +271 -0
- package/src/desktop/index.ts +18 -0
- package/src/desktop/native-modules.ts +289 -0
- package/src/index.ts +142 -0
- package/src/logging/LogCategories.ts +302 -0
- package/src/middleware/aiErrorHandler.ts +278 -0
- package/src/middleware/auth.ts +329 -0
- package/src/middleware/cors.ts +187 -0
- package/src/middleware/errorHandler.ts +103 -0
- package/src/middleware/fileUpload.ts +252 -0
- package/src/middleware/health.ts +206 -0
- package/src/middleware/index.ts +71 -0
- package/src/middleware/openapi.ts +305 -0
- package/src/middleware/requestLogging.ts +92 -0
- package/src/middleware/session.ts +238 -0
- package/src/middleware/validation.ts +603 -0
- package/src/services/aiService.ts +789 -0
- package/src/services/conversationStorage.ts +232 -0
- package/src/services/crossPlatformBuffer.ts +341 -0
- package/src/services/index.ts +47 -0
- package/src/services/networkService.ts +351 -0
- package/src/services/queueService.ts +446 -0
- package/src/services/settingsService.ts +549 -0
- package/src/services/systemMonitor.ts +936 -0
- package/src/services/updateService.ts +334 -0
- package/src/services/websocketEvents.ts +409 -0
- package/src/services/websocketServer.ts +394 -0
- package/src/settings/SettingsSchema.ts +664 -0
- package/src/testing/TestServer.ts +312 -0
- package/src/types/index.ts +154 -0
- package/src/utils/appPaths.ts +196 -0
- package/src/utils/fs-utils.ts +130 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/standardConfig.ts +178 -0
- package/src/utils/startupBanner.ts +287 -0
- package/src/utils/startupLogger.ts +268 -0
- package/ui/dist/index.d.mts +1221 -0
- package/ui/dist/index.d.ts +1221 -0
- package/ui/dist/index.js +73 -0
- package/ui/dist/index.js.map +1 -0
- package/ui/dist/index.mjs +73 -0
- package/ui/dist/index.mjs.map +1 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Middleware
|
|
3
|
+
* Request validation using Zod schemas with TypeScript support
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Request body, params, and query validation
|
|
7
|
+
* - Detailed field-level error reporting
|
|
8
|
+
* - Custom error messages
|
|
9
|
+
* - Async validation support
|
|
10
|
+
* - Schema composition and reuse
|
|
11
|
+
* - Full TypeScript type inference
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z, ZodSchema, ZodError } from "zod";
|
|
15
|
+
import { Request, Response, NextFunction } from "express";
|
|
16
|
+
import { ApiResponse, FieldValidationError } from "../types/index.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validation options for middleware
|
|
20
|
+
*/
|
|
21
|
+
export interface ValidationOptions {
|
|
22
|
+
abortEarly?: boolean;
|
|
23
|
+
stripUnknown?: boolean;
|
|
24
|
+
convert?: boolean;
|
|
25
|
+
presence?: "optional" | "required" | "forbidden";
|
|
26
|
+
context?: Record<string, any>;
|
|
27
|
+
allowUnknown?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default validation options
|
|
32
|
+
*/
|
|
33
|
+
const defaultOptions: ValidationOptions = {
|
|
34
|
+
abortEarly: false,
|
|
35
|
+
stripUnknown: true,
|
|
36
|
+
convert: true,
|
|
37
|
+
allowUnknown: false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format validation errors into a consistent structure
|
|
42
|
+
*/
|
|
43
|
+
function formatValidationErrors(error: ZodError): FieldValidationError[] {
|
|
44
|
+
return error.errors.map((err) => ({
|
|
45
|
+
field: err.path.join("."),
|
|
46
|
+
message: err.message,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a user-friendly error message
|
|
52
|
+
*/
|
|
53
|
+
function createErrorMessage(
|
|
54
|
+
errors: FieldValidationError[],
|
|
55
|
+
context: string,
|
|
56
|
+
): string {
|
|
57
|
+
if (errors.length === 1) {
|
|
58
|
+
return `${context}: ${errors[0].message}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fieldList = errors.map((e) => e.field).join(", ");
|
|
62
|
+
return `${context}: Multiple validation errors in fields: ${fieldList}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validation schemas for different endpoints
|
|
67
|
+
*/
|
|
68
|
+
export const schemas = {
|
|
69
|
+
// Template validation
|
|
70
|
+
template: z.object({
|
|
71
|
+
id: z
|
|
72
|
+
.string()
|
|
73
|
+
.regex(
|
|
74
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
75
|
+
"ID must contain only letters, numbers, underscores, and hyphens",
|
|
76
|
+
),
|
|
77
|
+
name: z.string().min(1, "Name is required").max(100),
|
|
78
|
+
description: z.string().max(500).optional(),
|
|
79
|
+
manufacturer: z.string().min(1, "Manufacturer is required"),
|
|
80
|
+
model: z.string().optional(),
|
|
81
|
+
protocol: z.enum(["modbus", "bacnet"]).default("modbus"),
|
|
82
|
+
metadata: z
|
|
83
|
+
.object({
|
|
84
|
+
manufacturer: z.string().optional(),
|
|
85
|
+
models: z
|
|
86
|
+
.array(
|
|
87
|
+
z.object({
|
|
88
|
+
name: z.string(),
|
|
89
|
+
variants: z.array(z.string()).optional(),
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
.optional(),
|
|
93
|
+
category: z.string().optional(),
|
|
94
|
+
tags: z.array(z.string()).optional(),
|
|
95
|
+
source: z.string().optional(),
|
|
96
|
+
lastUpdated: z.string().datetime().optional(),
|
|
97
|
+
version: z.string().optional(),
|
|
98
|
+
author: z.string().optional(),
|
|
99
|
+
})
|
|
100
|
+
.optional(),
|
|
101
|
+
data_points: z
|
|
102
|
+
.array(
|
|
103
|
+
z.object({
|
|
104
|
+
name: z.string(),
|
|
105
|
+
address: z.number().int().min(0).max(65535),
|
|
106
|
+
type: z.enum(["coil", "discrete", "holding", "input"]),
|
|
107
|
+
dataType: z.enum([
|
|
108
|
+
"uint16",
|
|
109
|
+
"int16",
|
|
110
|
+
"uint32",
|
|
111
|
+
"int32",
|
|
112
|
+
"float32",
|
|
113
|
+
"string",
|
|
114
|
+
"bool",
|
|
115
|
+
"bits",
|
|
116
|
+
]),
|
|
117
|
+
writable: z.boolean().default(false),
|
|
118
|
+
unit: z.string().optional(),
|
|
119
|
+
min: z.number().optional(),
|
|
120
|
+
max: z.number().optional(),
|
|
121
|
+
scaling_factor: z.number().default(1),
|
|
122
|
+
description: z.string().optional(),
|
|
123
|
+
simulation_type: z
|
|
124
|
+
.enum(["static", "random", "csv", "function"])
|
|
125
|
+
.optional(),
|
|
126
|
+
initial_value: z.any().optional(),
|
|
127
|
+
csv_file: z.string().optional(),
|
|
128
|
+
function_code: z.string().optional(),
|
|
129
|
+
update_interval: z.number().int().min(100).optional(),
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
.min(1, "At least one data point is required"),
|
|
133
|
+
devices: z
|
|
134
|
+
.array(
|
|
135
|
+
z.object({
|
|
136
|
+
name: z.string(),
|
|
137
|
+
device_type: z.string(),
|
|
138
|
+
description: z.string().optional(),
|
|
139
|
+
unit_id: z.number().int().min(1).max(247).optional(),
|
|
140
|
+
data_points: z.array(z.any()).optional(),
|
|
141
|
+
}),
|
|
142
|
+
)
|
|
143
|
+
.optional(),
|
|
144
|
+
attachments: z
|
|
145
|
+
.array(
|
|
146
|
+
z.object({
|
|
147
|
+
id: z.string(),
|
|
148
|
+
name: z.string(),
|
|
149
|
+
size: z.number(),
|
|
150
|
+
mimeType: z.string(),
|
|
151
|
+
uploadedAt: z.string().datetime(),
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
.optional(),
|
|
155
|
+
intelligent: z.boolean().optional(),
|
|
156
|
+
state_machine: z
|
|
157
|
+
.object({
|
|
158
|
+
states: z.array(
|
|
159
|
+
z.object({
|
|
160
|
+
name: z.string(),
|
|
161
|
+
values: z.record(z.any()),
|
|
162
|
+
transitions: z.array(z.any()),
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
initial_state: z.string(),
|
|
166
|
+
current_state: z.string().optional(),
|
|
167
|
+
})
|
|
168
|
+
.optional(),
|
|
169
|
+
data_links: z
|
|
170
|
+
.array(
|
|
171
|
+
z.object({
|
|
172
|
+
source: z.string(),
|
|
173
|
+
targets: z.array(z.string()),
|
|
174
|
+
transform: z.string().optional(),
|
|
175
|
+
}),
|
|
176
|
+
)
|
|
177
|
+
.optional(),
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
// Template update validation (partial)
|
|
181
|
+
templateUpdate: z
|
|
182
|
+
.object({
|
|
183
|
+
name: z.string().min(1).max(100).optional(),
|
|
184
|
+
description: z.string().max(500).optional(),
|
|
185
|
+
manufacturer: z.string().min(1).optional(),
|
|
186
|
+
model: z.string().optional(),
|
|
187
|
+
metadata: z.any().optional(),
|
|
188
|
+
data_points: z.array(z.any()).optional(),
|
|
189
|
+
devices: z.array(z.any()).optional(),
|
|
190
|
+
attachments: z.array(z.any()).optional(),
|
|
191
|
+
intelligent: z.boolean().optional(),
|
|
192
|
+
state_machine: z.any().optional(),
|
|
193
|
+
data_links: z.array(z.any()).optional(),
|
|
194
|
+
})
|
|
195
|
+
.refine((obj) => Object.keys(obj).length > 0, {
|
|
196
|
+
message: "At least one field must be provided for update",
|
|
197
|
+
}),
|
|
198
|
+
|
|
199
|
+
// Simulator validation
|
|
200
|
+
simulator: z.object({
|
|
201
|
+
id: z.string().uuid(),
|
|
202
|
+
name: z.string().min(1).max(100),
|
|
203
|
+
templateId: z.string(),
|
|
204
|
+
config: z.object({
|
|
205
|
+
port: z.number().int().min(1).max(65535),
|
|
206
|
+
host: z.string().default("0.0.0.0"),
|
|
207
|
+
unitId: z.number().int().min(1).max(247).default(1),
|
|
208
|
+
protocol: z.enum(["modbus", "bacnet"]),
|
|
209
|
+
}),
|
|
210
|
+
autoStart: z.boolean().default(false),
|
|
211
|
+
}),
|
|
212
|
+
|
|
213
|
+
// Settings validation
|
|
214
|
+
settings: z.object({
|
|
215
|
+
api: z
|
|
216
|
+
.object({
|
|
217
|
+
port: z.number().int().min(1024).max(65535),
|
|
218
|
+
host: z.string(),
|
|
219
|
+
cors: z.object({
|
|
220
|
+
enabled: z.boolean(),
|
|
221
|
+
origins: z.array(z.string()),
|
|
222
|
+
}),
|
|
223
|
+
})
|
|
224
|
+
.optional(),
|
|
225
|
+
simulator: z
|
|
226
|
+
.object({
|
|
227
|
+
defaultPort: z.number().int().min(1024).max(65535),
|
|
228
|
+
autoRestart: z.boolean(),
|
|
229
|
+
maxInstances: z.number().int().min(1).max(100),
|
|
230
|
+
})
|
|
231
|
+
.optional(),
|
|
232
|
+
logging: z
|
|
233
|
+
.object({
|
|
234
|
+
level: z.enum(["error", "warn", "info", "debug"]),
|
|
235
|
+
file: z.boolean(),
|
|
236
|
+
console: z.boolean(),
|
|
237
|
+
maxSize: z.string().regex(/^\d+[kmg]b?$/i),
|
|
238
|
+
maxFiles: z.string(),
|
|
239
|
+
})
|
|
240
|
+
.optional(),
|
|
241
|
+
ui: z
|
|
242
|
+
.object({
|
|
243
|
+
theme: z.enum(["light", "dark", "auto"]),
|
|
244
|
+
language: z.enum(["en", "es", "fr", "de"]),
|
|
245
|
+
dateFormat: z.string(),
|
|
246
|
+
})
|
|
247
|
+
.optional(),
|
|
248
|
+
}),
|
|
249
|
+
|
|
250
|
+
// Common ID validation
|
|
251
|
+
id: z.object({
|
|
252
|
+
id: z.string().min(1, "ID is required"),
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
// Pagination validation
|
|
256
|
+
pagination: z.object({
|
|
257
|
+
page: z.number().int().min(1).default(1),
|
|
258
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
259
|
+
sort: z.string().optional(),
|
|
260
|
+
order: z.enum(["asc", "desc"]).default("asc"),
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
// Search/filter validation
|
|
264
|
+
filter: z.object({
|
|
265
|
+
search: z.string().max(100).optional(),
|
|
266
|
+
tags: z.array(z.string()).optional(),
|
|
267
|
+
category: z.string().optional(),
|
|
268
|
+
protocol: z.enum(["modbus", "bacnet"]).optional(),
|
|
269
|
+
status: z.enum(["active", "inactive", "error"]).optional(),
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create a validation middleware for request body
|
|
275
|
+
*/
|
|
276
|
+
export function validate<T extends ZodSchema>(
|
|
277
|
+
schema: T,
|
|
278
|
+
options?: ValidationOptions,
|
|
279
|
+
) {
|
|
280
|
+
const opts = { ...defaultOptions, ...options };
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
req: Request,
|
|
284
|
+
res: Response<ApiResponse<any>>,
|
|
285
|
+
next: NextFunction,
|
|
286
|
+
): any => {
|
|
287
|
+
try {
|
|
288
|
+
const value = schema.parse(req.body);
|
|
289
|
+
|
|
290
|
+
// Replace request body with validated and sanitized value
|
|
291
|
+
if (opts.stripUnknown) {
|
|
292
|
+
req.body = value;
|
|
293
|
+
}
|
|
294
|
+
next();
|
|
295
|
+
} catch (_error) {
|
|
296
|
+
if (_error instanceof ZodError) {
|
|
297
|
+
const errors = formatValidationErrors(_error);
|
|
298
|
+
const message = createErrorMessage(errors, "Invalid request");
|
|
299
|
+
|
|
300
|
+
return res.status(400).json({
|
|
301
|
+
success: false,
|
|
302
|
+
error: "Validation failed",
|
|
303
|
+
message,
|
|
304
|
+
errors,
|
|
305
|
+
timestamp: new Date().toISOString(),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
next(_error);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate request parameters
|
|
315
|
+
*/
|
|
316
|
+
export function validateParams<T extends ZodSchema>(
|
|
317
|
+
schema: T,
|
|
318
|
+
options?: ValidationOptions,
|
|
319
|
+
) {
|
|
320
|
+
const opts = { ...defaultOptions, ...options };
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
req: Request,
|
|
324
|
+
res: Response<ApiResponse<any>>,
|
|
325
|
+
next: NextFunction,
|
|
326
|
+
): any => {
|
|
327
|
+
try {
|
|
328
|
+
const value = schema.parse(req.params);
|
|
329
|
+
|
|
330
|
+
if (opts.stripUnknown) {
|
|
331
|
+
req.params = value;
|
|
332
|
+
}
|
|
333
|
+
next();
|
|
334
|
+
} catch (_error) {
|
|
335
|
+
if (_error instanceof ZodError) {
|
|
336
|
+
const errors = formatValidationErrors(_error);
|
|
337
|
+
const message = createErrorMessage(errors, "Invalid parameters");
|
|
338
|
+
|
|
339
|
+
return res.status(400).json({
|
|
340
|
+
success: false,
|
|
341
|
+
error: "Invalid parameters",
|
|
342
|
+
message,
|
|
343
|
+
errors,
|
|
344
|
+
timestamp: new Date().toISOString(),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
next(_error);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Validate query parameters
|
|
354
|
+
*/
|
|
355
|
+
export function validateQuery<T extends ZodSchema>(
|
|
356
|
+
schema: T,
|
|
357
|
+
options?: ValidationOptions,
|
|
358
|
+
) {
|
|
359
|
+
const opts = { ...defaultOptions, ...options };
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
req: Request,
|
|
363
|
+
res: Response<ApiResponse<any>>,
|
|
364
|
+
next: NextFunction,
|
|
365
|
+
): any => {
|
|
366
|
+
try {
|
|
367
|
+
const value = schema.parse(req.query);
|
|
368
|
+
|
|
369
|
+
if (opts.stripUnknown) {
|
|
370
|
+
req.query = value;
|
|
371
|
+
}
|
|
372
|
+
next();
|
|
373
|
+
} catch (_error) {
|
|
374
|
+
if (_error instanceof ZodError) {
|
|
375
|
+
const errors = formatValidationErrors(_error);
|
|
376
|
+
const message = createErrorMessage(errors, "Invalid query");
|
|
377
|
+
|
|
378
|
+
return res.status(400).json({
|
|
379
|
+
success: false,
|
|
380
|
+
error: "Invalid query parameters",
|
|
381
|
+
message,
|
|
382
|
+
errors,
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
next(_error);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Combined validation for body, params, and query
|
|
393
|
+
*/
|
|
394
|
+
export function validateRequest<
|
|
395
|
+
T extends {
|
|
396
|
+
body?: ZodSchema;
|
|
397
|
+
params?: ZodSchema;
|
|
398
|
+
query?: ZodSchema;
|
|
399
|
+
},
|
|
400
|
+
>(schemas: T, options?: ValidationOptions) {
|
|
401
|
+
const opts = { ...defaultOptions, ...options };
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
req: Request,
|
|
405
|
+
res: Response<ApiResponse<any>>,
|
|
406
|
+
next: NextFunction,
|
|
407
|
+
): any => {
|
|
408
|
+
const errors: FieldValidationError[] = [];
|
|
409
|
+
|
|
410
|
+
// Validate body
|
|
411
|
+
if (schemas.body) {
|
|
412
|
+
try {
|
|
413
|
+
const value = schemas.body.parse(req.body);
|
|
414
|
+
if (opts.stripUnknown) {
|
|
415
|
+
req.body = value;
|
|
416
|
+
}
|
|
417
|
+
} catch (_error) {
|
|
418
|
+
if (_error instanceof ZodError) {
|
|
419
|
+
errors.push(...formatValidationErrors(_error));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Validate params
|
|
425
|
+
if (schemas.params) {
|
|
426
|
+
try {
|
|
427
|
+
const value = schemas.params.parse(req.params);
|
|
428
|
+
if (opts.stripUnknown) {
|
|
429
|
+
req.params = value;
|
|
430
|
+
}
|
|
431
|
+
} catch (_error) {
|
|
432
|
+
if (_error instanceof ZodError) {
|
|
433
|
+
errors.push(...formatValidationErrors(_error));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Validate query
|
|
439
|
+
if (schemas.query) {
|
|
440
|
+
try {
|
|
441
|
+
const value = schemas.query.parse(req.query);
|
|
442
|
+
if (opts.stripUnknown) {
|
|
443
|
+
req.query = value;
|
|
444
|
+
}
|
|
445
|
+
} catch (_error) {
|
|
446
|
+
if (_error instanceof ZodError) {
|
|
447
|
+
errors.push(...formatValidationErrors(_error));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (errors.length > 0) {
|
|
453
|
+
const message = createErrorMessage(errors, "Request validation failed");
|
|
454
|
+
|
|
455
|
+
return res.status(400).json({
|
|
456
|
+
success: false,
|
|
457
|
+
error: "Validation failed",
|
|
458
|
+
message,
|
|
459
|
+
errors,
|
|
460
|
+
timestamp: new Date().toISOString(),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
next();
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Async validation wrapper for custom validation logic
|
|
470
|
+
*/
|
|
471
|
+
export function validateAsync(validationFn: (req: Request) => Promise<any>) {
|
|
472
|
+
return async (
|
|
473
|
+
req: Request,
|
|
474
|
+
res: Response<ApiResponse<any>>,
|
|
475
|
+
next: NextFunction,
|
|
476
|
+
) => {
|
|
477
|
+
try {
|
|
478
|
+
await validationFn(req);
|
|
479
|
+
next();
|
|
480
|
+
} catch (_error: any) {
|
|
481
|
+
const validationError: FieldValidationError = {
|
|
482
|
+
field: _error.field || "unknown",
|
|
483
|
+
message: _error.message || "Validation failed",
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
res.status(400).json({
|
|
487
|
+
success: false,
|
|
488
|
+
error: "Validation failed",
|
|
489
|
+
message: _error.message,
|
|
490
|
+
errors: [validationError],
|
|
491
|
+
timestamp: new Date().toISOString(),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create a conditional validation middleware
|
|
499
|
+
*/
|
|
500
|
+
export function validateIf<T extends ZodSchema>(
|
|
501
|
+
condition: (req: Request) => boolean,
|
|
502
|
+
schema: T,
|
|
503
|
+
options?: ValidationOptions,
|
|
504
|
+
) {
|
|
505
|
+
return (
|
|
506
|
+
req: Request,
|
|
507
|
+
res: Response<ApiResponse<any>>,
|
|
508
|
+
next: NextFunction,
|
|
509
|
+
): any => {
|
|
510
|
+
if (condition(req)) {
|
|
511
|
+
return validate(schema, options)(req, res, next);
|
|
512
|
+
}
|
|
513
|
+
next();
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Schema composition helpers using Zod's built-in methods
|
|
519
|
+
*/
|
|
520
|
+
export const compose = {
|
|
521
|
+
/**
|
|
522
|
+
* Merge multiple schemas
|
|
523
|
+
*/
|
|
524
|
+
merge: <T extends ZodSchema, U extends ZodSchema>(schema1: T, schema2: U) => {
|
|
525
|
+
if (schema1 instanceof z.ZodObject && schema2 instanceof z.ZodObject) {
|
|
526
|
+
return schema1.merge(schema2);
|
|
527
|
+
}
|
|
528
|
+
return z.intersection(schema1, schema2);
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Make all fields optional
|
|
533
|
+
*/
|
|
534
|
+
partial: <T extends z.ZodObject<any>>(schema: T) => {
|
|
535
|
+
return schema.partial();
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Make specific fields required
|
|
540
|
+
*/
|
|
541
|
+
require: <T extends z.ZodObject<any>>(
|
|
542
|
+
schema: T,
|
|
543
|
+
fields: Array<keyof T["shape"]>,
|
|
544
|
+
) => {
|
|
545
|
+
const partialSchema = schema.partial();
|
|
546
|
+
const requiredOverrides: any = {};
|
|
547
|
+
fields.forEach((field) => {
|
|
548
|
+
const fieldSchema = schema.shape[field as string];
|
|
549
|
+
if (fieldSchema) {
|
|
550
|
+
requiredOverrides[field] = fieldSchema;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
return partialSchema.extend(requiredOverrides);
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Pick specific fields from a schema
|
|
558
|
+
*/
|
|
559
|
+
pick: <T extends z.ZodObject<any>>(
|
|
560
|
+
schema: T,
|
|
561
|
+
fields: Array<keyof T["shape"]>,
|
|
562
|
+
) => {
|
|
563
|
+
const picked: any = {};
|
|
564
|
+
fields.forEach((field) => {
|
|
565
|
+
if (schema.shape[field as string]) {
|
|
566
|
+
picked[field] = schema.shape[field as string];
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return z.object(picked);
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Omit specific fields from a schema
|
|
574
|
+
*/
|
|
575
|
+
omit: <T extends z.ZodObject<any>>(
|
|
576
|
+
schema: T,
|
|
577
|
+
fields: Array<keyof T["shape"]>,
|
|
578
|
+
) => {
|
|
579
|
+
return schema.omit(
|
|
580
|
+
fields.reduce((acc, field) => {
|
|
581
|
+
acc[field] = true;
|
|
582
|
+
return acc;
|
|
583
|
+
}, {} as any),
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Export Zod for direct use
|
|
590
|
+
*/
|
|
591
|
+
export { z, z as zod };
|
|
592
|
+
|
|
593
|
+
export default {
|
|
594
|
+
validate,
|
|
595
|
+
validateParams,
|
|
596
|
+
validateQuery,
|
|
597
|
+
validateRequest,
|
|
598
|
+
validateAsync,
|
|
599
|
+
validateIf,
|
|
600
|
+
schemas,
|
|
601
|
+
compose,
|
|
602
|
+
z,
|
|
603
|
+
};
|