@seam-rpc/server 5.0.2 → 5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seam-rpc/server",
3
- "version": "5.0.2",
3
+ "version": "5.1.0",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -1,12 +0,0 @@
1
- export declare function generateClient(): Promise<void>;
2
- interface GenerateOptions {
3
- tsPath: string;
4
- jsPath: string;
5
- outputPath: string;
6
- }
7
- export declare function getProcedureMetadata(filePath: string): Record<string, {
8
- comment: string;
9
- returnType?: string;
10
- }>;
11
- export declare function generateClientFile({ tsPath, jsPath, outputPath, }: GenerateOptions): Promise<string>;
12
- export {};
@@ -1,208 +0,0 @@
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
- gen(sourceFiles[i], outputPath);
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));
26
- }
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")}`);
28
- // } catch (err: any) {
29
- // console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
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
- export function getProcedureMetadata(filePath) {
44
- const sourceText = fs.readFileSync(filePath, "utf8");
45
- const program = ts.createProgram([filePath], {
46
- target: ts.ScriptTarget.Latest,
47
- module: ts.ModuleKind.CommonJS,
48
- strict: true,
49
- });
50
- const checker = program.getTypeChecker();
51
- const sourceFile = program.getSourceFile(filePath);
52
- if (!sourceFile)
53
- return {};
54
- const metadata = {};
55
- function getHandlerReturnType(node) {
56
- let current = node;
57
- while (current) {
58
- if (ts.isCallExpression(current) &&
59
- ts.isPropertyAccessExpression(current.expression)) {
60
- const prop = current.expression;
61
- if (prop.name.text === "handler") {
62
- const handlerFn = current.arguments[0];
63
- if (handlerFn &&
64
- (ts.isArrowFunction(handlerFn) ||
65
- ts.isFunctionExpression(handlerFn))) {
66
- const signature = checker.getSignatureFromDeclaration(handlerFn);
67
- if (!signature)
68
- return;
69
- const returnType = checker.getReturnTypeOfSignature(signature);
70
- return checker.typeToString(returnType, undefined, ts.TypeFormatFlags.NoTruncation);
71
- }
72
- }
73
- current = prop.expression;
74
- continue;
75
- }
76
- break;
77
- }
78
- return;
79
- }
80
- function visit(node) {
81
- if (ts.isVariableStatement(node)) {
82
- const comment = getFullComment(node, sourceText);
83
- for (const decl of node.declarationList.declarations) {
84
- if (!ts.isIdentifier(decl.name))
85
- continue;
86
- const name = decl.name.text;
87
- let returnType;
88
- if (decl.initializer) {
89
- returnType = getHandlerReturnType(decl.initializer);
90
- }
91
- metadata[name] = {
92
- comment,
93
- returnType,
94
- };
95
- }
96
- }
97
- ts.forEachChild(node, visit);
98
- }
99
- visit(sourceFile);
100
- return metadata;
101
- }
102
- function getFullComment(node, sourceText) {
103
- const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
104
- if (!ranges)
105
- return "";
106
- for (const range of ranges) {
107
- const comment = sourceText.slice(range.pos, range.end);
108
- // Only keep JSDoc-style comments
109
- if (comment.startsWith("/**")) {
110
- return comment;
111
- }
112
- }
113
- return "";
114
- }
115
- function convert(schema) {
116
- const store = createAuxiliaryTypeStore();
117
- const { node } = zodToTs(schema, { auxiliaryTypeStore: store });
118
- return printNode(node);
119
- }
120
- function gen(src, outputPath) {
121
- const options = {
122
- declaration: true,
123
- emitDeclarationOnly: true,
124
- outDir: outputPath,
125
- target: ts.ScriptTarget.ES2020,
126
- module: ts.ModuleKind.NodeNext,
127
- strict: true,
128
- };
129
- const host = ts.createCompilerHost(options);
130
- const file = path.resolve(process.cwd(), src);
131
- const program = ts.createProgram([file], options, host);
132
- console.log(file, outputPath);
133
- const emitResult = program.emit();
134
- const diagnostics = ts.getPreEmitDiagnostics(program)
135
- .concat(emitResult.diagnostics);
136
- for (const d of diagnostics) {
137
- console.log(ts.flattenDiagnosticMessageText(d.messageText, "\n"));
138
- }
139
- }
140
- export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
141
- const tsFile = path.resolve(process.cwd(), tsPath);
142
- const tsFileName = path.basename(tsFile).slice(0, -path.extname(tsFile).length);
143
- const routerName = tsFileName.slice(0, tsFileName.indexOf("."));
144
- const jsFile = path.resolve(process.cwd(), path.join(jsPath, tsFileName + ".js"));
145
- if (!existsSync(tsFile)) {
146
- throw new Error(`TS file not found: ${tsFile}`);
147
- }
148
- if (!existsSync(jsFile)) {
149
- throw new Error(`JS file not found: ${jsFile}`);
150
- }
151
- const mod = await import("file://" + jsFile);
152
- const procedures = mod.default;
153
- // const routerName = path.basename(jsFile, path.extname(jsFile));
154
- const procMetadata = getProcedureMetadata(tsFile);
155
- let output = `/**
156
- * +===================================+
157
- * | File auto-generated by SeamRPC. |
158
- * | --- DO NOT EDIT --- |
159
- * +===================================+
160
- */
161
-
162
- import { callApi, RpcError } from "@seam-rpc/client";
163
-
164
- `;
165
- const functions = [];
166
- for (const [name, proc] of Object.entries(procedures)) {
167
- // Input
168
- let inputType = "void";
169
- let hasInput = false;
170
- if (proc._def.input) {
171
- try {
172
- const fields = [];
173
- for (const [key, schema] of Object.entries(proc._def.input)) {
174
- const isOptional = schema.isOptional?.() ?? false;
175
- const tsType = convert(schema);
176
- fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
177
- }
178
- if (fields.length > 0) {
179
- inputType = `{ ${fields.join("; ")} }`;
180
- hasInput = true;
181
- }
182
- }
183
- catch (e) {
184
- console.error(`Failed to process input of procedure "${name}".`);
185
- process.exit(1);
186
- }
187
- }
188
- // const dataType = proc._def.output ? convert(proc._def.output) : "void";
189
- // const errors = Object.entries(proc._def.errors ?? {});
190
- // const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1] as any)}>`).join(" | ");
191
- // const fullReturnType = `Promise<Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>>`;
192
- const fullReturnType = procMetadata[name].returnType;
193
- const params = hasInput ? `input: ${inputType}` : "";
194
- const args = [`"${routerName}"`, `"${name}"`];
195
- if (hasInput)
196
- args.push("input");
197
- const call = `callApi(${args.join(", ")})`;
198
- const comment = procMetadata[name].comment ? procMetadata[name].comment + "\n" : "";
199
- const func = `${comment}export function ${name}(${params}): ${fullReturnType} {
200
- return ${call};
201
- }`;
202
- functions.push(func);
203
- }
204
- output += functions.join("\n\n");
205
- const outFile = path.join(outputPath, `${routerName}.ts`);
206
- writeFileSync(outFile, output, "utf-8");
207
- return outFile;
208
- }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- export interface SeamConfig {
3
- source: string;
4
- compiledFolder: string;
5
- outputFolder: string;
6
- }
package/dist/bin/index.js DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env node
2
- import { generateClient } from "./generate.js";
3
- import { initConfig } from "./init.js";
4
- main();
5
- function main() {
6
- const args = process.argv;
7
- switch (args[2]) {
8
- case "generate":
9
- generateClient();
10
- break;
11
- case "init":
12
- initConfig();
13
- break;
14
- default:
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
- }
17
- }
@@ -1 +0,0 @@
1
- export declare function initConfig(): Promise<void>;
package/dist/bin/init.js DELETED
@@ -1,34 +0,0 @@
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
- }
@@ -1,84 +0,0 @@
1
- import { Result, ApiError } from "@seam-rpc/core";
2
- import EventEmitter from "events";
3
- import express, { Express, NextFunction, Request, RequestHandler, Response } from "express";
4
- import * as z from "zod";
5
- export { ApiError as RpcError };
6
- export type { Result };
7
- export interface RouterDefinition {
8
- [procName: string]: (...args: any[]) => Promise<any>;
9
- }
10
- type Simplify<T> = T extends File ? File : T extends object ? {
11
- [K in keyof T]: Simplify<T[K]>;
12
- } : T;
13
- export interface SeamContext {
14
- request: Request;
15
- response: Response;
16
- next: NextFunction;
17
- }
18
- export interface SeamErrorContext {
19
- routerPath: string;
20
- procedureName: string;
21
- input?: Record<string, unknown> | null;
22
- validatedInput?: Record<string, unknown> | null;
23
- output?: unknown;
24
- validatedOutput?: unknown;
25
- request: Request;
26
- response: Response;
27
- next: NextFunction;
28
- }
29
- export interface SeamEvents {
30
- apiError: [error: unknown, context: SeamErrorContext];
31
- internalError: [error: unknown, context: SeamErrorContext];
32
- inputValidationError: [error: unknown, context: SeamErrorContext];
33
- outputValidationError: [error: unknown, context: SeamErrorContext];
34
- }
35
- type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => Result<z.infer<Output>, ApiError> | Promise<Result<z.infer<Output>, ApiError>>;
36
- interface ProcedureOptions<Input extends ProcedureInput, Errors extends ProcedureErrors> {
37
- input: Simplify<ProcedureInputData<Input>>;
38
- ctx: SeamContext;
39
- error: RpcErrorFactory<Errors>;
40
- }
41
- type RpcErrorFactory<Errors extends ProcedureErrors> = <Code extends keyof Errors>(code: Code, ...args: z.infer<Errors[Code]> extends undefined ? [] : [data: z.infer<Errors[Code]>]) => void;
42
- type ProcedureInput = Record<string, z.ZodType>;
43
- type ProcedureOutput = z.ZodType;
44
- type ProcedureErrors = Record<string, z.ZodType>;
45
- type ProcedureBuilderList = Record<string, ProcedureBuilder<any, any, any>>;
46
- type ProcedureInputData<T extends ProcedureInput> = {
47
- [K in keyof T]: z.infer<T[K]>;
48
- };
49
- interface SeamProcedure<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> {
50
- input?: Input;
51
- output?: Output;
52
- errors?: Errors;
53
- handler?: ProcedureHandler<Input, Output, Errors>;
54
- }
55
- export type ProcedureBuilder<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = {
56
- _def: SeamProcedure<Input, Output, Errors>;
57
- input: <T extends ProcedureInput>(schema: T) => ProcedureBuilder<T, Output, Errors>;
58
- output: <T extends ProcedureOutput>(schema: T) => ProcedureBuilder<Input, T, Errors>;
59
- errors: <T extends ProcedureErrors>(errors: T) => ProcedureBuilder<Input, Output, T>;
60
- handler: (handler: ProcedureHandler<Input, Output, Errors>) => ProcedureBuilder<Input, Output, Errors>;
61
- };
62
- export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): Promise<SeamSpace>;
63
- export declare function seamRouter(path: string, procedures: ProcedureBuilderList): SeamRouter;
64
- export declare function seamProcedure(): ProcedureBuilder<ProcedureInput, ProcedureOutput, ProcedureErrors>;
65
- export declare class SeamRouter extends EventEmitter<SeamEvents> {
66
- private _procedures;
67
- private _router;
68
- constructor(path: string, procedures: ProcedureBuilderList);
69
- get router(): express.Router;
70
- private runMiddleware;
71
- private validateInput;
72
- private validateOutput;
73
- private validateData;
74
- }
75
- export declare class SeamSpace extends EventEmitter<SeamEvents> {
76
- private _app;
77
- private _fileHandler;
78
- private _jsonParser;
79
- constructor(_app: Express, _fileHandler: RequestHandler);
80
- createRouter(path: string, procedures?: ProcedureBuilderList): SeamRouter;
81
- get app(): express.Express;
82
- get jsonParser(): import("connect").NextHandleFunction;
83
- get fileHandler(): express.RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
84
- }
@@ -1,254 +0,0 @@
1
- import { extractFiles, injectFiles, ApiError } from "@seam-rpc/core";
2
- import EventEmitter from "events";
3
- import express, { Router } from "express";
4
- import FormData from "form-data";
5
- import * as z from "zod";
6
- export { ApiError as RpcError };
7
- ;
8
- ;
9
- const errorHandler = (code, ...args) => {
10
- return new ApiError(code, args[0]);
11
- };
12
- export async function createSeamSpace(app, fileHandler) {
13
- if (!fileHandler) {
14
- let multer;
15
- try {
16
- multer = (await import("multer")).default;
17
- }
18
- catch {
19
- throw new Error("Multer is required as default file handler. Install it or provide a custom fileHandler.");
20
- }
21
- const upload = multer();
22
- fileHandler = upload.any();
23
- }
24
- return new SeamSpace(app, fileHandler);
25
- }
26
- export function seamRouter(path, procedures) {
27
- return new SeamRouter(path, procedures);
28
- }
29
- export function seamProcedure() {
30
- return createProcedureBuilder();
31
- }
32
- function createProcedureBuilder(definition = {}) {
33
- return {
34
- _def: definition,
35
- input: schema => createProcedureBuilder({
36
- ...definition,
37
- input: schema,
38
- }),
39
- output: schema => createProcedureBuilder({
40
- ...definition,
41
- output: schema,
42
- }),
43
- errors: errors => createProcedureBuilder({
44
- ...definition,
45
- errors
46
- }),
47
- handler: handler => createProcedureBuilder({
48
- ...definition,
49
- handler
50
- }),
51
- };
52
- }
53
- export class SeamRouter extends EventEmitter {
54
- constructor(path, procedures) {
55
- super();
56
- this._procedures = {};
57
- this._router = Router();
58
- for (const proc in procedures) {
59
- this._procedures[proc] = procedures[proc]._def;
60
- }
61
- // seamSpace.app.use(path, this._router);
62
- this._router.post("/:procName", async (req, res, next) => {
63
- const procedure = this._procedures[req.params.procName];
64
- if (!procedure || !procedure.handler)
65
- return res.sendStatus(404);
66
- let input;
67
- let validatedInput = undefined;
68
- let output = undefined;
69
- let validatedOutput;
70
- // Middleware
71
- try {
72
- input = await this.runMiddleware(req, res);
73
- }
74
- catch (err) {
75
- console.error(err);
76
- return res.status(415).send(String(err));
77
- }
78
- // Validate input
79
- try {
80
- validatedInput = this.validateInput(input, procedure.input);
81
- }
82
- catch (err) {
83
- this.emit("inputValidationError", err, {
84
- routerPath: path,
85
- procedureName: req.params.procName,
86
- input,
87
- validatedInput,
88
- output,
89
- validatedOutput,
90
- request: req,
91
- response: res,
92
- next,
93
- });
94
- res.sendStatus(400);
95
- return;
96
- }
97
- // Call procedure
98
- const ctx = {
99
- request: req,
100
- response: res,
101
- next,
102
- };
103
- function toResError(error) {
104
- if (error instanceof ApiError)
105
- return { isApiError: true, error: error.toJSON() };
106
- else
107
- return { isApiError: false };
108
- }
109
- try {
110
- output = await procedure.handler({ input: validatedInput, ctx, error: errorHandler });
111
- }
112
- catch (error) {
113
- this.emit("apiError", error, {
114
- routerPath: path,
115
- procedureName: req.params.procName,
116
- input,
117
- validatedInput,
118
- output,
119
- validatedOutput,
120
- request: req,
121
- response: res,
122
- next: () => { res.status(400).json(toResError(error)); return next(); },
123
- });
124
- return;
125
- }
126
- if (!output.ok) {
127
- this.emit("apiError", output.error, {
128
- routerPath: path,
129
- procedureName: req.params.procName,
130
- input,
131
- validatedInput,
132
- output,
133
- validatedOutput,
134
- request: req,
135
- response: res,
136
- next: () => { res.status(400).json(toResError(output.error)); return next(); },
137
- });
138
- return;
139
- }
140
- // Validate output
141
- try {
142
- validatedOutput = this.validateOutput(output, z.object({ ok: z.literal(true), data: procedure.output }).or(z.object({ ok: z.literal(false), error: z.any() })));
143
- }
144
- catch (err) {
145
- this.emit("outputValidationError", err, {
146
- routerPath: path,
147
- procedureName: req.params.procName,
148
- input,
149
- validatedInput,
150
- output,
151
- validatedOutput,
152
- request: req,
153
- response: res,
154
- next,
155
- });
156
- res.sendStatus(500);
157
- return;
158
- }
159
- try {
160
- const { json, files, paths } = extractFiles({ result: validatedOutput });
161
- // Does not include file(s)
162
- if (files.length === 0) {
163
- res.json(json);
164
- return;
165
- }
166
- // Includes file(s)
167
- const form = new FormData();
168
- form.append("json", JSON.stringify(json));
169
- form.append("paths", JSON.stringify(paths));
170
- for (let i = 0; i < files.length; i++) {
171
- const file = files[i];
172
- const buffer = Buffer.from(await file.arrayBuffer());
173
- form.append(`file-${i}`, buffer, {
174
- filename: file.name || `file-${i}`,
175
- contentType: file.type || "application/octet-stream",
176
- });
177
- }
178
- res.writeHead(200, form.getHeaders());
179
- form.pipe(res);
180
- }
181
- catch (error) {
182
- this.emit("internalError", error, {
183
- routerPath: path,
184
- procedureName: req.params.procName,
185
- input,
186
- validatedInput,
187
- output,
188
- validatedOutput,
189
- request: req,
190
- response: res,
191
- next: next,
192
- });
193
- res.sendStatus(500); //.send({ error: String(error) });
194
- }
195
- });
196
- }
197
- get router() {
198
- return this._router;
199
- }
200
- async runMiddleware(req, res) {
201
- const contentType = req.headers["content-type"] || "";
202
- const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
203
- // if (contentType.startsWith("application/json")) {
204
- // await runMiddleware(this.seamSpace.jsonParser);
205
- // } else if (contentType.startsWith("multipart/form-data")) {
206
- // await runMiddleware(this.seamSpace.fileHandler);
207
- // } else {
208
- // throw new Error("Unsupported content type.");
209
- // }
210
- if (contentType.startsWith("application/json"))
211
- return req.body;
212
- // multipart/form-data (already checked before)
213
- let input = JSON.parse(req.body.json);
214
- const paths = JSON.parse(req.body.paths);
215
- const files = (req.files ?? []).map((file, index) => ({
216
- path: paths[index],
217
- file: new File([file.buffer], file.originalname, { type: file.mimetype }),
218
- }));
219
- injectFiles(input, files);
220
- return input;
221
- }
222
- validateInput(input, inputSchema) {
223
- return this.validateData(input, z.object(inputSchema));
224
- }
225
- validateOutput(output, procOutput) {
226
- return this.validateData(output, procOutput);
227
- }
228
- validateData(data, schema) {
229
- if (!schema) {
230
- if (data)
231
- throw new Error("Received data, but no data expected.");
232
- return null;
233
- }
234
- if (!data)
235
- throw new Error("No data was received, but data was expected.");
236
- return schema.parse(data);
237
- }
238
- }
239
- export class SeamSpace extends EventEmitter {
240
- constructor(_app, _fileHandler) {
241
- super();
242
- this._app = _app;
243
- this._fileHandler = _fileHandler;
244
- this._jsonParser = express.json();
245
- }
246
- createRouter(path, procedures = {}) {
247
- const router = new SeamRouter(path, procedures);
248
- // router.addProcedures(procedures);
249
- return router;
250
- }
251
- get app() { return this._app; }
252
- get jsonParser() { return this._jsonParser; }
253
- get fileHandler() { return this._fileHandler; }
254
- }