sprint-es 0.0.78 ā 0.0.80
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/dist/cjs/cli.cjs +214 -0
- package/dist/cjs/index.cjs +66 -57
- package/dist/cjs/modules/schemas/index.cjs +3 -1
- package/dist/esm/cli.js +215 -1
- package/dist/esm/index.js +66 -57
- package/dist/esm/modules/schemas/index.js +3 -1
- package/dist/types/middleware.d.ts.map +1 -1
- package/dist/types/modules/schemas/index.d.ts.map +1 -1
- package/dist/types/modules/telemetry/types.d.ts +1 -0
- package/dist/types/modules/telemetry/types.d.ts.map +1 -1
- package/dist/types/sprint.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cjs/cli.cjs
CHANGED
|
@@ -23,6 +23,22 @@ function _interopNamespaceDefault(e) {
|
|
|
23
23
|
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
24
24
|
const args = process.argv.slice(2);
|
|
25
25
|
const command = args[0];
|
|
26
|
+
const pc = {
|
|
27
|
+
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
28
|
+
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
29
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
30
|
+
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
31
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
32
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`
|
|
33
|
+
};
|
|
34
|
+
const logger = {
|
|
35
|
+
error: (...args2) => console.log(pc.red(args2.join(" "))),
|
|
36
|
+
warn: (...args2) => console.log(pc.yellow(args2.join(" "))),
|
|
37
|
+
info: (...args2) => console.log(pc.cyan(args2.join(" "))),
|
|
38
|
+
success: (...args2) => console.log(pc.green(args2.join(" "))),
|
|
39
|
+
dim: (...args2) => console.log(pc.dim(args2.join(" "))),
|
|
40
|
+
break: () => console.log("")
|
|
41
|
+
};
|
|
26
42
|
if (!command) {
|
|
27
43
|
console.log("\nš Sprint CLI\n");
|
|
28
44
|
console.log("Usage: sprint-es <command>");
|
|
@@ -30,6 +46,7 @@ if (!command) {
|
|
|
30
46
|
console.log(" dev Start development server");
|
|
31
47
|
console.log(" build Build for production");
|
|
32
48
|
console.log(" start Start production server");
|
|
49
|
+
console.log(" doctor Analyze routes and middlewares for missing schemas");
|
|
33
50
|
console.log(" generate-keys Generate secure keys for JWT encryption");
|
|
34
51
|
console.log("\nOptions:");
|
|
35
52
|
console.log(" --help Show this help message");
|
|
@@ -42,6 +59,7 @@ if (command === "--help" || command === "-h") {
|
|
|
42
59
|
console.log(" dev Start development server");
|
|
43
60
|
console.log(" build Build for production");
|
|
44
61
|
console.log(" start Start production server");
|
|
62
|
+
console.log(" doctor Analyze routes and middlewares for missing schemas");
|
|
45
63
|
console.log(" generate-keys Generate secure keys for JWT encryption");
|
|
46
64
|
process.exit(0);
|
|
47
65
|
}
|
|
@@ -84,6 +102,199 @@ function generateJWTSecret() {
|
|
|
84
102
|
}
|
|
85
103
|
return chars.join("");
|
|
86
104
|
}
|
|
105
|
+
function scanDirectory(dir, extensions) {
|
|
106
|
+
const files = [];
|
|
107
|
+
if (!fs.existsSync(dir)) return files;
|
|
108
|
+
const entries = fs.readdirSync(dir);
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const fullPath = path.join(dir, entry);
|
|
111
|
+
const stat = fs.statSync(fullPath);
|
|
112
|
+
if (stat.isDirectory()) {
|
|
113
|
+
files.push(...scanDirectory(fullPath, extensions));
|
|
114
|
+
} else if (stat.isFile() && extensions.some((ext) => entry.endsWith(ext))) {
|
|
115
|
+
files.push(fullPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
function extractRoutesFromFile(filePath) {
|
|
121
|
+
const routes = [];
|
|
122
|
+
try {
|
|
123
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
124
|
+
const lines = content.split("\n");
|
|
125
|
+
const routerMethods = ["get", "post", "put", "delete", "patch", "all", "head", "options"];
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
for (const method of routerMethods) {
|
|
129
|
+
const match = trimmed.match(new RegExp(`router\\.${method}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`));
|
|
130
|
+
if (match) {
|
|
131
|
+
routes.push({
|
|
132
|
+
file: filePath,
|
|
133
|
+
path: match[1],
|
|
134
|
+
method: method.toUpperCase(),
|
|
135
|
+
hasSchema: false
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
return routes;
|
|
144
|
+
}
|
|
145
|
+
function checkRouteHasSchema(filePath, routePath, method) {
|
|
146
|
+
try {
|
|
147
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
148
|
+
const routePattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*(\\w+)`);
|
|
149
|
+
const schemaVarMatch = content.match(routePattern);
|
|
150
|
+
if (schemaVarMatch) {
|
|
151
|
+
const schemaVar = schemaVarMatch[1];
|
|
152
|
+
const schemaDefinitionPattern = new RegExp(`export\\s+const\\s+${schemaVar}\\s*=\\s*defineRouteSchema`);
|
|
153
|
+
if (schemaDefinitionPattern.test(content)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const directSchemaPattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*defineRouteSchema`);
|
|
158
|
+
if (directSchemaPattern.test(content)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function hasSchemaInMiddleware(filePath) {
|
|
167
|
+
try {
|
|
168
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
169
|
+
if (content.includes("schema:") || content.includes("__sprintMiddlewareSchema")) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function extractMiddlewareName(filePath) {
|
|
178
|
+
try {
|
|
179
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
180
|
+
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
181
|
+
if (nameMatch) {
|
|
182
|
+
return nameMatch[1];
|
|
183
|
+
}
|
|
184
|
+
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
|
185
|
+
return fileName.replace(/\.(ts|js)$/, "");
|
|
186
|
+
} catch {
|
|
187
|
+
return filePath;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function renderFramedBox(lines, title) {
|
|
191
|
+
if (lines.length === 0) return;
|
|
192
|
+
const maxLength = Math.max(...lines.map((line) => line.length));
|
|
193
|
+
const horizontalPadding = 1;
|
|
194
|
+
const borderLine = "ā".repeat(maxLength + horizontalPadding * 2);
|
|
195
|
+
console.log(pc.dim(`ā${borderLine}ā`));
|
|
196
|
+
if (title) {
|
|
197
|
+
const titlePadding = " ".repeat(maxLength - title.length + horizontalPadding * 2);
|
|
198
|
+
console.log(pc.dim("ā") + " " + pc.bold(pc.cyan(title)) + titlePadding + pc.dim("ā"));
|
|
199
|
+
console.log(pc.dim(`ā${borderLine}ā¤`));
|
|
200
|
+
}
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const padding = " ".repeat(maxLength - line.length + horizontalPadding * 2);
|
|
203
|
+
console.log(pc.dim("ā") + " " + line + padding + pc.dim("ā"));
|
|
204
|
+
}
|
|
205
|
+
console.log(pc.dim(`ā${borderLine}ā`));
|
|
206
|
+
}
|
|
207
|
+
async function runDoctor() {
|
|
208
|
+
logger.break();
|
|
209
|
+
logger.info("š Sprint Doctor - Analyzing routes and middlewares...\n");
|
|
210
|
+
const routesPath = path.join(projectRoot, "src/routes");
|
|
211
|
+
const middlewaresPath = path.join(projectRoot, "src/middlewares");
|
|
212
|
+
const routeFiles = scanDirectory(routesPath, [".ts", ".js"]);
|
|
213
|
+
const middlewareFiles = scanDirectory(middlewaresPath, [".ts", ".js"]);
|
|
214
|
+
const allRoutes = [];
|
|
215
|
+
for (const file of routeFiles) {
|
|
216
|
+
const routes = extractRoutesFromFile(file);
|
|
217
|
+
for (const route of routes) {
|
|
218
|
+
route.hasSchema = checkRouteHasSchema(file, route.path, route.method);
|
|
219
|
+
allRoutes.push(route);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const middlewares = [];
|
|
223
|
+
for (const file of middlewareFiles) {
|
|
224
|
+
middlewares.push({
|
|
225
|
+
file,
|
|
226
|
+
name: extractMiddlewareName(file),
|
|
227
|
+
hasSchema: hasSchemaInMiddleware(file)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const routesWithoutSchema = allRoutes.filter((r) => !r.hasSchema);
|
|
231
|
+
const middlewaresWithoutSchema = middlewares.filter((m) => !m.hasSchema);
|
|
232
|
+
const routesWithSchema = allRoutes.filter((r) => r.hasSchema);
|
|
233
|
+
const middlewaresWithSchema = middlewares.filter((m) => m.hasSchema);
|
|
234
|
+
logger.break();
|
|
235
|
+
console.log(pc.bold("š Schema Coverage Report\n"));
|
|
236
|
+
const routesLines = [
|
|
237
|
+
`${pc.green("ā")} Routes with schema: ${routesWithSchema.length}`,
|
|
238
|
+
`${routesWithoutSchema.length > 0 ? pc.red("ā") : pc.green("ā")} Routes without schema: ${routesWithoutSchema.length}`,
|
|
239
|
+
`${pc.dim("ā".repeat(40))}`,
|
|
240
|
+
`Total routes: ${allRoutes.length}`
|
|
241
|
+
];
|
|
242
|
+
renderFramedBox(routesLines, "Routes");
|
|
243
|
+
logger.break();
|
|
244
|
+
const middlewareLines = [
|
|
245
|
+
`${pc.green("ā")} Middlewares with schema: ${middlewaresWithSchema.length}`,
|
|
246
|
+
`${middlewaresWithoutSchema.length > 0 ? pc.red("ā") : pc.green("ā")} Middlewares without schema: ${middlewaresWithoutSchema.length}`,
|
|
247
|
+
`${pc.dim("ā".repeat(40))}`,
|
|
248
|
+
`Total middlewares: ${middlewares.length}`
|
|
249
|
+
];
|
|
250
|
+
renderFramedBox(middlewareLines, "Middlewares");
|
|
251
|
+
logger.break();
|
|
252
|
+
if (routesWithoutSchema.length > 0) {
|
|
253
|
+
console.log(pc.bold(pc.yellow("ā ļø Routes without schema:")));
|
|
254
|
+
logger.break();
|
|
255
|
+
const uniqueRoutes = routesWithoutSchema.reduce((acc, route) => {
|
|
256
|
+
const key = `${route.method}:${route.path}`;
|
|
257
|
+
if (!acc[key]) acc[key] = { ...route, files: [route.file] };
|
|
258
|
+
else if (!acc[key].files.includes(route.file)) acc[key].files.push(route.file);
|
|
259
|
+
return acc;
|
|
260
|
+
}, {});
|
|
261
|
+
for (const route of Object.values(uniqueRoutes)) {
|
|
262
|
+
const fileName = route.file.split(/[/\\]/).pop();
|
|
263
|
+
console.log(` ${pc.red("ā")} ${pc.bold(route.method)} ${route.path}`);
|
|
264
|
+
console.log(` ${pc.dim("File:")} ${fileName}`);
|
|
265
|
+
}
|
|
266
|
+
logger.break();
|
|
267
|
+
}
|
|
268
|
+
if (middlewaresWithoutSchema.length > 0) {
|
|
269
|
+
console.log(pc.bold(pc.yellow("ā ļø Middlewares without schema:")));
|
|
270
|
+
logger.break();
|
|
271
|
+
for (const mw of middlewaresWithoutSchema) {
|
|
272
|
+
const fileName = mw.file.split(/[/\\]/).pop();
|
|
273
|
+
console.log(` ${pc.red("ā")} ${pc.bold(mw.name)}`);
|
|
274
|
+
console.log(` ${pc.dim("File:")} ${fileName}`);
|
|
275
|
+
}
|
|
276
|
+
logger.break();
|
|
277
|
+
}
|
|
278
|
+
if (routesWithoutSchema.length === 0 && middlewaresWithoutSchema.length === 0) {
|
|
279
|
+
console.log(pc.green(pc.bold("ā
All routes and middlewares have schemas defined!")));
|
|
280
|
+
logger.break();
|
|
281
|
+
}
|
|
282
|
+
const totalWithoutSchema = routesWithoutSchema.length + middlewaresWithoutSchema.length;
|
|
283
|
+
const totalItems = allRoutes.length + middlewares.length;
|
|
284
|
+
if (totalItems > 0) {
|
|
285
|
+
const coverage = Math.round((totalItems - totalWithoutSchema) / totalItems * 100);
|
|
286
|
+
console.log(pc.bold("š Schema Coverage: ") + (coverage >= 80 ? pc.green(`${coverage}%`) : coverage >= 50 ? pc.yellow(`${coverage}%`) : pc.red(`${coverage}%`)));
|
|
287
|
+
if (coverage < 80) {
|
|
288
|
+
logger.break();
|
|
289
|
+
console.log(pc.yellow("š” Tip: Adding schemas helps with:"));
|
|
290
|
+
console.log(pc.dim(" ⢠Request validation (body, query, params, headers)"));
|
|
291
|
+
console.log(pc.dim(" ⢠OpenAPI/Swagger UI auto-generation"));
|
|
292
|
+
console.log(pc.dim(" ⢠Better security through input validation"));
|
|
293
|
+
console.log(pc.dim(" ⢠Auto-completion in IDEs"));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
logger.break();
|
|
297
|
+
}
|
|
87
298
|
switch (command) {
|
|
88
299
|
case "dev":
|
|
89
300
|
console.log("š Starting development server with hot reload...");
|
|
@@ -103,6 +314,9 @@ switch (command) {
|
|
|
103
314
|
console.log("š Starting production server...");
|
|
104
315
|
runCommand("node dist/index.js", { NODE_ENV: "production" });
|
|
105
316
|
break;
|
|
317
|
+
case "doctor":
|
|
318
|
+
runDoctor();
|
|
319
|
+
break;
|
|
106
320
|
case "generate-keys":
|
|
107
321
|
const { publicKey, privateKey } = crypto__namespace.generateKeyPairSync("rsa", {
|
|
108
322
|
modulusLength: 2048,
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -341,19 +341,18 @@ class Sprint {
|
|
|
341
341
|
const route = layer.route;
|
|
342
342
|
for (const routeLayer of route.stack) {
|
|
343
343
|
const handlers = Array.isArray(routeLayer.handle) ? routeLayer.handle : [routeLayer.handle];
|
|
344
|
+
let schema;
|
|
344
345
|
for (const handler of handlers) {
|
|
345
|
-
|
|
346
|
-
if (schema)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
346
|
+
schema = handler.__sprintRouteSchema;
|
|
347
|
+
if (schema) break;
|
|
348
|
+
}
|
|
349
|
+
const method = (routeLayer.method || "").toUpperCase();
|
|
350
|
+
if (method) {
|
|
351
|
+
this.registeredRoutes.push({
|
|
352
|
+
method,
|
|
353
|
+
path: finalRoute + route.path,
|
|
354
|
+
schema
|
|
355
|
+
});
|
|
357
356
|
}
|
|
358
357
|
}
|
|
359
358
|
}
|
|
@@ -472,6 +471,37 @@ class Sprint {
|
|
|
472
471
|
console.warn("[Sprint] Failed to convert headers schema:", e);
|
|
473
472
|
}
|
|
474
473
|
}
|
|
474
|
+
if (route.schema?.sprint?.authorization) {
|
|
475
|
+
const authSchema = route.schema.sprint.authorization;
|
|
476
|
+
const description = authSchema._def?.description;
|
|
477
|
+
let sources = ["query:token", "headers:authorization"];
|
|
478
|
+
if (description) {
|
|
479
|
+
try {
|
|
480
|
+
const parsed = JSON.parse(description);
|
|
481
|
+
if (parsed.__sprintAuthorization && parsed.sources) sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const isRequired = sources.length === 1;
|
|
486
|
+
for (const source of sources) {
|
|
487
|
+
const [type, key] = source.split(":");
|
|
488
|
+
if (type === "query") {
|
|
489
|
+
allParams.push({
|
|
490
|
+
name: key,
|
|
491
|
+
in: "query",
|
|
492
|
+
required: isRequired,
|
|
493
|
+
schema: { type: "string" }
|
|
494
|
+
});
|
|
495
|
+
} else if (type === "headers") {
|
|
496
|
+
allParams.push({
|
|
497
|
+
name: key,
|
|
498
|
+
in: "header",
|
|
499
|
+
required: isRequired,
|
|
500
|
+
schema: { type: "string" }
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
475
505
|
if (this.openapi.generateOnBuild) {
|
|
476
506
|
try {
|
|
477
507
|
const routeMiddlewares = this.getMiddlewaresForRoute(route.path);
|
|
@@ -497,26 +527,25 @@ class Sprint {
|
|
|
497
527
|
if (description) {
|
|
498
528
|
try {
|
|
499
529
|
const parsed = JSON.parse(description);
|
|
500
|
-
if (parsed.__sprintAuthorization && parsed.sources)
|
|
501
|
-
sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
|
|
502
|
-
}
|
|
530
|
+
if (parsed.__sprintAuthorization && parsed.sources) sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
|
|
503
531
|
} catch {
|
|
504
532
|
}
|
|
505
533
|
}
|
|
534
|
+
const isRequired = sources.length === 1;
|
|
506
535
|
for (const source of sources) {
|
|
507
536
|
const [type, key] = source.split(":");
|
|
508
537
|
if (type === "query") {
|
|
509
538
|
allParams.push({
|
|
510
539
|
name: key,
|
|
511
540
|
in: "query",
|
|
512
|
-
required:
|
|
541
|
+
required: isRequired,
|
|
513
542
|
schema: { type: "string" }
|
|
514
543
|
});
|
|
515
544
|
} else if (type === "headers") {
|
|
516
545
|
allParams.push({
|
|
517
546
|
name: key,
|
|
518
547
|
in: "header",
|
|
519
|
-
required:
|
|
548
|
+
required: isRequired,
|
|
520
549
|
schema: { type: "string" }
|
|
521
550
|
});
|
|
522
551
|
}
|
|
@@ -529,9 +558,7 @@ class Sprint {
|
|
|
529
558
|
}
|
|
530
559
|
}
|
|
531
560
|
if (allParams.length > 0) {
|
|
532
|
-
const uniqueParams = allParams.filter(
|
|
533
|
-
(param, index, self) => index === self.findIndex((p) => p.name === param.name && p.in === param.in)
|
|
534
|
-
);
|
|
561
|
+
const uniqueParams = allParams.filter((param, index, self) => index === self.findIndex((p) => p.name === param.name && p.in === param.in));
|
|
535
562
|
routeSpec.parameters = uniqueParams;
|
|
536
563
|
}
|
|
537
564
|
paths[route.path][method] = routeSpec;
|
|
@@ -556,25 +583,15 @@ class Sprint {
|
|
|
556
583
|
const zodDef = value._def;
|
|
557
584
|
const typeName = zodDef?.typeName;
|
|
558
585
|
let propSchema = {};
|
|
559
|
-
if (typeName === "ZodString") {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
propSchema = { type: "array", items: this.zodSchemaToOpenAPI(zodDef?.type) };
|
|
567
|
-
} else if (typeName === "ZodObject") {
|
|
568
|
-
propSchema = this.zodSchemaToOpenAPI(zodDef?.type);
|
|
569
|
-
} else if (typeName === "ZodOptional") {
|
|
570
|
-
continue;
|
|
571
|
-
} else {
|
|
572
|
-
propSchema = { type: "string" };
|
|
573
|
-
}
|
|
586
|
+
if (typeName === "ZodString") propSchema = { type: "string" };
|
|
587
|
+
else if (typeName === "ZodNumber") propSchema = { type: "number" };
|
|
588
|
+
else if (typeName === "ZodBoolean") propSchema = { type: "boolean" };
|
|
589
|
+
else if (typeName === "ZodArray") propSchema = { type: "array", items: this.zodSchemaToOpenAPI(zodDef?.type) };
|
|
590
|
+
else if (typeName === "ZodObject") propSchema = this.zodSchemaToOpenAPI(zodDef?.type);
|
|
591
|
+
else if (typeName === "ZodOptional") continue;
|
|
592
|
+
else propSchema = { type: "string" };
|
|
574
593
|
properties[key] = propSchema;
|
|
575
|
-
if (!zodDef?.isOptional && typeName !== "ZodOptional")
|
|
576
|
-
required.push(key);
|
|
577
|
-
}
|
|
594
|
+
if (!zodDef?.isOptional && typeName !== "ZodOptional") required.push(key);
|
|
578
595
|
}
|
|
579
596
|
return { type: "object", properties, required: required.length > 0 ? required : void 0 };
|
|
580
597
|
}
|
|
@@ -589,15 +606,10 @@ class Sprint {
|
|
|
589
606
|
const zodDef = value._def;
|
|
590
607
|
const typeName = zodDef?.typeName;
|
|
591
608
|
let paramSchema = {};
|
|
592
|
-
if (typeName === "ZodString") {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
} else if (typeName === "ZodBoolean") {
|
|
597
|
-
paramSchema = { type: "boolean" };
|
|
598
|
-
} else {
|
|
599
|
-
paramSchema = { type: "string" };
|
|
600
|
-
}
|
|
609
|
+
if (typeName === "ZodString") paramSchema = { type: "string" };
|
|
610
|
+
else if (typeName === "ZodNumber") paramSchema = { type: "number" };
|
|
611
|
+
else if (typeName === "ZodBoolean") paramSchema = { type: "boolean" };
|
|
612
|
+
else paramSchema = { type: "string" };
|
|
601
613
|
params.push({
|
|
602
614
|
name: key,
|
|
603
615
|
in: "query",
|
|
@@ -616,15 +628,10 @@ class Sprint {
|
|
|
616
628
|
const zodDef = value._def;
|
|
617
629
|
const typeName = zodDef?.typeName;
|
|
618
630
|
let paramSchema = {};
|
|
619
|
-
if (typeName === "ZodString") {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
} else if (typeName === "ZodBoolean") {
|
|
624
|
-
paramSchema = { type: "boolean" };
|
|
625
|
-
} else {
|
|
626
|
-
paramSchema = { type: "string" };
|
|
627
|
-
}
|
|
631
|
+
if (typeName === "ZodString") paramSchema = { type: "string" };
|
|
632
|
+
else if (typeName === "ZodNumber") paramSchema = { type: "number" };
|
|
633
|
+
else if (typeName === "ZodBoolean") paramSchema = { type: "boolean" };
|
|
634
|
+
else paramSchema = { type: "string" };
|
|
628
635
|
headers.push({
|
|
629
636
|
name: key,
|
|
630
637
|
in: "header",
|
|
@@ -713,7 +720,9 @@ class Sprint {
|
|
|
713
720
|
function createSchemaValidationMiddleware(schema) {
|
|
714
721
|
return (req, res, next) => {
|
|
715
722
|
const errors = [];
|
|
716
|
-
|
|
723
|
+
const method = req.method.toUpperCase();
|
|
724
|
+
const noBodyMethods = ["GET", "HEAD", "DELETE"];
|
|
725
|
+
if (schema.body && !noBodyMethods.includes(method)) {
|
|
717
726
|
const result = schema.body.safeParse(req.body);
|
|
718
727
|
if (!result.success) {
|
|
719
728
|
errors.push(...result.error.issues.map((issue) => ({
|
|
@@ -38,7 +38,9 @@ function parseSchema(schema, data) {
|
|
|
38
38
|
function defineRouteSchema(schema) {
|
|
39
39
|
const middleware = (req, res, next) => {
|
|
40
40
|
const errors = [];
|
|
41
|
-
|
|
41
|
+
const method = req.method.toUpperCase();
|
|
42
|
+
const noBodyMethods = ["GET", "HEAD", "DELETE"];
|
|
43
|
+
if (schema.body && !noBodyMethods.includes(method)) {
|
|
42
44
|
const result = parseSchema(schema.body, req.body);
|
|
43
45
|
if (!result.success) errors.push(...result.errors.map((e) => ({ location: "body", ...e })));
|
|
44
46
|
}
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "fs";
|
|
2
|
+
import { existsSync, readdirSync, statSync, readFileSync } from "fs";
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
4
|
import { join, resolve } from "path";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
const command = args[0];
|
|
8
|
+
const pc = {
|
|
9
|
+
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
10
|
+
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
11
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
12
|
+
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
13
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
14
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`
|
|
15
|
+
};
|
|
16
|
+
const logger = {
|
|
17
|
+
error: (...args2) => console.log(pc.red(args2.join(" "))),
|
|
18
|
+
warn: (...args2) => console.log(pc.yellow(args2.join(" "))),
|
|
19
|
+
info: (...args2) => console.log(pc.cyan(args2.join(" "))),
|
|
20
|
+
success: (...args2) => console.log(pc.green(args2.join(" "))),
|
|
21
|
+
dim: (...args2) => console.log(pc.dim(args2.join(" "))),
|
|
22
|
+
break: () => console.log("")
|
|
23
|
+
};
|
|
8
24
|
if (!command) {
|
|
9
25
|
console.log("\nš Sprint CLI\n");
|
|
10
26
|
console.log("Usage: sprint-es <command>");
|
|
@@ -12,6 +28,7 @@ if (!command) {
|
|
|
12
28
|
console.log(" dev Start development server");
|
|
13
29
|
console.log(" build Build for production");
|
|
14
30
|
console.log(" start Start production server");
|
|
31
|
+
console.log(" doctor Analyze routes and middlewares for missing schemas");
|
|
15
32
|
console.log(" generate-keys Generate secure keys for JWT encryption");
|
|
16
33
|
console.log("\nOptions:");
|
|
17
34
|
console.log(" --help Show this help message");
|
|
@@ -24,6 +41,7 @@ if (command === "--help" || command === "-h") {
|
|
|
24
41
|
console.log(" dev Start development server");
|
|
25
42
|
console.log(" build Build for production");
|
|
26
43
|
console.log(" start Start production server");
|
|
44
|
+
console.log(" doctor Analyze routes and middlewares for missing schemas");
|
|
27
45
|
console.log(" generate-keys Generate secure keys for JWT encryption");
|
|
28
46
|
process.exit(0);
|
|
29
47
|
}
|
|
@@ -66,6 +84,199 @@ function generateJWTSecret() {
|
|
|
66
84
|
}
|
|
67
85
|
return chars.join("");
|
|
68
86
|
}
|
|
87
|
+
function scanDirectory(dir, extensions) {
|
|
88
|
+
const files = [];
|
|
89
|
+
if (!existsSync(dir)) return files;
|
|
90
|
+
const entries = readdirSync(dir);
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const fullPath = join(dir, entry);
|
|
93
|
+
const stat = statSync(fullPath);
|
|
94
|
+
if (stat.isDirectory()) {
|
|
95
|
+
files.push(...scanDirectory(fullPath, extensions));
|
|
96
|
+
} else if (stat.isFile() && extensions.some((ext) => entry.endsWith(ext))) {
|
|
97
|
+
files.push(fullPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return files;
|
|
101
|
+
}
|
|
102
|
+
function extractRoutesFromFile(filePath) {
|
|
103
|
+
const routes = [];
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(filePath, "utf-8");
|
|
106
|
+
const lines = content.split("\n");
|
|
107
|
+
const routerMethods = ["get", "post", "put", "delete", "patch", "all", "head", "options"];
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
for (const method of routerMethods) {
|
|
111
|
+
const match = trimmed.match(new RegExp(`router\\.${method}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`));
|
|
112
|
+
if (match) {
|
|
113
|
+
routes.push({
|
|
114
|
+
file: filePath,
|
|
115
|
+
path: match[1],
|
|
116
|
+
method: method.toUpperCase(),
|
|
117
|
+
hasSchema: false
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
return routes;
|
|
126
|
+
}
|
|
127
|
+
function checkRouteHasSchema(filePath, routePath, method) {
|
|
128
|
+
try {
|
|
129
|
+
const content = readFileSync(filePath, "utf-8");
|
|
130
|
+
const routePattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*(\\w+)`);
|
|
131
|
+
const schemaVarMatch = content.match(routePattern);
|
|
132
|
+
if (schemaVarMatch) {
|
|
133
|
+
const schemaVar = schemaVarMatch[1];
|
|
134
|
+
const schemaDefinitionPattern = new RegExp(`export\\s+const\\s+${schemaVar}\\s*=\\s*defineRouteSchema`);
|
|
135
|
+
if (schemaDefinitionPattern.test(content)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const directSchemaPattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*defineRouteSchema`);
|
|
140
|
+
if (directSchemaPattern.test(content)) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function hasSchemaInMiddleware(filePath) {
|
|
149
|
+
try {
|
|
150
|
+
const content = readFileSync(filePath, "utf-8");
|
|
151
|
+
if (content.includes("schema:") || content.includes("__sprintMiddlewareSchema")) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function extractMiddlewareName(filePath) {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(filePath, "utf-8");
|
|
162
|
+
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
163
|
+
if (nameMatch) {
|
|
164
|
+
return nameMatch[1];
|
|
165
|
+
}
|
|
166
|
+
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
|
167
|
+
return fileName.replace(/\.(ts|js)$/, "");
|
|
168
|
+
} catch {
|
|
169
|
+
return filePath;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function renderFramedBox(lines, title) {
|
|
173
|
+
if (lines.length === 0) return;
|
|
174
|
+
const maxLength = Math.max(...lines.map((line) => line.length));
|
|
175
|
+
const horizontalPadding = 1;
|
|
176
|
+
const borderLine = "ā".repeat(maxLength + horizontalPadding * 2);
|
|
177
|
+
console.log(pc.dim(`ā${borderLine}ā`));
|
|
178
|
+
if (title) {
|
|
179
|
+
const titlePadding = " ".repeat(maxLength - title.length + horizontalPadding * 2);
|
|
180
|
+
console.log(pc.dim("ā") + " " + pc.bold(pc.cyan(title)) + titlePadding + pc.dim("ā"));
|
|
181
|
+
console.log(pc.dim(`ā${borderLine}ā¤`));
|
|
182
|
+
}
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
const padding = " ".repeat(maxLength - line.length + horizontalPadding * 2);
|
|
185
|
+
console.log(pc.dim("ā") + " " + line + padding + pc.dim("ā"));
|
|
186
|
+
}
|
|
187
|
+
console.log(pc.dim(`ā${borderLine}ā`));
|
|
188
|
+
}
|
|
189
|
+
async function runDoctor() {
|
|
190
|
+
logger.break();
|
|
191
|
+
logger.info("š Sprint Doctor - Analyzing routes and middlewares...\n");
|
|
192
|
+
const routesPath = join(projectRoot, "src/routes");
|
|
193
|
+
const middlewaresPath = join(projectRoot, "src/middlewares");
|
|
194
|
+
const routeFiles = scanDirectory(routesPath, [".ts", ".js"]);
|
|
195
|
+
const middlewareFiles = scanDirectory(middlewaresPath, [".ts", ".js"]);
|
|
196
|
+
const allRoutes = [];
|
|
197
|
+
for (const file of routeFiles) {
|
|
198
|
+
const routes = extractRoutesFromFile(file);
|
|
199
|
+
for (const route of routes) {
|
|
200
|
+
route.hasSchema = checkRouteHasSchema(file, route.path, route.method);
|
|
201
|
+
allRoutes.push(route);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const middlewares = [];
|
|
205
|
+
for (const file of middlewareFiles) {
|
|
206
|
+
middlewares.push({
|
|
207
|
+
file,
|
|
208
|
+
name: extractMiddlewareName(file),
|
|
209
|
+
hasSchema: hasSchemaInMiddleware(file)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const routesWithoutSchema = allRoutes.filter((r) => !r.hasSchema);
|
|
213
|
+
const middlewaresWithoutSchema = middlewares.filter((m) => !m.hasSchema);
|
|
214
|
+
const routesWithSchema = allRoutes.filter((r) => r.hasSchema);
|
|
215
|
+
const middlewaresWithSchema = middlewares.filter((m) => m.hasSchema);
|
|
216
|
+
logger.break();
|
|
217
|
+
console.log(pc.bold("š Schema Coverage Report\n"));
|
|
218
|
+
const routesLines = [
|
|
219
|
+
`${pc.green("ā")} Routes with schema: ${routesWithSchema.length}`,
|
|
220
|
+
`${routesWithoutSchema.length > 0 ? pc.red("ā") : pc.green("ā")} Routes without schema: ${routesWithoutSchema.length}`,
|
|
221
|
+
`${pc.dim("ā".repeat(40))}`,
|
|
222
|
+
`Total routes: ${allRoutes.length}`
|
|
223
|
+
];
|
|
224
|
+
renderFramedBox(routesLines, "Routes");
|
|
225
|
+
logger.break();
|
|
226
|
+
const middlewareLines = [
|
|
227
|
+
`${pc.green("ā")} Middlewares with schema: ${middlewaresWithSchema.length}`,
|
|
228
|
+
`${middlewaresWithoutSchema.length > 0 ? pc.red("ā") : pc.green("ā")} Middlewares without schema: ${middlewaresWithoutSchema.length}`,
|
|
229
|
+
`${pc.dim("ā".repeat(40))}`,
|
|
230
|
+
`Total middlewares: ${middlewares.length}`
|
|
231
|
+
];
|
|
232
|
+
renderFramedBox(middlewareLines, "Middlewares");
|
|
233
|
+
logger.break();
|
|
234
|
+
if (routesWithoutSchema.length > 0) {
|
|
235
|
+
console.log(pc.bold(pc.yellow("ā ļø Routes without schema:")));
|
|
236
|
+
logger.break();
|
|
237
|
+
const uniqueRoutes = routesWithoutSchema.reduce((acc, route) => {
|
|
238
|
+
const key = `${route.method}:${route.path}`;
|
|
239
|
+
if (!acc[key]) acc[key] = { ...route, files: [route.file] };
|
|
240
|
+
else if (!acc[key].files.includes(route.file)) acc[key].files.push(route.file);
|
|
241
|
+
return acc;
|
|
242
|
+
}, {});
|
|
243
|
+
for (const route of Object.values(uniqueRoutes)) {
|
|
244
|
+
const fileName = route.file.split(/[/\\]/).pop();
|
|
245
|
+
console.log(` ${pc.red("ā")} ${pc.bold(route.method)} ${route.path}`);
|
|
246
|
+
console.log(` ${pc.dim("File:")} ${fileName}`);
|
|
247
|
+
}
|
|
248
|
+
logger.break();
|
|
249
|
+
}
|
|
250
|
+
if (middlewaresWithoutSchema.length > 0) {
|
|
251
|
+
console.log(pc.bold(pc.yellow("ā ļø Middlewares without schema:")));
|
|
252
|
+
logger.break();
|
|
253
|
+
for (const mw of middlewaresWithoutSchema) {
|
|
254
|
+
const fileName = mw.file.split(/[/\\]/).pop();
|
|
255
|
+
console.log(` ${pc.red("ā")} ${pc.bold(mw.name)}`);
|
|
256
|
+
console.log(` ${pc.dim("File:")} ${fileName}`);
|
|
257
|
+
}
|
|
258
|
+
logger.break();
|
|
259
|
+
}
|
|
260
|
+
if (routesWithoutSchema.length === 0 && middlewaresWithoutSchema.length === 0) {
|
|
261
|
+
console.log(pc.green(pc.bold("ā
All routes and middlewares have schemas defined!")));
|
|
262
|
+
logger.break();
|
|
263
|
+
}
|
|
264
|
+
const totalWithoutSchema = routesWithoutSchema.length + middlewaresWithoutSchema.length;
|
|
265
|
+
const totalItems = allRoutes.length + middlewares.length;
|
|
266
|
+
if (totalItems > 0) {
|
|
267
|
+
const coverage = Math.round((totalItems - totalWithoutSchema) / totalItems * 100);
|
|
268
|
+
console.log(pc.bold("š Schema Coverage: ") + (coverage >= 80 ? pc.green(`${coverage}%`) : coverage >= 50 ? pc.yellow(`${coverage}%`) : pc.red(`${coverage}%`)));
|
|
269
|
+
if (coverage < 80) {
|
|
270
|
+
logger.break();
|
|
271
|
+
console.log(pc.yellow("š” Tip: Adding schemas helps with:"));
|
|
272
|
+
console.log(pc.dim(" ⢠Request validation (body, query, params, headers)"));
|
|
273
|
+
console.log(pc.dim(" ⢠OpenAPI/Swagger UI auto-generation"));
|
|
274
|
+
console.log(pc.dim(" ⢠Better security through input validation"));
|
|
275
|
+
console.log(pc.dim(" ⢠Auto-completion in IDEs"));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
logger.break();
|
|
279
|
+
}
|
|
69
280
|
switch (command) {
|
|
70
281
|
case "dev":
|
|
71
282
|
console.log("š Starting development server with hot reload...");
|
|
@@ -85,6 +296,9 @@ switch (command) {
|
|
|
85
296
|
console.log("š Starting production server...");
|
|
86
297
|
runCommand("node dist/index.js", { NODE_ENV: "production" });
|
|
87
298
|
break;
|
|
299
|
+
case "doctor":
|
|
300
|
+
runDoctor();
|
|
301
|
+
break;
|
|
88
302
|
case "generate-keys":
|
|
89
303
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
90
304
|
modulusLength: 2048,
|
package/dist/esm/index.js
CHANGED
|
@@ -316,19 +316,18 @@ class Sprint {
|
|
|
316
316
|
const route = layer.route;
|
|
317
317
|
for (const routeLayer of route.stack) {
|
|
318
318
|
const handlers = Array.isArray(routeLayer.handle) ? routeLayer.handle : [routeLayer.handle];
|
|
319
|
+
let schema;
|
|
319
320
|
for (const handler of handlers) {
|
|
320
|
-
|
|
321
|
-
if (schema)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
321
|
+
schema = handler.__sprintRouteSchema;
|
|
322
|
+
if (schema) break;
|
|
323
|
+
}
|
|
324
|
+
const method = (routeLayer.method || "").toUpperCase();
|
|
325
|
+
if (method) {
|
|
326
|
+
this.registeredRoutes.push({
|
|
327
|
+
method,
|
|
328
|
+
path: finalRoute + route.path,
|
|
329
|
+
schema
|
|
330
|
+
});
|
|
332
331
|
}
|
|
333
332
|
}
|
|
334
333
|
}
|
|
@@ -447,6 +446,37 @@ class Sprint {
|
|
|
447
446
|
console.warn("[Sprint] Failed to convert headers schema:", e);
|
|
448
447
|
}
|
|
449
448
|
}
|
|
449
|
+
if (route.schema?.sprint?.authorization) {
|
|
450
|
+
const authSchema = route.schema.sprint.authorization;
|
|
451
|
+
const description = authSchema._def?.description;
|
|
452
|
+
let sources = ["query:token", "headers:authorization"];
|
|
453
|
+
if (description) {
|
|
454
|
+
try {
|
|
455
|
+
const parsed = JSON.parse(description);
|
|
456
|
+
if (parsed.__sprintAuthorization && parsed.sources) sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const isRequired = sources.length === 1;
|
|
461
|
+
for (const source of sources) {
|
|
462
|
+
const [type, key] = source.split(":");
|
|
463
|
+
if (type === "query") {
|
|
464
|
+
allParams.push({
|
|
465
|
+
name: key,
|
|
466
|
+
in: "query",
|
|
467
|
+
required: isRequired,
|
|
468
|
+
schema: { type: "string" }
|
|
469
|
+
});
|
|
470
|
+
} else if (type === "headers") {
|
|
471
|
+
allParams.push({
|
|
472
|
+
name: key,
|
|
473
|
+
in: "header",
|
|
474
|
+
required: isRequired,
|
|
475
|
+
schema: { type: "string" }
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
450
480
|
if (this.openapi.generateOnBuild) {
|
|
451
481
|
try {
|
|
452
482
|
const routeMiddlewares = this.getMiddlewaresForRoute(route.path);
|
|
@@ -472,26 +502,25 @@ class Sprint {
|
|
|
472
502
|
if (description) {
|
|
473
503
|
try {
|
|
474
504
|
const parsed = JSON.parse(description);
|
|
475
|
-
if (parsed.__sprintAuthorization && parsed.sources)
|
|
476
|
-
sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
|
|
477
|
-
}
|
|
505
|
+
if (parsed.__sprintAuthorization && parsed.sources) sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
|
|
478
506
|
} catch {
|
|
479
507
|
}
|
|
480
508
|
}
|
|
509
|
+
const isRequired = sources.length === 1;
|
|
481
510
|
for (const source of sources) {
|
|
482
511
|
const [type, key] = source.split(":");
|
|
483
512
|
if (type === "query") {
|
|
484
513
|
allParams.push({
|
|
485
514
|
name: key,
|
|
486
515
|
in: "query",
|
|
487
|
-
required:
|
|
516
|
+
required: isRequired,
|
|
488
517
|
schema: { type: "string" }
|
|
489
518
|
});
|
|
490
519
|
} else if (type === "headers") {
|
|
491
520
|
allParams.push({
|
|
492
521
|
name: key,
|
|
493
522
|
in: "header",
|
|
494
|
-
required:
|
|
523
|
+
required: isRequired,
|
|
495
524
|
schema: { type: "string" }
|
|
496
525
|
});
|
|
497
526
|
}
|
|
@@ -504,9 +533,7 @@ class Sprint {
|
|
|
504
533
|
}
|
|
505
534
|
}
|
|
506
535
|
if (allParams.length > 0) {
|
|
507
|
-
const uniqueParams = allParams.filter(
|
|
508
|
-
(param, index, self) => index === self.findIndex((p) => p.name === param.name && p.in === param.in)
|
|
509
|
-
);
|
|
536
|
+
const uniqueParams = allParams.filter((param, index, self) => index === self.findIndex((p) => p.name === param.name && p.in === param.in));
|
|
510
537
|
routeSpec.parameters = uniqueParams;
|
|
511
538
|
}
|
|
512
539
|
paths[route.path][method] = routeSpec;
|
|
@@ -531,25 +558,15 @@ class Sprint {
|
|
|
531
558
|
const zodDef = value._def;
|
|
532
559
|
const typeName = zodDef?.typeName;
|
|
533
560
|
let propSchema = {};
|
|
534
|
-
if (typeName === "ZodString") {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
propSchema = { type: "array", items: this.zodSchemaToOpenAPI(zodDef?.type) };
|
|
542
|
-
} else if (typeName === "ZodObject") {
|
|
543
|
-
propSchema = this.zodSchemaToOpenAPI(zodDef?.type);
|
|
544
|
-
} else if (typeName === "ZodOptional") {
|
|
545
|
-
continue;
|
|
546
|
-
} else {
|
|
547
|
-
propSchema = { type: "string" };
|
|
548
|
-
}
|
|
561
|
+
if (typeName === "ZodString") propSchema = { type: "string" };
|
|
562
|
+
else if (typeName === "ZodNumber") propSchema = { type: "number" };
|
|
563
|
+
else if (typeName === "ZodBoolean") propSchema = { type: "boolean" };
|
|
564
|
+
else if (typeName === "ZodArray") propSchema = { type: "array", items: this.zodSchemaToOpenAPI(zodDef?.type) };
|
|
565
|
+
else if (typeName === "ZodObject") propSchema = this.zodSchemaToOpenAPI(zodDef?.type);
|
|
566
|
+
else if (typeName === "ZodOptional") continue;
|
|
567
|
+
else propSchema = { type: "string" };
|
|
549
568
|
properties[key] = propSchema;
|
|
550
|
-
if (!zodDef?.isOptional && typeName !== "ZodOptional")
|
|
551
|
-
required.push(key);
|
|
552
|
-
}
|
|
569
|
+
if (!zodDef?.isOptional && typeName !== "ZodOptional") required.push(key);
|
|
553
570
|
}
|
|
554
571
|
return { type: "object", properties, required: required.length > 0 ? required : void 0 };
|
|
555
572
|
}
|
|
@@ -564,15 +581,10 @@ class Sprint {
|
|
|
564
581
|
const zodDef = value._def;
|
|
565
582
|
const typeName = zodDef?.typeName;
|
|
566
583
|
let paramSchema = {};
|
|
567
|
-
if (typeName === "ZodString") {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
} else if (typeName === "ZodBoolean") {
|
|
572
|
-
paramSchema = { type: "boolean" };
|
|
573
|
-
} else {
|
|
574
|
-
paramSchema = { type: "string" };
|
|
575
|
-
}
|
|
584
|
+
if (typeName === "ZodString") paramSchema = { type: "string" };
|
|
585
|
+
else if (typeName === "ZodNumber") paramSchema = { type: "number" };
|
|
586
|
+
else if (typeName === "ZodBoolean") paramSchema = { type: "boolean" };
|
|
587
|
+
else paramSchema = { type: "string" };
|
|
576
588
|
params.push({
|
|
577
589
|
name: key,
|
|
578
590
|
in: "query",
|
|
@@ -591,15 +603,10 @@ class Sprint {
|
|
|
591
603
|
const zodDef = value._def;
|
|
592
604
|
const typeName = zodDef?.typeName;
|
|
593
605
|
let paramSchema = {};
|
|
594
|
-
if (typeName === "ZodString") {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
} else if (typeName === "ZodBoolean") {
|
|
599
|
-
paramSchema = { type: "boolean" };
|
|
600
|
-
} else {
|
|
601
|
-
paramSchema = { type: "string" };
|
|
602
|
-
}
|
|
606
|
+
if (typeName === "ZodString") paramSchema = { type: "string" };
|
|
607
|
+
else if (typeName === "ZodNumber") paramSchema = { type: "number" };
|
|
608
|
+
else if (typeName === "ZodBoolean") paramSchema = { type: "boolean" };
|
|
609
|
+
else paramSchema = { type: "string" };
|
|
603
610
|
headers.push({
|
|
604
611
|
name: key,
|
|
605
612
|
in: "header",
|
|
@@ -688,7 +695,9 @@ class Sprint {
|
|
|
688
695
|
function createSchemaValidationMiddleware(schema) {
|
|
689
696
|
return (req, res, next) => {
|
|
690
697
|
const errors = [];
|
|
691
|
-
|
|
698
|
+
const method = req.method.toUpperCase();
|
|
699
|
+
const noBodyMethods = ["GET", "HEAD", "DELETE"];
|
|
700
|
+
if (schema.body && !noBodyMethods.includes(method)) {
|
|
692
701
|
const result = schema.body.safeParse(req.body);
|
|
693
702
|
if (!result.success) {
|
|
694
703
|
errors.push(...result.error.issues.map((issue) => ({
|
|
@@ -36,7 +36,9 @@ function parseSchema(schema, data) {
|
|
|
36
36
|
function defineRouteSchema(schema) {
|
|
37
37
|
const middleware = (req, res, next) => {
|
|
38
38
|
const errors = [];
|
|
39
|
-
|
|
39
|
+
const method = req.method.toUpperCase();
|
|
40
|
+
const noBodyMethods = ["GET", "HEAD", "DELETE"];
|
|
41
|
+
if (schema.body && !noBodyMethods.includes(method)) {
|
|
40
42
|
const result = parseSchema(schema.body, req.body);
|
|
41
43
|
if (!result.success) errors.push(...result.errors.map((e) => ({ location: "body", ...e })));
|
|
42
44
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAyC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAyC,MAAM,SAAS,CAAC;AA+FlF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CA+B3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,SAAS,IAAI,aAAa,EAAE,UAAU,EAAuB,MAAM,KAAK,CAAC;AAErF,MAAM,MAAM,mBAAmB,GAAG,SAAS,MAAM,EAAE,GAAG,WAAW,MAAM,EAAE,CAAC;AAE1E,MAAM,WAAW,0BAA0B;IACvC,OAAO,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;CACzD;AAED,iBAAS,+BAA+B,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAKxH;AAED,QAAA,MAAM,aAAa;;CAElB,CAAC;AAEF,QAAA,MAAM,UAAU;;CAEf,CAAC;AAEF,KAAK,cAAc,GAAG,OAAO,UAAU,GAAG,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAQpE,KAAK,aAAa,GAAG,OAAO,CAAC,GAAG;IAC5B,MAAM,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,QAAA,MAAM,MAAM,EAKN,aAAa,CAAC;AAEpB,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC;AACvB,OAAO,EAAE,aAAa,IAAI,MAAM,EAAE,CAAC;AAEnC,MAAM,WAAW,kBAAkB;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAC3C,WAAW,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;KAC7D,CAAC;CACL;AAqBD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,kBAAkB,EAAE,MAAM,EAAE,CAAC,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,SAAS,IAAI,aAAa,EAAE,UAAU,EAAuB,MAAM,KAAK,CAAC;AAErF,MAAM,MAAM,mBAAmB,GAAG,SAAS,MAAM,EAAE,GAAG,WAAW,MAAM,EAAE,CAAC;AAE1E,MAAM,WAAW,0BAA0B;IACvC,OAAO,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;CACzD;AAED,iBAAS,+BAA+B,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAKxH;AAED,QAAA,MAAM,aAAa;;CAElB,CAAC;AAEF,QAAA,MAAM,UAAU;;CAEf,CAAC;AAEF,KAAK,cAAc,GAAG,OAAO,UAAU,GAAG,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAQpE,KAAK,aAAa,GAAG,OAAO,CAAC,GAAG;IAC5B,MAAM,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,QAAA,MAAM,MAAM,EAKN,aAAa,CAAC;AAEpB,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC;AACvB,OAAO,EAAE,aAAa,IAAI,MAAM,EAAE,CAAC;AAEnC,MAAM,WAAW,kBAAkB;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAC3C,WAAW,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;KAC7D,CAAC;CACL;AAqBD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,kBAAkB,EAAE,MAAM,EAAE,CAAC,GAAG,cAAc,CA2EzF;AAED,YAAY,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/modules/telemetry/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,EAAE,QAAQ,GAAG,WAAW,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACtB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/modules/telemetry/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAEnE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AAEzC,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,EAAE,QAAQ,GAAG,WAAW,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sprint.d.ts","sourceRoot":"","sources":["../../src/sprint.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAA+B,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AACnG,OAAO,OAAO,EAAE,EAAE,WAAW,EAAoD,MAAM,SAAS,CAAC;AAejG,eAAO,MAAM,aAAa,SAAQ,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AAwCnC,qBAAa,MAAM;IACR,GAAG,EAAE,WAAW,CAAC;IACxB,OAAO,CAAC,IAAI,CAAwD;IACpE,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,OAAO,CAUb;IACF,OAAO,CAAC,gBAAgB,CAIhB;;YA2EM,IAAI;IA8BlB,OAAO,CAAC,YAAY;IAiDpB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA4B9B;;OAEG;YACW,eAAe;IAgC7B,OAAO,CAAC,eAAe;YAcT,UAAU;
|
|
1
|
+
{"version":3,"file":"sprint.d.ts","sourceRoot":"","sources":["../../src/sprint.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAA+B,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AACnG,OAAO,OAAO,EAAE,EAAE,WAAW,EAAoD,MAAM,SAAS,CAAC;AAejG,eAAO,MAAM,aAAa,SAAQ,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AAwCnC,qBAAa,MAAM;IACR,GAAG,EAAE,WAAW,CAAC;IACxB,OAAO,CAAC,IAAI,CAAwD;IACpE,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,OAAO,CAUb;IACF,OAAO,CAAC,gBAAgB,CAIhB;;YA2EM,IAAI;IA8BlB,OAAO,CAAC,YAAY;IAiDpB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA4B9B;;OAEG;YACW,eAAe;IAgC7B,OAAO,CAAC,eAAe;YAcT,UAAU;YAiFV,YAAY;IAmB1B,OAAO,CAAC,YAAY;IAgCpB,+BAA+B;IAC/B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,mBAAmB;IA+K3B,OAAO,CAAC,kBAAkB;IAqC1B,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,mBAAmB;IA+BpB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACnC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACrC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACpC,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,GAAG,gBAAgB,EAAE,YAAY,CAAC,EAAE,OAAO;IAYlF,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;CA0DzC"}
|