@seam-rpc/server 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/gen-client.js +193 -0
- package/dist/bin/gen-config.js +44 -0
- package/dist/bin/generate.d.ts +4 -0
- package/dist/bin/generate.js +82 -34
- package/dist/index.d.ts +2 -1
- package/dist/validation.js +54 -0
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
import { zodToTs, createAuxiliaryTypeStore, printNode } from "zod-to-ts";
|
|
7
|
+
import { existsSync, writeFileSync } from "fs";
|
|
8
|
+
export async function genClient() {
|
|
9
|
+
const args = process.argv;
|
|
10
|
+
let config;
|
|
11
|
+
if (args.length == 3) {
|
|
12
|
+
// Use config
|
|
13
|
+
// Check if config exists
|
|
14
|
+
if (!fs.existsSync("./seam-rpc.config.json"))
|
|
15
|
+
return console.error("\x1b[31mCommand arguments omitted and no config file found.\x1b[0m\n"
|
|
16
|
+
+ "Either define a config file with \x1b[36mseam-rpc gen-config\x1b[0m or generate the client files using \x1b[36mseam-rpc gen-client <input-files> <output-folder> [global-types-file]\x1b[0m.");
|
|
17
|
+
// Load config from config file
|
|
18
|
+
config = JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
else if (args.length == 5 || args.length == 6) {
|
|
21
|
+
// Use command args
|
|
22
|
+
// Load config from command args
|
|
23
|
+
config = {
|
|
24
|
+
inputFiles: args[3],
|
|
25
|
+
outputFolder: args[4]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Invalid command usage
|
|
30
|
+
return console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
|
|
31
|
+
}
|
|
32
|
+
const inputFiles = await fg(config.inputFiles);
|
|
33
|
+
const outputPath = path.resolve(config.outputFolder);
|
|
34
|
+
const rootPath = path.resolve(".");
|
|
35
|
+
try {
|
|
36
|
+
const outputFiles = [];
|
|
37
|
+
for (const inputFile of inputFiles) {
|
|
38
|
+
// console.log(getProcedureInfo(inputFile));
|
|
39
|
+
const outputFile = await generateClientFile(inputFile, outputPath);
|
|
40
|
+
outputFiles.push(removeRootPath(outputFile, rootPath));
|
|
41
|
+
}
|
|
42
|
+
console.log("\x1b[32m%s\x1b[0m\n\x1b[36m%s\x1b[0m", `✅ Successfully generated client files at ${removeRootPath(outputPath, rootPath)}`, `${outputFiles.join("\n")}`);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error("❌ Failed to generate client file:", err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// interface ProcedureInfo {
|
|
50
|
+
// name: string;
|
|
51
|
+
// inputType: string;
|
|
52
|
+
// outputType: string;
|
|
53
|
+
// comments: string;
|
|
54
|
+
// }
|
|
55
|
+
// export function getProcedureInfo(filePath: string): ProcedureInfo[] {
|
|
56
|
+
// const fullPath = path.resolve(filePath);
|
|
57
|
+
// const sourceText = fs.readFileSync(fullPath, "utf8");
|
|
58
|
+
// const sourceFile = ts.createSourceFile(
|
|
59
|
+
// fullPath,
|
|
60
|
+
// sourceText,
|
|
61
|
+
// ts.ScriptTarget.Latest,
|
|
62
|
+
// true,
|
|
63
|
+
// ts.ScriptKind.TS
|
|
64
|
+
// );
|
|
65
|
+
// const procedures: ProcedureInfo[] = [];
|
|
66
|
+
// function visit(node: ts.Node) {
|
|
67
|
+
// // Look for const assignments
|
|
68
|
+
// if (ts.isVariableStatement(node)) {
|
|
69
|
+
// node.declarationList.declarations.forEach((decl) => {
|
|
70
|
+
// if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
71
|
+
// let varName = decl.name.text;
|
|
72
|
+
// let inputType = "";
|
|
73
|
+
// let outputType = "";
|
|
74
|
+
// let comments = "";
|
|
75
|
+
// const jsDocs = ts.getJSDocCommentsAndTags(node); // node = VariableStatement
|
|
76
|
+
// // Get JSDoc comments
|
|
77
|
+
// if (jsDocs.length > 0) {
|
|
78
|
+
// comments = jsDocs.map(doc => doc.getText()).join("\n");
|
|
79
|
+
// }
|
|
80
|
+
// // Check for chain calls: .input(...).output(...)
|
|
81
|
+
// if (ts.isCallExpression(decl.initializer) || ts.isPropertyAccessExpression(decl.initializer)) {
|
|
82
|
+
// function extractChain(expr: ts.Expression) {
|
|
83
|
+
// if (ts.isCallExpression(expr)) {
|
|
84
|
+
// if (ts.isPropertyAccessExpression(expr.expression)) {
|
|
85
|
+
// const propName = expr.expression.name.text;
|
|
86
|
+
// if (propName === "input" && expr.arguments.length > 0) {
|
|
87
|
+
// inputType = expr.arguments[0].getText();
|
|
88
|
+
// } else if (propName === "output" && expr.arguments.length > 0) {
|
|
89
|
+
// outputType = expr.arguments[0].getText();
|
|
90
|
+
// }
|
|
91
|
+
// extractChain(expr.expression.expression);
|
|
92
|
+
// }
|
|
93
|
+
// } else if (ts.isPropertyAccessExpression(expr)) {
|
|
94
|
+
// extractChain(expr.expression);
|
|
95
|
+
// }
|
|
96
|
+
// }
|
|
97
|
+
// extractChain(decl.initializer);
|
|
98
|
+
// }
|
|
99
|
+
// if (inputType || outputType || comments) {
|
|
100
|
+
// procedures.push({ name: varName, inputType, outputType, comments });
|
|
101
|
+
// }
|
|
102
|
+
// }
|
|
103
|
+
// });
|
|
104
|
+
// }
|
|
105
|
+
// ts.forEachChild(node, visit);
|
|
106
|
+
// }
|
|
107
|
+
// visit(sourceFile);
|
|
108
|
+
// return procedures;
|
|
109
|
+
// }
|
|
110
|
+
function removeRootPath(path, rootPath) {
|
|
111
|
+
return "." + path.slice(rootPath.length);
|
|
112
|
+
}
|
|
113
|
+
async function generateClientFile(sourcePath, outputPath) {
|
|
114
|
+
const sourceFile = path.resolve(process.cwd(), sourcePath);
|
|
115
|
+
if (!existsSync(sourceFile)) {
|
|
116
|
+
console.error(`File ${sourceFile} not found`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const module = await import("file://" + sourceFile);
|
|
120
|
+
const procedures = module.default;
|
|
121
|
+
const routerName = path.basename(sourceFile, path.extname(sourceFile));
|
|
122
|
+
let fileContent = `/* Auto-generated by SeamRPC - DO NOT EDIT */
|
|
123
|
+
|
|
124
|
+
import { callApi } from \"@seam-rpc/client\";
|
|
125
|
+
|
|
126
|
+
`;
|
|
127
|
+
// Parse the source file using TypeScript API
|
|
128
|
+
const program = ts.createProgram([sourceFile], {});
|
|
129
|
+
const tsFile = program.getSourceFile(sourceFile);
|
|
130
|
+
// Helper to get JSDoc comment for a variable
|
|
131
|
+
function getJsDocComment(node) {
|
|
132
|
+
const jsDocs = node.jsDoc;
|
|
133
|
+
if (!jsDocs || jsDocs.length === 0)
|
|
134
|
+
return "";
|
|
135
|
+
return jsDocs.map(doc => doc.comment).filter(Boolean).join("\n");
|
|
136
|
+
}
|
|
137
|
+
const functions = [];
|
|
138
|
+
for (const [procName, proc] of Object.entries(procedures)) {
|
|
139
|
+
// Input
|
|
140
|
+
const input = [];
|
|
141
|
+
if (proc._def.input) {
|
|
142
|
+
for (const [paramName, param] of Object.entries(proc._def.input)) {
|
|
143
|
+
input.push(`${paramName}${param instanceof z.ZodOptional ? "?" : ""}: ${convert(param)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Output
|
|
147
|
+
const returnType = proc._def.output ? convert(proc._def.output) : "void";
|
|
148
|
+
// Input
|
|
149
|
+
const hasInput = input.length != 0;
|
|
150
|
+
const params = hasInput ? `input: { ${input.join(", ")} }` : "";
|
|
151
|
+
// Function body
|
|
152
|
+
const body = `return callApi("${routerName}", "${procName}"${hasInput ? ", input" : ""});`;
|
|
153
|
+
// Comments
|
|
154
|
+
let comments = "";
|
|
155
|
+
ts.forEachChild(tsFile, node => {
|
|
156
|
+
if (ts.isVariableStatement(node)) {
|
|
157
|
+
node.declarationList.declarations.forEach(decl => {
|
|
158
|
+
console.log(decl.name.getText(), procName);
|
|
159
|
+
if (decl.name.getText() === procName) {
|
|
160
|
+
const doc = getJsDocComment(decl);
|
|
161
|
+
if (doc)
|
|
162
|
+
comments = `/** ${doc} */\n`;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const func = `${comments}export function ${procName}(${params}): Promise<${returnType}> { ${body} }`;
|
|
168
|
+
functions.push(func);
|
|
169
|
+
}
|
|
170
|
+
fileContent += functions.join("\n\n");
|
|
171
|
+
writeFileSync(`${outputPath}/${routerName}.ts`, fileContent, "utf-8");
|
|
172
|
+
return sourceFile;
|
|
173
|
+
}
|
|
174
|
+
function convert(schema) {
|
|
175
|
+
const auxiliaryTypeStore = createAuxiliaryTypeStore();
|
|
176
|
+
const { node } = zodToTs(schema, { auxiliaryTypeStore });
|
|
177
|
+
return printNode(node);
|
|
178
|
+
}
|
|
179
|
+
// function zodToTs(schema: z.ZodType): { value: string, definition?: string } {
|
|
180
|
+
// if (schema instanceof z.ZodString) {
|
|
181
|
+
// return { value: "string" };
|
|
182
|
+
// }
|
|
183
|
+
// if (schema instanceof z.ZodNumber) {
|
|
184
|
+
// return { value: "number" };
|
|
185
|
+
// }
|
|
186
|
+
// if (schema instanceof z.ZodArray) {
|
|
187
|
+
// return { value: zodToTs(schema.def.type) + "[]" };
|
|
188
|
+
// }
|
|
189
|
+
// if (schema instanceof z.ZodOptional) {
|
|
190
|
+
// return {value: };
|
|
191
|
+
// }
|
|
192
|
+
// return { value: "any" };
|
|
193
|
+
// }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
export async function genConfig() {
|
|
4
|
+
const args = process.argv;
|
|
5
|
+
let inputFiles = "./src/api/*";
|
|
6
|
+
let outputFolder = "../client/src/api";
|
|
7
|
+
if (args.length == 5) {
|
|
8
|
+
inputFiles = args[3];
|
|
9
|
+
outputFolder = args[4];
|
|
10
|
+
}
|
|
11
|
+
else if (args.length > 3) {
|
|
12
|
+
return console.error("Usage: seam-rpc gen-config [input-files] [output-folder]");
|
|
13
|
+
}
|
|
14
|
+
if (fs.existsSync("./seam-rpc.config.json")) {
|
|
15
|
+
const rl = readline.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
});
|
|
19
|
+
rl.question("Config file already exists. Do you want to overwrite it? [Y/n] ", answer => {
|
|
20
|
+
if (answer && answer.toLowerCase() != "y" && answer.toLowerCase() != "yes") {
|
|
21
|
+
console.log("Operation canceled.");
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
rl.close();
|
|
25
|
+
generateConfig();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
generateConfig();
|
|
30
|
+
}
|
|
31
|
+
function generateConfig() {
|
|
32
|
+
const config = {
|
|
33
|
+
inputFiles,
|
|
34
|
+
outputFolder
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync("./seam-rpc.config.json", JSON.stringify(config, null, 4), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.log("\x1b[31mFailed to generate config file ./seam-rpc.config.json\x1b[0m\n" + e);
|
|
41
|
+
}
|
|
42
|
+
console.log(`\x1b[32mSuccessfully generated config file ./seam-rpc.config.json\x1b[0m`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/bin/generate.d.ts
CHANGED
|
@@ -4,5 +4,9 @@ interface GenerateOptions {
|
|
|
4
4
|
jsPath: string;
|
|
5
5
|
outputPath: string;
|
|
6
6
|
}
|
|
7
|
+
export declare function getProcedureMetadata(filePath: string): Record<string, {
|
|
8
|
+
comment: string;
|
|
9
|
+
returnType?: string;
|
|
10
|
+
}>;
|
|
7
11
|
export declare function generateClientFile({ tsPath, jsPath, outputPath, }: GenerateOptions): Promise<string>;
|
|
8
12
|
export {};
|
package/dist/bin/generate.js
CHANGED
|
@@ -15,20 +15,19 @@ export async function generateClient() {
|
|
|
15
15
|
function removeRootPath(path) {
|
|
16
16
|
return "." + path.slice(rootPath.length);
|
|
17
17
|
}
|
|
18
|
-
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
console.log("\x1b[32m\x1b[1m%s\x1b[0m\n\x1b[94m%s\x1b[0m", `✅ Successfully generated client files at ${config.outputFolder}`, `${outputFiles.join("\n")}`);
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
|
|
30
|
-
process.exit(1);
|
|
18
|
+
// try {
|
|
19
|
+
const outputFiles = [];
|
|
20
|
+
for (let i = 0; i < sourceFiles.length; i++) {
|
|
21
|
+
const sourceFile = sourceFiles[i];
|
|
22
|
+
const path = await generateClientFile({ tsPath: sourceFile, jsPath: compiledFolder, outputPath });
|
|
23
|
+
const symb = i < sourceFiles.length - 1 ? " ├╴" : " └╴";
|
|
24
|
+
outputFiles.push(symb + removeRootPath(path));
|
|
31
25
|
}
|
|
26
|
+
console.log("\x1b[32m\x1b[1m%s\x1b[0m\n\x1b[94m%s\x1b[0m", `✅ Successfully generated client files at ${config.outputFolder}`, `${outputFiles.join("\n")}`);
|
|
27
|
+
// } catch (err: any) {
|
|
28
|
+
// console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
|
|
29
|
+
// process.exit(1);
|
|
30
|
+
// }
|
|
32
31
|
}
|
|
33
32
|
function loadConfig() {
|
|
34
33
|
// Check if config exists
|
|
@@ -40,22 +39,64 @@ function loadConfig() {
|
|
|
40
39
|
return JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
41
40
|
}
|
|
42
41
|
// Extracts comments from TS file.
|
|
43
|
-
function
|
|
42
|
+
export function getProcedureMetadata(filePath) {
|
|
44
43
|
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
45
|
-
const
|
|
46
|
-
|
|
44
|
+
const program = ts.createProgram([filePath], {
|
|
45
|
+
target: ts.ScriptTarget.Latest,
|
|
46
|
+
module: ts.ModuleKind.CommonJS,
|
|
47
|
+
strict: true,
|
|
48
|
+
});
|
|
49
|
+
const checker = program.getTypeChecker();
|
|
50
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
51
|
+
if (!sourceFile)
|
|
52
|
+
return {};
|
|
53
|
+
const metadata = {};
|
|
54
|
+
function getHandlerReturnType(node) {
|
|
55
|
+
let current = node;
|
|
56
|
+
while (current) {
|
|
57
|
+
if (ts.isCallExpression(current) &&
|
|
58
|
+
ts.isPropertyAccessExpression(current.expression)) {
|
|
59
|
+
const prop = current.expression;
|
|
60
|
+
if (prop.name.text === "handler") {
|
|
61
|
+
const handlerFn = current.arguments[0];
|
|
62
|
+
if (handlerFn &&
|
|
63
|
+
(ts.isArrowFunction(handlerFn) ||
|
|
64
|
+
ts.isFunctionExpression(handlerFn))) {
|
|
65
|
+
const signature = checker.getSignatureFromDeclaration(handlerFn);
|
|
66
|
+
if (!signature)
|
|
67
|
+
return;
|
|
68
|
+
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
69
|
+
return checker.typeToString(returnType);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
current = prop.expression;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
47
79
|
function visit(node) {
|
|
48
80
|
if (ts.isVariableStatement(node)) {
|
|
49
81
|
const comment = getFullComment(node, sourceText);
|
|
50
82
|
for (const decl of node.declarationList.declarations) {
|
|
51
|
-
if (ts.isIdentifier(decl.name))
|
|
52
|
-
|
|
83
|
+
if (!ts.isIdentifier(decl.name))
|
|
84
|
+
continue;
|
|
85
|
+
const name = decl.name.text;
|
|
86
|
+
let returnType;
|
|
87
|
+
if (decl.initializer) {
|
|
88
|
+
returnType = getHandlerReturnType(decl.initializer);
|
|
89
|
+
}
|
|
90
|
+
metadata[name] = {
|
|
91
|
+
comment,
|
|
92
|
+
returnType,
|
|
93
|
+
};
|
|
53
94
|
}
|
|
54
95
|
}
|
|
55
96
|
ts.forEachChild(node, visit);
|
|
56
97
|
}
|
|
57
98
|
visit(sourceFile);
|
|
58
|
-
return
|
|
99
|
+
return metadata;
|
|
59
100
|
}
|
|
60
101
|
function getFullComment(node, sourceText) {
|
|
61
102
|
const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
|
|
@@ -88,7 +129,7 @@ export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
|
|
|
88
129
|
const mod = await import("file://" + jsFile);
|
|
89
130
|
const procedures = mod.default;
|
|
90
131
|
const routerName = path.basename(jsFile, path.extname(jsFile));
|
|
91
|
-
const
|
|
132
|
+
const procMetadata = getProcedureMetadata(tsFile);
|
|
92
133
|
let output = `/**
|
|
93
134
|
* +===================================+
|
|
94
135
|
* | File auto-generated by SeamRPC. |
|
|
@@ -105,28 +146,35 @@ import { callApi, Result, RpcError } from "@seam-rpc/client";
|
|
|
105
146
|
let inputType = "void";
|
|
106
147
|
let hasInput = false;
|
|
107
148
|
if (proc._def.input) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
try {
|
|
150
|
+
const fields = [];
|
|
151
|
+
for (const [key, schema] of Object.entries(proc._def.input)) {
|
|
152
|
+
const isOptional = schema.isOptional?.() ?? false;
|
|
153
|
+
const tsType = convert(schema);
|
|
154
|
+
fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
|
|
155
|
+
}
|
|
156
|
+
if (fields.length > 0) {
|
|
157
|
+
inputType = `{ ${fields.join("; ")} }`;
|
|
158
|
+
hasInput = true;
|
|
159
|
+
}
|
|
113
160
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
161
|
+
catch (e) {
|
|
162
|
+
console.error(`Failed to process input of procedure "${name}".`);
|
|
163
|
+
process.exit(1);
|
|
117
164
|
}
|
|
118
165
|
}
|
|
119
|
-
const dataType = proc._def.output ? convert(proc._def.output) : "void";
|
|
120
|
-
const errors = Object.entries(proc._def.errors ?? {});
|
|
121
|
-
const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1])}>`).join(" | ");
|
|
122
|
-
const fullReturnType = `Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}
|
|
166
|
+
// const dataType = proc._def.output ? convert(proc._def.output) : "void";
|
|
167
|
+
// const errors = Object.entries(proc._def.errors ?? {});
|
|
168
|
+
// const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1] as any)}>`).join(" | ");
|
|
169
|
+
// const fullReturnType = `Promise<Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>>`;
|
|
170
|
+
const fullReturnType = procMetadata[name].returnType;
|
|
123
171
|
const params = hasInput ? `input: ${inputType}` : "";
|
|
124
172
|
const args = [`"${routerName}"`, `"${name}"`];
|
|
125
173
|
if (hasInput)
|
|
126
174
|
args.push("input");
|
|
127
175
|
const call = `callApi(${args.join(", ")})`;
|
|
128
|
-
const comment =
|
|
129
|
-
const func = `${comment}export function ${name}(${params}):
|
|
176
|
+
const comment = procMetadata[name].comment ? procMetadata[name].comment + "\n" : "";
|
|
177
|
+
const func = `${comment}export function ${name}(${params}): ${fullReturnType} {
|
|
130
178
|
return ${call};
|
|
131
179
|
}`;
|
|
132
180
|
functions.push(func);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Result } from "@seam-rpc/core";
|
|
1
2
|
import EventEmitter from "events";
|
|
2
3
|
import express, { Express, NextFunction, Request, RequestHandler, Response } from "express";
|
|
3
4
|
import * as z from "zod";
|
|
@@ -29,7 +30,7 @@ export interface SeamEvents {
|
|
|
29
30
|
inputValidationError: [error: unknown, context: SeamErrorContext];
|
|
30
31
|
outputValidationError: [error: unknown, context: SeamErrorContext];
|
|
31
32
|
}
|
|
32
|
-
type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => z.infer<Output> | Promise<z.infer<Output>>;
|
|
33
|
+
type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => Result<z.infer<Output>, unknown> | Promise<Result<z.infer<Output>, unknown>>;
|
|
33
34
|
interface ProcedureOptions<Input extends ProcedureInput, Errors extends ProcedureErrors> {
|
|
34
35
|
input: Simplify<ProcedureInputData<Input>>;
|
|
35
36
|
ctx: SeamContext;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class ValString {
|
|
2
|
+
constructor(value) {
|
|
3
|
+
this._isValid = true;
|
|
4
|
+
this._value = value;
|
|
5
|
+
}
|
|
6
|
+
get isValid() {
|
|
7
|
+
return this._isValid;
|
|
8
|
+
}
|
|
9
|
+
min(value) {
|
|
10
|
+
if (this._isValid)
|
|
11
|
+
this._isValid = this._value.length >= value;
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
max(value) {
|
|
15
|
+
if (this._isValid)
|
|
16
|
+
this._isValid = this._value.length <= value;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
length(value) {
|
|
20
|
+
if (this._isValid)
|
|
21
|
+
this._isValid = this._value.length == value;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
startsWith(value) {
|
|
25
|
+
if (this._isValid)
|
|
26
|
+
this._isValid = this._value.startsWith(value);
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
endsWith(value) {
|
|
30
|
+
if (this._isValid)
|
|
31
|
+
this._isValid = this._value.endsWith(value);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
includes(value) {
|
|
35
|
+
if (this._isValid)
|
|
36
|
+
this._isValid = this._value.includes(value);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
regex(regExpr) {
|
|
40
|
+
// if (this._isValid)
|
|
41
|
+
// this._isValid = this._value.startsWith(value);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
email() {
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function createUser(name, age, data) {
|
|
49
|
+
name.min(3).max(50);
|
|
50
|
+
age.gte(1).lt(150);
|
|
51
|
+
data.description?.max(250);
|
|
52
|
+
}
|
|
53
|
+
function validate(data) {
|
|
54
|
+
}
|