@seam-rpc/server 3.0.2 → 4.1.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/dist/bin/gen-client.d.ts +7 -0
- package/dist/bin/gen-client.js +131 -78
- package/dist/bin/gen-config.js +5 -3
- package/dist/bin/generate.d.ts +8 -0
- package/dist/bin/generate.js +135 -0
- package/dist/bin/index.d.ts +2 -1
- package/dist/bin/index.js +7 -7
- package/dist/bin/init.d.ts +1 -0
- package/dist/bin/init.js +34 -0
- package/dist/index.d.ts +63 -17
- package/dist/index.js +175 -55
- package/package.json +6 -4
package/dist/bin/gen-client.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
1
|
export declare function genClient(): Promise<void>;
|
|
2
|
+
interface GenerateOptions {
|
|
3
|
+
tsPath: string;
|
|
4
|
+
jsPath: string;
|
|
5
|
+
outputPath: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateClientFile({ tsPath, jsPath, outputPath, }: GenerateOptions): Promise<string>;
|
|
8
|
+
export {};
|
package/dist/bin/gen-client.js
CHANGED
|
@@ -1,101 +1,154 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import ts from "typescript";
|
|
3
2
|
import path from "path";
|
|
4
3
|
import fg from "fast-glob";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { zodToTs, createAuxiliaryTypeStore, printNode } from "zod-to-ts";
|
|
6
|
+
import { existsSync, writeFileSync } from "fs";
|
|
5
7
|
export async function genClient() {
|
|
6
8
|
const args = process.argv;
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
config = JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
13
|
-
}
|
|
14
|
-
else if (args.length == 5 || args.length == 6) {
|
|
15
|
-
config = {
|
|
16
|
-
inputFiles: args[3],
|
|
17
|
-
outputFolder: args[4]
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
return console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
|
|
22
|
-
}
|
|
23
|
-
const inputFiles = await fg(config.inputFiles);
|
|
9
|
+
const config = loadConfig(args);
|
|
10
|
+
if (!config)
|
|
11
|
+
return;
|
|
12
|
+
const sourceFiles = await fg(config.source);
|
|
13
|
+
const compiledFolder = path.resolve(config.compiledFolder);
|
|
24
14
|
const outputPath = path.resolve(config.outputFolder);
|
|
25
15
|
const rootPath = path.resolve(".");
|
|
16
|
+
function removeRootPath(path) {
|
|
17
|
+
return "." + path.slice(rootPath.length);
|
|
18
|
+
}
|
|
26
19
|
try {
|
|
27
20
|
const outputFiles = [];
|
|
28
|
-
for (
|
|
29
|
-
const
|
|
30
|
-
|
|
21
|
+
for (let i = 0; i < sourceFiles.length; i++) {
|
|
22
|
+
const sourceFile = sourceFiles[i];
|
|
23
|
+
const path = await generateClientFile({ tsPath: sourceFile, jsPath: compiledFolder, outputPath });
|
|
24
|
+
const symb = i < sourceFiles.length - 1 ? " ├╴" : " └╴";
|
|
25
|
+
outputFiles.push(symb + removeRootPath(path));
|
|
31
26
|
}
|
|
32
|
-
console.log("\x1b[32m%s\x1b[0m\n\x1b[
|
|
27
|
+
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")}`);
|
|
33
28
|
}
|
|
34
29
|
catch (err) {
|
|
35
30
|
console.error("❌ Failed to generate client file:", err.message);
|
|
36
31
|
process.exit(1);
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
const imports = ["import { callApi, SeamFile, ISeamFile } from \"@seam-rpc/client\";"];
|
|
49
|
-
const apiDef = [];
|
|
50
|
-
const typeDefs = [];
|
|
51
|
-
const routerName = path.basename(file, path.extname(file));
|
|
52
|
-
const fileContent = fs.readFileSync(file, "utf-8");
|
|
53
|
-
const sourceFile = ts.createSourceFile(file, fileContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
54
|
-
ts.forEachChild(sourceFile, (node) => {
|
|
55
|
-
if (ts.isImportDeclaration(node)) {
|
|
56
|
-
const moduleSpecifier = node.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
57
|
-
if (moduleSpecifier.startsWith("./"))
|
|
58
|
-
imports.push(node.getText());
|
|
34
|
+
function loadConfig(args) {
|
|
35
|
+
if (args.length == 3) {
|
|
36
|
+
// Use config
|
|
37
|
+
// Check if config exists
|
|
38
|
+
if (!fs.existsSync("./seam-rpc.config.json")) {
|
|
39
|
+
console.error("\x1b[31mCommand arguments omitted and no config file found.\x1b[0m\n"
|
|
40
|
+
+ "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.");
|
|
41
|
+
return null;
|
|
59
42
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
43
|
+
// Load config from config file
|
|
44
|
+
return JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
else if (args.length == 5 || args.length == 6) {
|
|
47
|
+
// Use command args
|
|
48
|
+
// Load config from command args
|
|
49
|
+
return {
|
|
50
|
+
source: args[3],
|
|
51
|
+
compiledFolder: args[4],
|
|
52
|
+
outputFolder: args[5]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Invalid command usage
|
|
57
|
+
console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Extracts comments from TS file.
|
|
62
|
+
function getProcedureComments(filePath) {
|
|
63
|
+
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
64
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
|
|
65
|
+
const comments = {};
|
|
66
|
+
function visit(node) {
|
|
67
|
+
if (ts.isVariableStatement(node)) {
|
|
68
|
+
const comment = getFullComment(node, sourceText);
|
|
69
|
+
for (const decl of node.declarationList.declarations) {
|
|
70
|
+
if (ts.isIdentifier(decl.name))
|
|
71
|
+
comments[decl.name.text] = comment;
|
|
64
72
|
}
|
|
65
|
-
const funcName = node.name.getText();
|
|
66
|
-
const jsDoc = ts.getJSDocCommentsAndTags(node).map(e => e.getFullText()).filter(Boolean).join("\n");
|
|
67
|
-
let signature = `${jsDoc}\nexport function ${funcName}(`;
|
|
68
|
-
const params = node.parameters.filter(p => !(p.type && p.type.getText() === "SeamContext"));
|
|
69
|
-
const paramsText = params
|
|
70
|
-
.map((p) => {
|
|
71
|
-
const paramName = p.name.getText();
|
|
72
|
-
const optional = p.questionToken ? "?" : "";
|
|
73
|
-
const type = p.type ? p.type.getText() : "any";
|
|
74
|
-
return `${paramName}${optional}: ${type}`;
|
|
75
|
-
})
|
|
76
|
-
.join(", ");
|
|
77
|
-
const returnTypeText = node.type?.getText() ?? "any";
|
|
78
|
-
signature += `${paramsText}): ${returnTypeText} { return callApi("${routerName}", "${funcName}", [${params.map(e => e.name.getText()).join(", ")}]); }`;
|
|
79
|
-
apiDef.push(signature);
|
|
80
73
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
ts.forEachChild(node, visit);
|
|
75
|
+
}
|
|
76
|
+
visit(sourceFile);
|
|
77
|
+
return comments;
|
|
78
|
+
}
|
|
79
|
+
function getFullComment(node, sourceText) {
|
|
80
|
+
const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
|
|
81
|
+
if (!ranges)
|
|
82
|
+
return "";
|
|
83
|
+
for (const range of ranges) {
|
|
84
|
+
const comment = sourceText.slice(range.pos, range.end);
|
|
85
|
+
// Only keep JSDoc-style comments
|
|
86
|
+
if (comment.startsWith("/**")) {
|
|
87
|
+
return comment;
|
|
87
88
|
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
fs.writeFileSync(path.resolve(outputPath, path.basename(file)), content, "utf-8");
|
|
91
|
-
return file;
|
|
89
|
+
}
|
|
90
|
+
return "";
|
|
92
91
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
function convert(schema) {
|
|
93
|
+
const store = createAuxiliaryTypeStore();
|
|
94
|
+
const { node } = zodToTs(schema, { auxiliaryTypeStore: store });
|
|
95
|
+
return printNode(node);
|
|
96
|
+
}
|
|
97
|
+
export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
|
|
98
|
+
const tsFile = path.resolve(process.cwd(), tsPath);
|
|
99
|
+
const tsFileName = path.basename(tsFile).slice(0, -path.extname(tsFile).length);
|
|
100
|
+
const jsFile = path.resolve(process.cwd(), path.join(jsPath, tsFileName + ".js"));
|
|
101
|
+
if (!existsSync(tsFile)) {
|
|
102
|
+
throw new Error(`TS file not found: ${tsFile}`);
|
|
103
|
+
}
|
|
104
|
+
if (!existsSync(jsFile)) {
|
|
105
|
+
throw new Error(`JS file not found: ${jsFile}`);
|
|
106
|
+
}
|
|
107
|
+
const mod = await import("file://" + jsFile);
|
|
108
|
+
const procedures = mod.default;
|
|
109
|
+
const routerName = path.basename(jsFile, path.extname(jsFile));
|
|
110
|
+
const comments = getProcedureComments(tsFile);
|
|
111
|
+
let output = `/**
|
|
112
|
+
* +===================================+
|
|
113
|
+
* | File auto-generated by SeamRPC. |
|
|
114
|
+
* | --- DO NOT EDIT --- |
|
|
115
|
+
* +===================================+
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
import { callApi } from "@seam-rpc/client";
|
|
119
|
+
|
|
120
|
+
`;
|
|
121
|
+
const functions = [];
|
|
122
|
+
for (const [name, proc] of Object.entries(procedures)) {
|
|
123
|
+
// Input
|
|
124
|
+
let inputType = "void";
|
|
125
|
+
let hasInput = false;
|
|
126
|
+
if (proc._def.input) {
|
|
127
|
+
const fields = [];
|
|
128
|
+
for (const [key, schema] of Object.entries(proc._def.input)) {
|
|
129
|
+
const isOptional = schema.isOptional?.() ?? false;
|
|
130
|
+
const tsType = convert(schema);
|
|
131
|
+
fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
|
|
132
|
+
}
|
|
133
|
+
if (fields.length > 0) {
|
|
134
|
+
inputType = `{ ${fields.join("; ")} }`;
|
|
135
|
+
hasInput = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const returnType = proc._def.output ? convert(proc._def.output) : "void";
|
|
139
|
+
const params = hasInput ? `input: ${inputType}` : "";
|
|
140
|
+
const args = [`"${routerName}"`, `"${name}"`];
|
|
141
|
+
if (hasInput)
|
|
142
|
+
args.push("input");
|
|
143
|
+
const call = `callApi(${args.join(", ")})`;
|
|
144
|
+
const comment = comments[name] ? comments[name] + "\n" : "";
|
|
145
|
+
const func = `${comment}export function ${name}(${params}): Promise<${returnType}> {
|
|
146
|
+
return ${call};
|
|
147
|
+
}`;
|
|
148
|
+
functions.push(func);
|
|
99
149
|
}
|
|
100
|
-
|
|
150
|
+
output += functions.join("\n\n");
|
|
151
|
+
const outFile = path.join(outputPath, `${routerName}.ts`);
|
|
152
|
+
writeFileSync(outFile, output, "utf-8");
|
|
153
|
+
return outFile;
|
|
101
154
|
}
|
package/dist/bin/gen-config.js
CHANGED
|
@@ -2,10 +2,11 @@ import fs from "fs";
|
|
|
2
2
|
import readline from "readline";
|
|
3
3
|
export async function genConfig() {
|
|
4
4
|
const args = process.argv;
|
|
5
|
-
let
|
|
5
|
+
let source = "./src/api/*";
|
|
6
|
+
let compiledFolder = "./dist/api";
|
|
6
7
|
let outputFolder = "../client/src/api";
|
|
7
8
|
if (args.length == 5) {
|
|
8
|
-
|
|
9
|
+
source = args[3];
|
|
9
10
|
outputFolder = args[4];
|
|
10
11
|
}
|
|
11
12
|
else if (args.length > 3) {
|
|
@@ -30,7 +31,8 @@ export async function genConfig() {
|
|
|
30
31
|
}
|
|
31
32
|
function generateConfig() {
|
|
32
33
|
const config = {
|
|
33
|
-
|
|
34
|
+
source,
|
|
35
|
+
compiledFolder,
|
|
34
36
|
outputFolder
|
|
35
37
|
};
|
|
36
38
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function generateClient(): Promise<void>;
|
|
2
|
+
interface GenerateOptions {
|
|
3
|
+
tsPath: string;
|
|
4
|
+
jsPath: string;
|
|
5
|
+
outputPath: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateClientFile({ tsPath, jsPath, outputPath, }: GenerateOptions): Promise<string>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { zodToTs, createAuxiliaryTypeStore, printNode } from "zod-to-ts";
|
|
6
|
+
import { existsSync, writeFileSync } from "fs";
|
|
7
|
+
export async function generateClient() {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
if (!config)
|
|
10
|
+
return;
|
|
11
|
+
const sourceFiles = await fg(config.source);
|
|
12
|
+
const compiledFolder = path.resolve(config.compiledFolder);
|
|
13
|
+
const outputPath = path.resolve(config.outputFolder);
|
|
14
|
+
const rootPath = path.resolve(".");
|
|
15
|
+
function removeRootPath(path) {
|
|
16
|
+
return "." + path.slice(rootPath.length);
|
|
17
|
+
}
|
|
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));
|
|
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);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function loadConfig() {
|
|
34
|
+
// Check if config exists
|
|
35
|
+
if (!fs.existsSync("./seam-rpc.config.json")) {
|
|
36
|
+
console.error("\x1b[31mNo config file found.\x1b[0m\n Create a config file with \x1b[36msrpc init\x1b[0m.");
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// Load config from config file
|
|
40
|
+
return JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
// Extracts comments from TS file.
|
|
43
|
+
function getProcedureComments(filePath) {
|
|
44
|
+
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
45
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
|
|
46
|
+
const comments = {};
|
|
47
|
+
function visit(node) {
|
|
48
|
+
if (ts.isVariableStatement(node)) {
|
|
49
|
+
const comment = getFullComment(node, sourceText);
|
|
50
|
+
for (const decl of node.declarationList.declarations) {
|
|
51
|
+
if (ts.isIdentifier(decl.name))
|
|
52
|
+
comments[decl.name.text] = comment;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
ts.forEachChild(node, visit);
|
|
56
|
+
}
|
|
57
|
+
visit(sourceFile);
|
|
58
|
+
return comments;
|
|
59
|
+
}
|
|
60
|
+
function getFullComment(node, sourceText) {
|
|
61
|
+
const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
|
|
62
|
+
if (!ranges)
|
|
63
|
+
return "";
|
|
64
|
+
for (const range of ranges) {
|
|
65
|
+
const comment = sourceText.slice(range.pos, range.end);
|
|
66
|
+
// Only keep JSDoc-style comments
|
|
67
|
+
if (comment.startsWith("/**")) {
|
|
68
|
+
return comment;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
function convert(schema) {
|
|
74
|
+
const store = createAuxiliaryTypeStore();
|
|
75
|
+
const { node } = zodToTs(schema, { auxiliaryTypeStore: store });
|
|
76
|
+
return printNode(node);
|
|
77
|
+
}
|
|
78
|
+
export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
|
|
79
|
+
const tsFile = path.resolve(process.cwd(), tsPath);
|
|
80
|
+
const tsFileName = path.basename(tsFile).slice(0, -path.extname(tsFile).length);
|
|
81
|
+
const jsFile = path.resolve(process.cwd(), path.join(jsPath, tsFileName + ".js"));
|
|
82
|
+
if (!existsSync(tsFile)) {
|
|
83
|
+
throw new Error(`TS file not found: ${tsFile}`);
|
|
84
|
+
}
|
|
85
|
+
if (!existsSync(jsFile)) {
|
|
86
|
+
throw new Error(`JS file not found: ${jsFile}`);
|
|
87
|
+
}
|
|
88
|
+
const mod = await import("file://" + jsFile);
|
|
89
|
+
const procedures = mod.default;
|
|
90
|
+
const routerName = path.basename(jsFile, path.extname(jsFile));
|
|
91
|
+
const comments = getProcedureComments(tsFile);
|
|
92
|
+
let output = `/**
|
|
93
|
+
* +===================================+
|
|
94
|
+
* | File auto-generated by SeamRPC. |
|
|
95
|
+
* | --- DO NOT EDIT --- |
|
|
96
|
+
* +===================================+
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
import { callApi } from "@seam-rpc/client";
|
|
100
|
+
|
|
101
|
+
`;
|
|
102
|
+
const functions = [];
|
|
103
|
+
for (const [name, proc] of Object.entries(procedures)) {
|
|
104
|
+
// Input
|
|
105
|
+
let inputType = "void";
|
|
106
|
+
let hasInput = false;
|
|
107
|
+
if (proc._def.input) {
|
|
108
|
+
const fields = [];
|
|
109
|
+
for (const [key, schema] of Object.entries(proc._def.input)) {
|
|
110
|
+
const isOptional = schema.isOptional?.() ?? false;
|
|
111
|
+
const tsType = convert(schema);
|
|
112
|
+
fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
|
|
113
|
+
}
|
|
114
|
+
if (fields.length > 0) {
|
|
115
|
+
inputType = `{ ${fields.join("; ")} }`;
|
|
116
|
+
hasInput = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const returnType = proc._def.output ? convert(proc._def.output) : "void";
|
|
120
|
+
const params = hasInput ? `input: ${inputType}` : "";
|
|
121
|
+
const args = [`"${routerName}"`, `"${name}"`];
|
|
122
|
+
if (hasInput)
|
|
123
|
+
args.push("input");
|
|
124
|
+
const call = `callApi(${args.join(", ")})`;
|
|
125
|
+
const comment = comments[name] ? comments[name] + "\n" : "";
|
|
126
|
+
const func = `${comment}export function ${name}(${params}): Promise<${returnType}> {
|
|
127
|
+
return ${call};
|
|
128
|
+
}`;
|
|
129
|
+
functions.push(func);
|
|
130
|
+
}
|
|
131
|
+
output += functions.join("\n\n");
|
|
132
|
+
const outFile = path.join(outputPath, `${routerName}.ts`);
|
|
133
|
+
writeFileSync(outFile, output, "utf-8");
|
|
134
|
+
return outFile;
|
|
135
|
+
}
|
package/dist/bin/index.d.ts
CHANGED
package/dist/bin/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { generateClient } from "./generate.js";
|
|
3
|
+
import { initConfig } from "./init.js";
|
|
4
4
|
main();
|
|
5
5
|
function main() {
|
|
6
6
|
const args = process.argv;
|
|
7
7
|
switch (args[2]) {
|
|
8
|
-
case "
|
|
9
|
-
|
|
8
|
+
case "generate":
|
|
9
|
+
generateClient();
|
|
10
10
|
break;
|
|
11
|
-
case "
|
|
12
|
-
|
|
11
|
+
case "init":
|
|
12
|
+
initConfig();
|
|
13
13
|
break;
|
|
14
14
|
default:
|
|
15
|
-
console.log("Commands:\n-
|
|
15
|
+
console.log("Commands:\n- \x1b[36msrpc generate\x1b[0m - Generate client files as defined in the config.\n- \x1b[36msrpc init\x1b[0m - Create config file.");
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initConfig(): Promise<void>;
|
package/dist/bin/init.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
export async function initConfig() {
|
|
4
|
+
let source = "./src/api/*";
|
|
5
|
+
let compiledFolder = "./dist/api";
|
|
6
|
+
let outputFolder = "../client/src/api";
|
|
7
|
+
if (fs.existsSync("./seam-rpc.config.json")) {
|
|
8
|
+
const rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
});
|
|
12
|
+
rl.question("Config file already exists. Do you want to overwrite it? [Y/n] ", answer => {
|
|
13
|
+
if (answer && answer.toLowerCase() != "y" && answer.toLowerCase() != "yes") {
|
|
14
|
+
console.log("Operation canceled.");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
rl.close();
|
|
18
|
+
createConfig();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
createConfig();
|
|
23
|
+
}
|
|
24
|
+
function createConfig() {
|
|
25
|
+
const config = { source, compiledFolder, outputFolder };
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync("./seam-rpc.config.json", JSON.stringify(config, null, 4), "utf-8");
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
console.log("\x1b[31mFailed to create config file ./seam-rpc.config.json\x1b[0m\n" + e);
|
|
31
|
+
}
|
|
32
|
+
console.log(`\x1b[32mSuccessfully created config file ./seam-rpc.config.json\x1b[0m`);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,31 +1,77 @@
|
|
|
1
|
-
import { SeamFile, ISeamFile } from "@seam-rpc/core";
|
|
2
1
|
import EventEmitter from "events";
|
|
3
|
-
import { Express, NextFunction, Request, RequestHandler, Response } from "express";
|
|
4
|
-
|
|
2
|
+
import express, { Express, NextFunction, Request, RequestHandler, Response } from "express";
|
|
3
|
+
import * as z from "zod";
|
|
5
4
|
export interface RouterDefinition {
|
|
6
|
-
[
|
|
5
|
+
[procName: string]: (...args: any[]) => Promise<any>;
|
|
6
|
+
}
|
|
7
|
+
export interface SeamContext {
|
|
8
|
+
request: Request;
|
|
9
|
+
response: Response;
|
|
10
|
+
next: NextFunction;
|
|
7
11
|
}
|
|
8
|
-
export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): Promise<SeamSpace>;
|
|
9
12
|
export interface SeamErrorContext {
|
|
10
13
|
routerPath: string;
|
|
11
|
-
|
|
14
|
+
procedureName: string;
|
|
15
|
+
input?: Record<string, unknown> | null;
|
|
16
|
+
validatedInput?: Record<string, unknown> | null;
|
|
17
|
+
output?: unknown;
|
|
18
|
+
validatedOutput?: unknown;
|
|
12
19
|
request: Request;
|
|
13
20
|
response: Response;
|
|
14
21
|
next: NextFunction;
|
|
15
22
|
}
|
|
16
|
-
export interface
|
|
23
|
+
export interface SeamEvents {
|
|
17
24
|
apiError: [error: unknown, context: SeamErrorContext];
|
|
18
25
|
internalError: [error: unknown, context: SeamErrorContext];
|
|
26
|
+
inputValidationError: [error: unknown, context: SeamErrorContext];
|
|
27
|
+
outputValidationError: [error: unknown, context: SeamErrorContext];
|
|
19
28
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
type ProcedureHandler<Input extends ProcedureInput, Output extends z.ZodType> = (options: ProcedureOptions<Input>) => z.infer<Output> | Promise<z.infer<Output>>;
|
|
30
|
+
type ProcedureInput = Record<string, z.ZodType>;
|
|
31
|
+
type ProcedureOutput = z.ZodType;
|
|
32
|
+
type ProcedureInputData<T extends ProcedureInput> = {
|
|
33
|
+
[K in keyof T]: z.infer<T[K]>;
|
|
34
|
+
};
|
|
35
|
+
interface ProcedureOptions<T extends ProcedureInput> {
|
|
36
|
+
input: Simplify<ProcedureInputData<T>>;
|
|
37
|
+
ctx: SeamContext;
|
|
26
38
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
type Simplify<T> = T extends File ? File : T extends object ? {
|
|
40
|
+
[K in keyof T]: Simplify<T[K]>;
|
|
41
|
+
} : T;
|
|
42
|
+
interface SeamProcedure<Input extends ProcedureInput, Output extends ProcedureOutput> {
|
|
43
|
+
input?: Input;
|
|
44
|
+
output?: Output;
|
|
45
|
+
handler?: ProcedureHandler<Input, Output>;
|
|
46
|
+
}
|
|
47
|
+
export type ProcedureBuilder<Input extends ProcedureInput, Output extends ProcedureOutput> = {
|
|
48
|
+
_def: SeamProcedure<Input, Output>;
|
|
49
|
+
input: <T extends ProcedureInput>(schema: T) => ProcedureBuilder<T, Output>;
|
|
50
|
+
output: <T extends ProcedureOutput>(schema: T) => ProcedureBuilder<Input, T>;
|
|
51
|
+
handler: (handler: ProcedureHandler<Input, Output>) => ProcedureBuilder<Input, Output>;
|
|
52
|
+
};
|
|
53
|
+
export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): Promise<SeamSpace>;
|
|
54
|
+
export declare function seamProcedure(): ProcedureBuilder<ProcedureInput, ProcedureOutput>;
|
|
55
|
+
export declare class SeamRouter {
|
|
56
|
+
private seamSpace;
|
|
57
|
+
private path;
|
|
58
|
+
private router;
|
|
59
|
+
private procedures;
|
|
60
|
+
constructor(seamSpace: SeamSpace, path: string);
|
|
61
|
+
private runMiddleware;
|
|
62
|
+
private validateInput;
|
|
63
|
+
private validateOutput;
|
|
64
|
+
private validateData;
|
|
65
|
+
addProcedures(procedures: Record<string, ProcedureBuilder<any, any>>): void;
|
|
66
|
+
}
|
|
67
|
+
export declare class SeamSpace extends EventEmitter<SeamEvents> {
|
|
68
|
+
private _app;
|
|
69
|
+
private _fileHandler;
|
|
70
|
+
private _jsonParser;
|
|
71
|
+
constructor(_app: Express, _fileHandler: RequestHandler);
|
|
72
|
+
createRouter(path: string): SeamRouter;
|
|
73
|
+
get app(): express.Express;
|
|
74
|
+
get jsonParser(): import("connect").NextHandleFunction;
|
|
75
|
+
get fileHandler(): express.RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
31
76
|
}
|
|
77
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractFiles, injectFiles } from "@seam-rpc/core";
|
|
2
2
|
import EventEmitter from "events";
|
|
3
3
|
import express, { Router } from "express";
|
|
4
4
|
import FormData from "form-data";
|
|
5
|
-
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
;
|
|
6
7
|
;
|
|
7
8
|
export async function createSeamSpace(app, fileHandler) {
|
|
8
9
|
if (!fileHandler) {
|
|
@@ -18,93 +19,212 @@ export async function createSeamSpace(app, fileHandler) {
|
|
|
18
19
|
}
|
|
19
20
|
return new SeamSpace(app, fileHandler);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
export function seamProcedure() {
|
|
23
|
+
return createProcedureBuilder();
|
|
24
|
+
}
|
|
25
|
+
function createProcedureBuilder(definition = {}) {
|
|
26
|
+
return {
|
|
27
|
+
_def: definition,
|
|
28
|
+
input: schema => createProcedureBuilder({
|
|
29
|
+
...definition,
|
|
30
|
+
input: schema,
|
|
31
|
+
}),
|
|
32
|
+
output: schema => createProcedureBuilder({
|
|
33
|
+
...definition,
|
|
34
|
+
output: schema,
|
|
35
|
+
}),
|
|
36
|
+
handler: handler => createProcedureBuilder({
|
|
37
|
+
...definition,
|
|
38
|
+
handler
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export class SeamRouter {
|
|
43
|
+
constructor(seamSpace, path) {
|
|
44
|
+
this.seamSpace = seamSpace;
|
|
45
|
+
this.path = path;
|
|
46
|
+
this.procedures = {};
|
|
47
|
+
this.router = Router();
|
|
48
|
+
this.router.post("/:procName", async (req, res, next) => {
|
|
49
|
+
const procedure = this.procedures[req.params.procName];
|
|
50
|
+
if (!procedure || !procedure.handler)
|
|
33
51
|
return res.sendStatus(404);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await runMiddleware(
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
return res.status(415).send("Unsupported content type");
|
|
44
|
-
}
|
|
45
|
-
let args;
|
|
46
|
-
if (contentType.startsWith("application/json")) {
|
|
47
|
-
args = req.body;
|
|
52
|
+
let input;
|
|
53
|
+
let validatedInput = undefined;
|
|
54
|
+
let output;
|
|
55
|
+
let validatedOutput;
|
|
56
|
+
// Middleware
|
|
57
|
+
try {
|
|
58
|
+
input = await this.runMiddleware(req, res);
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
args = JSON.parse(req.body.json);
|
|
52
|
-
const paths = JSON.parse(req.body.paths);
|
|
53
|
-
const files = (req.files ?? []).map((file, index) => ({
|
|
54
|
-
path: paths[index],
|
|
55
|
-
file: new SeamFile(file.buffer, file.originalname, file.mimetype),
|
|
56
|
-
}));
|
|
57
|
-
injectFiles(args, files);
|
|
60
|
+
catch (err) {
|
|
61
|
+
return res.status(415).send(String(err));
|
|
58
62
|
}
|
|
59
|
-
|
|
63
|
+
// Validate input
|
|
60
64
|
try {
|
|
61
|
-
|
|
65
|
+
validatedInput = this.validateInput(input, procedure.input);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
seamSpace.emit("inputValidationError", err, {
|
|
69
|
+
routerPath: path,
|
|
70
|
+
procedureName: req.params.procName,
|
|
71
|
+
input,
|
|
72
|
+
validatedInput,
|
|
73
|
+
output,
|
|
74
|
+
validatedOutput,
|
|
62
75
|
request: req,
|
|
63
76
|
response: res,
|
|
64
|
-
next
|
|
65
|
-
};
|
|
66
|
-
|
|
77
|
+
next,
|
|
78
|
+
});
|
|
79
|
+
res.sendStatus(400);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Call procedure
|
|
83
|
+
const ctx = {
|
|
84
|
+
request: req,
|
|
85
|
+
response: res,
|
|
86
|
+
next
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
output = await procedure.handler({ input: validatedInput, ctx });
|
|
67
90
|
}
|
|
68
91
|
catch (error) {
|
|
69
|
-
|
|
92
|
+
seamSpace.emit("apiError", error, {
|
|
70
93
|
routerPath: path,
|
|
71
|
-
|
|
94
|
+
procedureName: req.params.procName,
|
|
95
|
+
input,
|
|
96
|
+
validatedInput,
|
|
97
|
+
output,
|
|
98
|
+
validatedOutput,
|
|
72
99
|
request: req,
|
|
73
100
|
response: res,
|
|
74
|
-
next
|
|
101
|
+
next,
|
|
75
102
|
});
|
|
76
103
|
res.status(400).send({ error: String(error) });
|
|
77
104
|
return;
|
|
78
105
|
}
|
|
106
|
+
// Validate output
|
|
107
|
+
try {
|
|
108
|
+
validatedOutput = this.validateOutput(output, procedure.output);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
seamSpace.emit("outputValidationError", err, {
|
|
112
|
+
routerPath: path,
|
|
113
|
+
procedureName: req.params.procName,
|
|
114
|
+
input,
|
|
115
|
+
validatedInput,
|
|
116
|
+
output,
|
|
117
|
+
validatedOutput,
|
|
118
|
+
request: req,
|
|
119
|
+
response: res,
|
|
120
|
+
next,
|
|
121
|
+
});
|
|
122
|
+
res.sendStatus(500);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
79
125
|
try {
|
|
80
|
-
const { json, files, paths } = extractFiles({ result });
|
|
126
|
+
const { json, files, paths } = extractFiles({ result: validatedOutput });
|
|
127
|
+
// Does not include file(s)
|
|
81
128
|
if (files.length === 0) {
|
|
82
129
|
res.json(json);
|
|
83
130
|
return;
|
|
84
131
|
}
|
|
132
|
+
// Includes file(s)
|
|
85
133
|
const form = new FormData();
|
|
86
134
|
form.append("json", JSON.stringify(json));
|
|
87
135
|
form.append("paths", JSON.stringify(paths));
|
|
88
|
-
files.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
136
|
+
for (let i = 0; i < files.length; i++) {
|
|
137
|
+
const file = files[i];
|
|
138
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
139
|
+
form.append(`file-${i}`, buffer, {
|
|
140
|
+
filename: file.name || `file-${i}`,
|
|
141
|
+
contentType: file.type || "application/octet-stream",
|
|
92
142
|
});
|
|
93
|
-
}
|
|
143
|
+
}
|
|
94
144
|
res.writeHead(200, form.getHeaders());
|
|
95
145
|
form.pipe(res);
|
|
96
146
|
}
|
|
97
147
|
catch (error) {
|
|
98
|
-
|
|
148
|
+
seamSpace.emit("internalError", error, {
|
|
99
149
|
routerPath: path,
|
|
100
|
-
|
|
150
|
+
procedureName: req.params.procName,
|
|
151
|
+
input,
|
|
152
|
+
validatedInput,
|
|
153
|
+
output,
|
|
154
|
+
validatedOutput,
|
|
101
155
|
request: req,
|
|
102
156
|
response: res,
|
|
103
157
|
next: next,
|
|
104
158
|
});
|
|
105
|
-
|
|
159
|
+
console.log("INTERNAL ERROR", error);
|
|
160
|
+
res.sendStatus(500); //.send({ error: String(error) });
|
|
106
161
|
}
|
|
107
162
|
});
|
|
108
|
-
|
|
163
|
+
seamSpace.app.use(path, this.router);
|
|
164
|
+
}
|
|
165
|
+
async runMiddleware(req, res) {
|
|
166
|
+
const contentType = req.headers["content-type"] || "";
|
|
167
|
+
const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
|
|
168
|
+
if (contentType.startsWith("application/json")) {
|
|
169
|
+
await runMiddleware(this.seamSpace.jsonParser);
|
|
170
|
+
}
|
|
171
|
+
else if (contentType.startsWith("multipart/form-data")) {
|
|
172
|
+
await runMiddleware(this.seamSpace.fileHandler);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
throw new Error("Unsupported content type.");
|
|
176
|
+
}
|
|
177
|
+
if (contentType.startsWith("application/json"))
|
|
178
|
+
return req.body;
|
|
179
|
+
// multipart/form-data (already checked before)
|
|
180
|
+
let input = JSON.parse(req.body.json);
|
|
181
|
+
const paths = JSON.parse(req.body.paths);
|
|
182
|
+
const files = (req.files ?? []).map((file, index) => ({
|
|
183
|
+
path: paths[index],
|
|
184
|
+
file: new File([file.buffer], file.originalname, { type: file.mimetype }),
|
|
185
|
+
}));
|
|
186
|
+
injectFiles(input, files);
|
|
187
|
+
return input;
|
|
188
|
+
}
|
|
189
|
+
validateInput(input, inputSchema) {
|
|
190
|
+
return this.validateData(input, z.object(inputSchema));
|
|
191
|
+
}
|
|
192
|
+
validateOutput(output, procOutput) {
|
|
193
|
+
return this.validateData(output, procOutput);
|
|
194
|
+
}
|
|
195
|
+
validateData(data, schema) {
|
|
196
|
+
if (!schema) {
|
|
197
|
+
if (data)
|
|
198
|
+
throw new Error("Received data, but no data expected.");
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!data)
|
|
202
|
+
throw new Error("No data was received, but data was expected.");
|
|
203
|
+
return schema.parse(data);
|
|
204
|
+
}
|
|
205
|
+
addProcedures(procedures) {
|
|
206
|
+
for (const proc in procedures) {
|
|
207
|
+
this.procedures[proc] = procedures[proc]._def;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export class SeamSpace extends EventEmitter {
|
|
212
|
+
constructor(_app, _fileHandler) {
|
|
213
|
+
super();
|
|
214
|
+
this._app = _app;
|
|
215
|
+
this._fileHandler = _fileHandler;
|
|
216
|
+
this._jsonParser = express.json();
|
|
217
|
+
}
|
|
218
|
+
createRouter(path) {
|
|
219
|
+
return new SeamRouter(this, path);
|
|
220
|
+
}
|
|
221
|
+
get app() {
|
|
222
|
+
return this._app;
|
|
223
|
+
}
|
|
224
|
+
get jsonParser() {
|
|
225
|
+
return this._jsonParser;
|
|
226
|
+
}
|
|
227
|
+
get fileHandler() {
|
|
228
|
+
return this._fileHandler;
|
|
109
229
|
}
|
|
110
230
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seam-rpc/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"srpc": "dist/bin/index.js"
|
|
8
8
|
},
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
10
|
"files": [
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
"multer": "^2.0.2"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@seam-rpc/core": "
|
|
37
|
+
"@seam-rpc/core": "*",
|
|
38
38
|
"fast-glob": "^3.3.3",
|
|
39
39
|
"form-data": "^4.0.5",
|
|
40
|
-
"typescript": "^5.9.3"
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"zod": "^4.3.6",
|
|
42
|
+
"zod-to-ts": "^2.0.0"
|
|
41
43
|
}
|
|
42
44
|
}
|