@seam-rpc/server 3.0.1 → 4.0.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.
@@ -1,23 +1,32 @@
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 * as z from "zod";
6
+ import { zodToTs, createAuxiliaryTypeStore, printNode } from "zod-to-ts";
7
+ import { existsSync, writeFileSync } from "fs";
5
8
  export async function genClient() {
6
9
  const args = process.argv;
7
10
  let config;
8
11
  if (args.length == 3) {
12
+ // Use config
13
+ // Check if config exists
9
14
  if (!fs.existsSync("./seam-rpc.config.json"))
10
15
  return console.error("\x1b[31mCommand arguments omitted and no config file found.\x1b[0m\n"
11
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
12
18
  config = JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
13
19
  }
14
20
  else if (args.length == 5 || args.length == 6) {
21
+ // Use command args
22
+ // Load config from command args
15
23
  config = {
16
24
  inputFiles: args[3],
17
25
  outputFolder: args[4]
18
26
  };
19
27
  }
20
28
  else {
29
+ // Invalid command usage
21
30
  return console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
22
31
  }
23
32
  const inputFiles = await fg(config.inputFiles);
@@ -26,7 +35,8 @@ export async function genClient() {
26
35
  try {
27
36
  const outputFiles = [];
28
37
  for (const inputFile of inputFiles) {
29
- const outputFile = generateClientFile(inputFile, outputPath);
38
+ // console.log(getProcedureInfo(inputFile));
39
+ const outputFile = await generateClientFile(inputFile, outputPath);
30
40
  outputFiles.push(removeRootPath(outputFile, rootPath));
31
41
  }
32
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")}`);
@@ -36,66 +46,148 @@ export async function genClient() {
36
46
  process.exit(1);
37
47
  }
38
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
+ // }
39
110
  function removeRootPath(path, rootPath) {
40
111
  return "." + path.slice(rootPath.length);
41
112
  }
42
- function generateClientFile(inputFile, outputPath) {
43
- const file = path.resolve(process.cwd(), inputFile);
44
- if (!fs.existsSync(file)) {
45
- console.error(`File ${file} not found`);
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`);
46
117
  process.exit(1);
47
118
  }
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());
59
- }
60
- else if (ts.isFunctionDeclaration(node) && hasExportModifier(node)) {
61
- if (!node.name) {
62
- console.error("Missing function name.");
63
- process.exit(1);
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)}`);
64
144
  }
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
145
  }
81
- else if ((ts.isInterfaceDeclaration(node) ||
82
- ts.isTypeAliasDeclaration(node) ||
83
- ts.isEnumDeclaration(node))
84
- && hasExportModifier(node)) {
85
- const text = node.getFullText(sourceFile).trim();
86
- typeDefs.push(text);
87
- }
88
- });
89
- const content = [imports.join("\n"), typeDefs.join("\n"), apiDef.join("\n")].join("\n");
90
- fs.writeFileSync(path.resolve(outputPath, path.basename(file)), content, "utf-8");
91
- return file;
92
- }
93
- function hasExportModifier(node) {
94
- if (ts.isVariableStatement(node) ||
95
- ts.isFunctionDeclaration(node) ||
96
- ts.isClassDeclaration(node) ||
97
- ts.isInterfaceDeclaration(node)) {
98
- return !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
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);
99
169
  }
100
- return false;
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);
101
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
+ // }
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
- export { SeamFile, ISeamFile };
2
+ import express, { Express, NextFunction, Request, RequestHandler, Response } from "express";
3
+ import * as z from "zod";
5
4
  export interface RouterDefinition {
6
- [funcName: string]: (...args: any[]) => Promise<any>;
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
- functionName: string;
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 SeamSpaceEvents {
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
- export declare class SeamSpace extends EventEmitter<SeamSpaceEvents> {
21
- private app;
22
- private fileHandler;
23
- private jsonParser;
24
- constructor(app: Express, fileHandler: RequestHandler);
25
- createRouter(path: string, routerDefinition: RouterDefinition): void;
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
- export interface SeamContext {
28
- request: Request;
29
- response: Response;
30
- next: NextFunction;
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 { SeamFile, extractFiles, injectFiles } from "@seam-rpc/core";
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
- export { SeamFile };
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
- export class SeamSpace extends EventEmitter {
23
- constructor(app, fileHandler) {
24
- super();
25
- this.app = app;
26
- this.fileHandler = fileHandler;
27
- this.jsonParser = express.json();
28
- }
29
- createRouter(path, routerDefinition) {
30
- const router = Router();
31
- router.post("/:funcName", async (req, res, next) => {
32
- if (!(req.params.funcName in routerDefinition))
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
- const contentType = req.headers["content-type"] || "";
35
- const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
36
- if (contentType.startsWith("application/json")) {
37
- await runMiddleware(this.jsonParser);
38
- }
39
- else if (contentType.startsWith("multipart/form-data")) {
40
- await runMiddleware(this.fileHandler);
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
- else {
50
- // multipart/form-data
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
- let result;
63
+ // Validate input
60
64
  try {
61
- const ctx = {
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
- result = await routerDefinition[req.params.funcName](...args, ctx);
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
- this.emit("apiError", error, {
92
+ seamSpace.emit("apiError", error, {
70
93
  routerPath: path,
71
- functionName: req.params.funcName,
94
+ procedureName: req.params.procName,
95
+ input,
96
+ validatedInput,
97
+ output,
98
+ validatedOutput,
72
99
  request: req,
73
100
  response: res,
74
- next: 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.forEach((file, index) => {
89
- form.append(`file-${index}`, Buffer.from(file.data), {
90
- filename: file.fileName || `file-${index}`,
91
- contentType: file.mimeType || "application/octet-stream",
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
- this.emit("internalError", error, {
148
+ seamSpace.emit("internalError", error, {
99
149
  routerPath: path,
100
- functionName: req.params.funcName,
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
- res.status(500).send({ error: String(error) });
159
+ console.log("INTERNAL ERROR", error);
160
+ res.sendStatus(500); //.send({ error: String(error) });
106
161
  }
107
162
  });
108
- this.app.use(path, router);
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
  }
@@ -0,0 +1,54 @@
1
+ export class ValString {
2
+ constructor(value) {
3
+ this._isValid = true;
4
+ this._value = value;
5
+ }
6
+ get isValid() {
7
+ return this._isValid;
8
+ }
9
+ min(value) {
10
+ if (this._isValid)
11
+ this._isValid = this._value.length >= value;
12
+ return this;
13
+ }
14
+ max(value) {
15
+ if (this._isValid)
16
+ this._isValid = this._value.length <= value;
17
+ return this;
18
+ }
19
+ length(value) {
20
+ if (this._isValid)
21
+ this._isValid = this._value.length == value;
22
+ return this;
23
+ }
24
+ startsWith(value) {
25
+ if (this._isValid)
26
+ this._isValid = this._value.startsWith(value);
27
+ return this;
28
+ }
29
+ endsWith(value) {
30
+ if (this._isValid)
31
+ this._isValid = this._value.endsWith(value);
32
+ return this;
33
+ }
34
+ includes(value) {
35
+ if (this._isValid)
36
+ this._isValid = this._value.includes(value);
37
+ return this;
38
+ }
39
+ regex(regExpr) {
40
+ // if (this._isValid)
41
+ // this._isValid = this._value.startsWith(value);
42
+ return this;
43
+ }
44
+ email() {
45
+ return this;
46
+ }
47
+ }
48
+ function createUser(name, age, data) {
49
+ name.min(3).max(50);
50
+ age.gte(1).lt(150);
51
+ data.description?.max(250);
52
+ }
53
+ function validate(data) {
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seam-rpc/server",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,9 +34,11 @@
34
34
  "multer": "^2.0.2"
35
35
  },
36
36
  "dependencies": {
37
- "@seam-rpc/core": "^1.0.0",
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
  }
@@ -1 +0,0 @@
1
- export declare function genClient(): Promise<void>;
@@ -1 +0,0 @@
1
- export declare function genConfig(): Promise<void>;
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export interface SeamConfig {
3
- inputFiles: string;
4
- outputFolder: string;
5
- }