@seam-rpc/server 4.3.22 → 5.0.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/index.d.ts CHANGED
@@ -1,12 +1,9 @@
1
- import { Result, RpcError } from "@seam-rpc/core";
1
+ import { Result, ApiError, ApiErrorInterface } from "@seam-rpc/core";
2
2
  import EventEmitter from "events";
3
3
  import express, { Express, NextFunction, Request, RequestHandler, Response } from "express";
4
4
  import * as z from "zod";
5
- export { RpcError };
5
+ export { ApiError };
6
6
  export type { Result };
7
- export interface RouterDefinition {
8
- [procName: string]: (...args: any[]) => Promise<any>;
9
- }
10
7
  type Simplify<T> = T extends File ? File : T extends object ? {
11
8
  [K in keyof T]: Simplify<T[K]>;
12
9
  } : T;
@@ -32,53 +29,64 @@ export interface SeamEvents {
32
29
  inputValidationError: [error: unknown, context: SeamErrorContext];
33
30
  outputValidationError: [error: unknown, context: SeamErrorContext];
34
31
  }
35
- type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => Result<z.infer<Output>, RpcError> | Promise<Result<z.infer<Output>, RpcError>>;
36
- interface ProcedureOptions<Input extends ProcedureInput, Errors extends ProcedureErrors> {
32
+ type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput> = (options: ProcedureOptions<Input>) => Result<z.infer<Output>, ApiError> | Promise<Result<z.infer<Output>, ApiError>>;
33
+ interface ProcedureOptions<Input extends ProcedureInput> {
37
34
  input: Simplify<ProcedureInputData<Input>>;
38
35
  ctx: SeamContext;
39
- error: RpcErrorFactory<Errors>;
40
36
  }
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
37
  type ProcedureInput = Record<string, z.ZodType>;
43
38
  type ProcedureOutput = z.ZodType;
44
- type ProcedureErrors = Record<string, z.ZodType>;
45
- type ProcedureBuilderList = Record<string, ProcedureBuilder<any, any, any>>;
39
+ type ProcedureErrors = Record<string, z.ZodType | undefined>;
46
40
  type ProcedureInputData<T extends ProcedureInput> = {
47
41
  [K in keyof T]: z.infer<T[K]>;
48
42
  };
49
- interface SeamProcedure<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> {
43
+ interface SeamProcedure<Input extends ProcedureInput = {}, Output extends ProcedureOutput = z.ZodUndefined, Errors extends ProcedureErrors = {}> {
50
44
  input?: Input;
51
45
  output?: Output;
52
46
  errors?: Errors;
53
- handler?: ProcedureHandler<Input, Output, Errors>;
47
+ handler?: ProcedureHandler<Input, Output>;
54
48
  }
55
- export type ProcedureBuilder<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = {
49
+ type ExtractResultError<T> = Awaited<T> extends Result<any, infer E> ? E : never;
50
+ type ErrorUnionToMap<E> = {
51
+ [K in E extends ApiErrorInterface<any, infer Code> ? Code : never]: Extract<E, ApiErrorInterface<any, K>> extends ApiErrorInterface<infer Map, K> ? Map[K] : never;
52
+ };
53
+ export type ProcedureBuilder<Input extends ProcedureInput = {}, Output extends ProcedureOutput = z.ZodUndefined, Errors extends ProcedureErrors = {}> = {
56
54
  _def: SeamProcedure<Input, Output, Errors>;
57
55
  input: <T extends ProcedureInput>(schema: T) => ProcedureBuilder<T, Output, Errors>;
58
56
  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>;
57
+ handler: <H extends ProcedureHandler<Input, Output>>(handler: H) => ProcedureBuilder<Input, Output, ErrorUnionToMap<ExtractResultError<ReturnType<H>>>>;
61
58
  };
62
59
  export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): Promise<SeamSpace>;
63
- export declare function seamProcedure(): ProcedureBuilder<ProcedureInput, ProcedureOutput, ProcedureErrors>;
64
- export declare class SeamRouter {
65
- private seamSpace;
66
- private procedures;
67
- private router;
68
- constructor(seamSpace: SeamSpace, path: string);
69
- private runMiddleware;
70
- private validateInput;
71
- private validateOutput;
72
- private validateData;
73
- addProcedures(procedures: ProcedureBuilderList): void;
74
- }
60
+ export declare function seamProcedure(): ProcedureBuilder;
61
+ export type SeamRouterBuilder = Record<string, ProcedureBuilder<any, any, any>>;
75
62
  export declare class SeamSpace extends EventEmitter<SeamEvents> {
76
63
  private _app;
77
64
  private _fileHandler;
78
65
  private _jsonParser;
79
66
  constructor(_app: Express, _fileHandler: RequestHandler);
80
- createRouter(path: string, procedures?: ProcedureBuilderList): SeamRouter;
67
+ addRouters<T extends Record<string, SeamRouterBuilder>>(routers: T): RouterToClient<T>;
81
68
  get app(): express.Express;
82
69
  get jsonParser(): import("connect").NextHandleFunction;
83
70
  get fileHandler(): express.RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
84
71
  }
72
+ type ErrorMapToUnion<E extends Record<string, any>> = {
73
+ [K in keyof E]: ApiError<{
74
+ [P in K]: E[K];
75
+ }, K>;
76
+ }[keyof E];
77
+ type ResultFromErrors<Data, Errors extends Record<string, any>> = {
78
+ ok: true;
79
+ data: Data;
80
+ } | (ErrorMapToUnion<Errors> extends infer E ? E extends ApiError<any, any> ? {
81
+ ok: false;
82
+ error: E;
83
+ } : never : never);
84
+ type InferInput<P> = P extends ProcedureBuilder<infer I, any, any> ? {
85
+ [K in keyof I]: I[K] extends z.ZodType ? z.infer<I[K]> : never;
86
+ } : never;
87
+ type InferOutput<P> = P extends ProcedureBuilder<any, infer O, any> ? O extends z.ZodType ? z.infer<O> : never : never;
88
+ type InferErrors<P> = P extends ProcedureBuilder<any, any, infer E> ? E : never;
89
+ type ProcedureToFn<P> = P extends ProcedureBuilder<any, any, any> ? (input: InferInput<P>) => Promise<ResultFromErrors<InferOutput<P>, InferErrors<P>>> : never;
90
+ type RouterToClient<T> = {
91
+ [K in keyof T]: T[K] extends ProcedureBuilder<any, any, any> ? ProcedureToFn<T[K]> : RouterToClient<T[K]>;
92
+ };
package/dist/index.js CHANGED
@@ -1,14 +1,10 @@
1
- import { extractFiles, injectFiles, RpcError } from "@seam-rpc/core";
1
+ import { extractFiles, injectFiles, ApiError } 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
- export { RpcError };
6
+ export { ApiError };
7
7
  ;
8
- ;
9
- const errorHandler = (code, ...args) => {
10
- return new RpcError(code, args[0]);
11
- };
12
8
  export async function createSeamSpace(app, fileHandler) {
13
9
  if (!fileHandler) {
14
10
  let multer;
@@ -37,202 +33,189 @@ function createProcedureBuilder(definition = {}) {
37
33
  ...definition,
38
34
  output: schema,
39
35
  }),
40
- errors: errors => createProcedureBuilder({
41
- ...definition,
42
- errors
43
- }),
44
- handler: handler => createProcedureBuilder({
36
+ handler: (handler) => createProcedureBuilder({
45
37
  ...definition,
46
- handler
38
+ handler,
47
39
  }),
48
40
  };
49
41
  }
50
- export class SeamRouter {
51
- constructor(seamSpace, path) {
52
- this.seamSpace = seamSpace;
53
- this.procedures = {};
54
- this.router = Router();
55
- this.router.post("/:procName", async (req, res, next) => {
56
- const procedure = this.procedures[req.params.procName];
57
- if (!procedure || !procedure.handler)
58
- return res.sendStatus(404);
59
- let input;
60
- let validatedInput = undefined;
61
- let output = undefined;
62
- let validatedOutput;
63
- // Middleware
64
- try {
65
- input = await this.runMiddleware(req, res);
66
- }
67
- catch (err) {
68
- console.error(err);
69
- return res.status(415).send(String(err));
70
- }
71
- // Validate input
72
- try {
73
- validatedInput = this.validateInput(input, procedure.input);
74
- }
75
- catch (err) {
76
- seamSpace.emit("inputValidationError", err, {
77
- routerPath: path,
78
- procedureName: req.params.procName,
79
- input,
80
- validatedInput,
81
- output,
82
- validatedOutput,
83
- request: req,
84
- response: res,
85
- next,
86
- });
87
- res.sendStatus(400);
88
- return;
89
- }
90
- // Call procedure
91
- const ctx = {
42
+ function defineSeamRouter(seamSpace, path, seamRouterBuilder) {
43
+ const router = Router();
44
+ router.post("/:procName", async (req, res, next) => {
45
+ const procedure = seamRouterBuilder[req.params.procName]._def;
46
+ if (!procedure || !procedure.handler)
47
+ return res.sendStatus(404);
48
+ let input;
49
+ let validatedInput = undefined;
50
+ let output = undefined;
51
+ let validatedOutput;
52
+ // Middleware
53
+ try {
54
+ input = await runMiddleware(seamSpace, req, res);
55
+ }
56
+ catch (err) {
57
+ console.error(err);
58
+ return res.status(415).send(String(err));
59
+ }
60
+ // Validate input
61
+ try {
62
+ validatedInput = validateInput(input, procedure.input);
63
+ }
64
+ catch (err) {
65
+ seamSpace.emit("inputValidationError", err, {
66
+ routerPath: path,
67
+ procedureName: req.params.procName,
68
+ input,
69
+ validatedInput,
70
+ output,
71
+ validatedOutput,
92
72
  request: req,
93
73
  response: res,
94
74
  next,
95
- };
96
- function toResError(error) {
97
- if (error instanceof RpcError)
98
- return { rpcError: true, error: error.toJSON() };
99
- else
100
- return { rpcError: false };
101
- }
102
- try {
103
- output = await procedure.handler({ input: validatedInput, ctx, error: errorHandler });
104
- }
105
- catch (error) {
106
- seamSpace.emit("apiError", error, {
107
- routerPath: path,
108
- procedureName: req.params.procName,
109
- input,
110
- validatedInput,
111
- output,
112
- validatedOutput,
113
- request: req,
114
- response: res,
115
- next: () => { res.status(400).json(toResError(error)); return next(); },
116
- });
117
- return;
118
- }
119
- if (!output.ok) {
120
- seamSpace.emit("apiError", output.error, {
121
- routerPath: path,
122
- procedureName: req.params.procName,
123
- input,
124
- validatedInput,
125
- output,
126
- validatedOutput,
127
- request: req,
128
- response: res,
129
- next: () => { res.status(400).json(toResError(output.error)); return next(); },
130
- });
131
- return;
132
- }
133
- // Validate output
134
- try {
135
- validatedOutput = this.validateOutput(output, z.object({ ok: z.literal(true), data: procedure.output }).or(z.object({ ok: z.literal(false), error: z.any() })));
136
- }
137
- catch (err) {
138
- seamSpace.emit("outputValidationError", err, {
139
- routerPath: path,
140
- procedureName: req.params.procName,
141
- input,
142
- validatedInput,
143
- output,
144
- validatedOutput,
145
- request: req,
146
- response: res,
147
- next,
148
- });
149
- res.sendStatus(500);
75
+ });
76
+ res.sendStatus(400);
77
+ return;
78
+ }
79
+ // Call procedure
80
+ const ctx = {
81
+ request: req,
82
+ response: res,
83
+ next,
84
+ };
85
+ function toResError(error) {
86
+ if (error instanceof ApiError)
87
+ return { isApiError: true, error: error.toJSON() };
88
+ else
89
+ return { isApiError: false };
90
+ }
91
+ try {
92
+ output = await procedure.handler({ input: validatedInput, ctx });
93
+ }
94
+ catch (error) {
95
+ seamSpace.emit("apiError", error, {
96
+ routerPath: path,
97
+ procedureName: req.params.procName,
98
+ input,
99
+ validatedInput,
100
+ output,
101
+ validatedOutput,
102
+ request: req,
103
+ response: res,
104
+ next: () => { res.status(400).json(toResError(error)); return next(); },
105
+ });
106
+ return;
107
+ }
108
+ if (!output.ok) {
109
+ seamSpace.emit("apiError", output.error, {
110
+ routerPath: path,
111
+ procedureName: req.params.procName,
112
+ input,
113
+ validatedInput,
114
+ output,
115
+ validatedOutput,
116
+ request: req,
117
+ response: res,
118
+ next: () => { res.status(400).json(toResError(output.error)); return next(); },
119
+ });
120
+ return;
121
+ }
122
+ // Validate output
123
+ try {
124
+ validatedOutput = validateOutput(output, z.object({ ok: z.literal(true), data: procedure.output }).or(z.object({ ok: z.literal(false), error: z.any() })));
125
+ }
126
+ catch (err) {
127
+ seamSpace.emit("outputValidationError", err, {
128
+ routerPath: path,
129
+ procedureName: req.params.procName,
130
+ input,
131
+ validatedInput,
132
+ output,
133
+ validatedOutput,
134
+ request: req,
135
+ response: res,
136
+ next,
137
+ });
138
+ res.sendStatus(500);
139
+ return;
140
+ }
141
+ try {
142
+ const { json, files, paths } = extractFiles({ result: validatedOutput });
143
+ // Does not include file(s)
144
+ if (files.length === 0) {
145
+ res.json(json);
150
146
  return;
151
147
  }
152
- try {
153
- const { json, files, paths } = extractFiles({ result: validatedOutput });
154
- // Does not include file(s)
155
- if (files.length === 0) {
156
- res.json(json);
157
- return;
158
- }
159
- // Includes file(s)
160
- const form = new FormData();
161
- form.append("json", JSON.stringify(json));
162
- form.append("paths", JSON.stringify(paths));
163
- for (let i = 0; i < files.length; i++) {
164
- const file = files[i];
165
- const buffer = Buffer.from(await file.arrayBuffer());
166
- form.append(`file-${i}`, buffer, {
167
- filename: file.name || `file-${i}`,
168
- contentType: file.type || "application/octet-stream",
169
- });
170
- }
171
- res.writeHead(200, form.getHeaders());
172
- form.pipe(res);
173
- }
174
- catch (error) {
175
- seamSpace.emit("internalError", error, {
176
- routerPath: path,
177
- procedureName: req.params.procName,
178
- input,
179
- validatedInput,
180
- output,
181
- validatedOutput,
182
- request: req,
183
- response: res,
184
- next: next,
148
+ // Includes file(s)
149
+ const form = new FormData();
150
+ form.append("json", JSON.stringify(json));
151
+ form.append("paths", JSON.stringify(paths));
152
+ for (let i = 0; i < files.length; i++) {
153
+ const file = files[i];
154
+ const buffer = Buffer.from(await file.arrayBuffer());
155
+ form.append(`file-${i}`, buffer, {
156
+ filename: file.name || `file-${i}`,
157
+ contentType: file.type || "application/octet-stream",
185
158
  });
186
- res.sendStatus(500); //.send({ error: String(error) });
187
159
  }
188
- });
189
- seamSpace.app.use(path, this.router);
190
- }
191
- async runMiddleware(req, res) {
192
- const contentType = req.headers["content-type"] || "";
193
- const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
194
- if (contentType.startsWith("application/json")) {
195
- await runMiddleware(this.seamSpace.jsonParser);
196
- }
197
- else if (contentType.startsWith("multipart/form-data")) {
198
- await runMiddleware(this.seamSpace.fileHandler);
160
+ res.writeHead(200, form.getHeaders());
161
+ form.pipe(res);
199
162
  }
200
- else {
201
- throw new Error("Unsupported content type.");
163
+ catch (error) {
164
+ seamSpace.emit("internalError", error, {
165
+ routerPath: path,
166
+ procedureName: req.params.procName,
167
+ input,
168
+ validatedInput,
169
+ output,
170
+ validatedOutput,
171
+ request: req,
172
+ response: res,
173
+ next: next,
174
+ });
175
+ res.sendStatus(500);
202
176
  }
203
- if (contentType.startsWith("application/json"))
204
- return req.body;
205
- // multipart/form-data (already checked before)
206
- let input = JSON.parse(req.body.json);
207
- const paths = JSON.parse(req.body.paths);
208
- const files = (req.files ?? []).map((file, index) => ({
209
- path: paths[index],
210
- file: new File([file.buffer], file.originalname, { type: file.mimetype }),
211
- }));
212
- injectFiles(input, files);
213
- return input;
214
- }
215
- validateInput(input, inputSchema) {
216
- return this.validateData(input, z.object(inputSchema));
177
+ });
178
+ seamSpace.app.use(`/${path}`, router);
179
+ }
180
+ async function runMiddleware(seamSpace, req, res) {
181
+ const contentType = req.headers["content-type"] || "";
182
+ const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
183
+ if (contentType.startsWith("application/json")) {
184
+ await runMiddleware(seamSpace.jsonParser);
217
185
  }
218
- validateOutput(output, procOutput) {
219
- return this.validateData(output, procOutput);
186
+ else if (contentType.startsWith("multipart/form-data")) {
187
+ await runMiddleware(seamSpace.fileHandler);
220
188
  }
221
- validateData(data, schema) {
222
- if (!schema) {
223
- if (data)
224
- throw new Error("Received data, but no data expected.");
225
- return null;
226
- }
227
- if (!data)
228
- throw new Error("No data was received, but data was expected.");
229
- return schema.parse(data);
189
+ else {
190
+ throw new Error("Unsupported content type.");
230
191
  }
231
- addProcedures(procedures) {
232
- for (const proc in procedures) {
233
- this.procedures[proc] = procedures[proc]._def;
234
- }
192
+ if (contentType.startsWith("application/json"))
193
+ return req.body;
194
+ // multipart/form-data (already checked before)
195
+ let input = JSON.parse(req.body.json);
196
+ const paths = JSON.parse(req.body.paths);
197
+ const files = (req.files ?? []).map((file, index) => ({
198
+ path: paths[index],
199
+ file: new File([file.buffer], file.originalname, { type: file.mimetype }),
200
+ }));
201
+ injectFiles(input, files);
202
+ return input;
203
+ }
204
+ function validateInput(input, inputSchema) {
205
+ return validateData(input, z.object(inputSchema));
206
+ }
207
+ function validateOutput(output, procOutput) {
208
+ return validateData(output, procOutput);
209
+ }
210
+ function validateData(data, schema) {
211
+ if (!schema) {
212
+ if (data)
213
+ throw new Error("Received data, but no data expected.");
214
+ return null;
235
215
  }
216
+ if (!data)
217
+ throw new Error("No data was received, but data was expected.");
218
+ return schema.parse(data);
236
219
  }
237
220
  export class SeamSpace extends EventEmitter {
238
221
  constructor(_app, _fileHandler) {
@@ -241,10 +224,9 @@ export class SeamSpace extends EventEmitter {
241
224
  this._fileHandler = _fileHandler;
242
225
  this._jsonParser = express.json();
243
226
  }
244
- createRouter(path, procedures = {}) {
245
- const router = new SeamRouter(this, path);
246
- router.addProcedures(procedures);
247
- return router;
227
+ addRouters(routers) {
228
+ Object.entries(routers).forEach(e => defineSeamRouter(this, e[0], e[1]));
229
+ return {};
248
230
  }
249
231
  get app() { return this._app; }
250
232
  get jsonParser() { return this._jsonParser; }
package/package.json CHANGED
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "name": "@seam-rpc/server",
3
- "version": "4.3.22",
3
+ "version": "5.0.1",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
- "bin": {
7
- "srpc": "dist/bin/index.js"
8
- },
9
6
  "types": "dist/index.d.ts",
10
7
  "files": [
11
8
  "dist"
@@ -1,193 +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 * 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
- // }
@@ -1,44 +0,0 @@
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
- }
@@ -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,187 +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
- 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
- // } catch (err: any) {
28
- // console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
29
- // process.exit(1);
30
- // }
31
- }
32
- function loadConfig() {
33
- // Check if config exists
34
- if (!fs.existsSync("./seam-rpc.config.json")) {
35
- console.error("\x1b[31mNo config file found.\x1b[0m\n Create a config file with \x1b[36msrpc init\x1b[0m.");
36
- return null;
37
- }
38
- // Load config from config file
39
- return JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
40
- }
41
- // Extracts comments from TS file.
42
- export function getProcedureMetadata(filePath) {
43
- const sourceText = fs.readFileSync(filePath, "utf8");
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, undefined, ts.TypeFormatFlags.NoTruncation);
70
- }
71
- }
72
- current = prop.expression;
73
- continue;
74
- }
75
- break;
76
- }
77
- return;
78
- }
79
- function visit(node) {
80
- if (ts.isVariableStatement(node)) {
81
- const comment = getFullComment(node, sourceText);
82
- for (const decl of node.declarationList.declarations) {
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
- };
94
- }
95
- }
96
- ts.forEachChild(node, visit);
97
- }
98
- visit(sourceFile);
99
- return metadata;
100
- }
101
- function getFullComment(node, sourceText) {
102
- const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
103
- if (!ranges)
104
- return "";
105
- for (const range of ranges) {
106
- const comment = sourceText.slice(range.pos, range.end);
107
- // Only keep JSDoc-style comments
108
- if (comment.startsWith("/**")) {
109
- return comment;
110
- }
111
- }
112
- return "";
113
- }
114
- function convert(schema) {
115
- const store = createAuxiliaryTypeStore();
116
- const { node } = zodToTs(schema, { auxiliaryTypeStore: store });
117
- return printNode(node);
118
- }
119
- export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
120
- const tsFile = path.resolve(process.cwd(), tsPath);
121
- const tsFileName = path.basename(tsFile).slice(0, -path.extname(tsFile).length);
122
- const routerName = tsFileName.slice(0, tsFileName.indexOf("."));
123
- const jsFile = path.resolve(process.cwd(), path.join(jsPath, tsFileName + ".js"));
124
- if (!existsSync(tsFile)) {
125
- throw new Error(`TS file not found: ${tsFile}`);
126
- }
127
- if (!existsSync(jsFile)) {
128
- throw new Error(`JS file not found: ${jsFile}`);
129
- }
130
- const mod = await import("file://" + jsFile);
131
- const procedures = mod.default;
132
- // const routerName = path.basename(jsFile, path.extname(jsFile));
133
- const procMetadata = getProcedureMetadata(tsFile);
134
- let output = `/**
135
- * +===================================+
136
- * | File auto-generated by SeamRPC. |
137
- * | --- DO NOT EDIT --- |
138
- * +===================================+
139
- */
140
-
141
- import { callApi, RpcError } from "@seam-rpc/client";
142
-
143
- `;
144
- const functions = [];
145
- for (const [name, proc] of Object.entries(procedures)) {
146
- // Input
147
- let inputType = "void";
148
- let hasInput = false;
149
- if (proc._def.input) {
150
- try {
151
- const fields = [];
152
- for (const [key, schema] of Object.entries(proc._def.input)) {
153
- const isOptional = schema.isOptional?.() ?? false;
154
- const tsType = convert(schema);
155
- fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
156
- }
157
- if (fields.length > 0) {
158
- inputType = `{ ${fields.join("; ")} }`;
159
- hasInput = true;
160
- }
161
- }
162
- catch (e) {
163
- console.error(`Failed to process input of procedure "${name}".`);
164
- process.exit(1);
165
- }
166
- }
167
- // const dataType = proc._def.output ? convert(proc._def.output) : "void";
168
- // const errors = Object.entries(proc._def.errors ?? {});
169
- // const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1] as any)}>`).join(" | ");
170
- // const fullReturnType = `Promise<Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>>`;
171
- const fullReturnType = procMetadata[name].returnType;
172
- const params = hasInput ? `input: ${inputType}` : "";
173
- const args = [`"${routerName}"`, `"${name}"`];
174
- if (hasInput)
175
- args.push("input");
176
- const call = `callApi(${args.join(", ")})`;
177
- const comment = procMetadata[name].comment ? procMetadata[name].comment + "\n" : "";
178
- const func = `${comment}export function ${name}(${params}): ${fullReturnType} {
179
- return ${call};
180
- }`;
181
- functions.push(func);
182
- }
183
- output += functions.join("\n\n");
184
- const outFile = path.join(outputPath, `${routerName}.ts`);
185
- writeFileSync(outFile, output, "utf-8");
186
- return outFile;
187
- }
@@ -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
- }
package/dist/test.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import * as z from "zod";
2
- export declare function convert(schema: z.ZodTypeAny): string;
package/dist/test.js DELETED
@@ -1,56 +0,0 @@
1
- import { zodToTs, createAuxiliaryTypeStore, printNode } from "zod-to-ts";
2
- import * as z from "zod";
3
- import ts from "typescript";
4
- export function convert(schema) {
5
- const store = createAuxiliaryTypeStore();
6
- const { node } = zodToTs(schema, {
7
- auxiliaryTypeStore: store,
8
- });
9
- const typeAlias = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier("GeneratedType"), undefined, node);
10
- const file = ts.createSourceFile("types.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
11
- const printer = ts.createPrinter({
12
- newLine: ts.NewLineKind.LineFeed,
13
- removeComments: false,
14
- });
15
- return printer.printNode(ts.EmitHint.Unspecified, typeAlias, file);
16
- }
17
- function convert2(schema) {
18
- const store = createAuxiliaryTypeStore();
19
- const { node } = zodToTs(schema, { auxiliaryTypeStore: store });
20
- return printNode(node);
21
- }
22
- console.log(convert2(z.object({
23
- user: z.object({
24
- email: z.string(),
25
- id: z.string(),
26
- createdAt: z.date(),
27
- firstName: z.string(),
28
- lastName: z.string(),
29
- trackingStartDate: z.string(),
30
- trackingWorkItemId: z.string(),
31
- }),
32
- roles: z.object({
33
- id: z.string(),
34
- name: z.string(),
35
- createdAt: z.date(),
36
- permissions: z.number().array(),
37
- projectId: z.string(),
38
- }),
39
- workItems: z.object({
40
- id: z.string(),
41
- createdAt: z.date(),
42
- date: z.date(),
43
- link: z.string(),
44
- title: z.string(),
45
- description: z.string(),
46
- projectId: z.string(),
47
- authorId: z.string(),
48
- durationMethod: z.string(),
49
- startTime: z.number(),
50
- endTime: z.number(),
51
- duration: z.number(),
52
- labels: z.string(),
53
- updatedAt: z.string(),
54
- projectMemberId: z.string(),
55
- })
56
- })));
@@ -1,54 +0,0 @@
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
- }