@seam-rpc/server 4.3.21 → 5.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.
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 as RpcError };
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; }
@@ -0,0 +1,14 @@
1
+ import { Router } from "express";
2
+ import { ProcedureBuilderList, SeamSpace } from "./index.js";
3
+ export declare class SeamRouter {
4
+ private seamSpace;
5
+ private _procedures;
6
+ private _router;
7
+ constructor(seamSpace: SeamSpace, name: string);
8
+ get router(): Router;
9
+ private runMiddleware;
10
+ private validateInput;
11
+ private validateOutput;
12
+ private validateData;
13
+ addProcedures(procedures: ProcedureBuilderList): void;
14
+ }
package/dist/router.js ADDED
@@ -0,0 +1,197 @@
1
+ import { extractFiles, injectFiles, ApiError } from "@seam-rpc/core";
2
+ import { Router } from "express";
3
+ import FormData from "form-data";
4
+ import * as z from "zod";
5
+ export class SeamRouter {
6
+ constructor(seamSpace, name) {
7
+ this.seamSpace = seamSpace;
8
+ this._procedures = {};
9
+ this._router = Router();
10
+ this._router.post("/:procName", async (req, res, next) => {
11
+ const procedure = this._procedures[req.params.procName];
12
+ if (!procedure || !procedure.handler)
13
+ return res.sendStatus(404);
14
+ let input;
15
+ let validatedInput = undefined;
16
+ let output = undefined;
17
+ let validatedOutput;
18
+ // Middleware
19
+ try {
20
+ input = await this.runMiddleware(req, res);
21
+ }
22
+ catch (err) {
23
+ console.error(err);
24
+ return res.status(415).send(String(err));
25
+ }
26
+ // Validate input
27
+ try {
28
+ validatedInput = this.validateInput(input, procedure.input);
29
+ }
30
+ catch (err) {
31
+ seamSpace.emit("inputValidationError", err, {
32
+ routerName: name,
33
+ procedureName: req.params.procName,
34
+ input,
35
+ validatedInput,
36
+ output,
37
+ validatedOutput,
38
+ request: req,
39
+ response: res,
40
+ next,
41
+ });
42
+ res.sendStatus(400);
43
+ return;
44
+ }
45
+ // Call procedure
46
+ const ctx = {
47
+ request: req,
48
+ response: res,
49
+ next,
50
+ };
51
+ function toResError(error) {
52
+ if (error instanceof ApiError)
53
+ return { isApiError: true, error: error.toJSON() };
54
+ else
55
+ return { isApiError: false };
56
+ }
57
+ try {
58
+ output = await procedure.handler({ input: validatedInput, ctx, error: errorHandler });
59
+ }
60
+ catch (error) {
61
+ seamSpace.emit("apiError", error, {
62
+ routerName: name,
63
+ procedureName: req.params.procName,
64
+ input,
65
+ validatedInput,
66
+ output,
67
+ validatedOutput,
68
+ request: req,
69
+ response: res,
70
+ next: () => { res.status(400).json(toResError(error)); return next(); },
71
+ });
72
+ return;
73
+ }
74
+ if (!output.ok) {
75
+ seamSpace.emit("apiError", output.error, {
76
+ routerName: name,
77
+ procedureName: req.params.procName,
78
+ input,
79
+ validatedInput,
80
+ output,
81
+ validatedOutput,
82
+ request: req,
83
+ response: res,
84
+ next: () => { res.status(400).json(toResError(output.error)); return next(); },
85
+ });
86
+ return;
87
+ }
88
+ // Validate output
89
+ try {
90
+ validatedOutput = this.validateOutput(output, z.object({ ok: z.literal(true), data: procedure.output }).or(z.object({ ok: z.literal(false), error: z.any() })));
91
+ }
92
+ catch (err) {
93
+ seamSpace.emit("outputValidationError", err, {
94
+ routerName: name,
95
+ procedureName: req.params.procName,
96
+ input,
97
+ validatedInput,
98
+ output,
99
+ validatedOutput,
100
+ request: req,
101
+ response: res,
102
+ next,
103
+ });
104
+ res.sendStatus(500);
105
+ return;
106
+ }
107
+ try {
108
+ const { json, files, paths } = extractFiles({ result: validatedOutput });
109
+ // Does not include file(s)
110
+ if (files.length === 0) {
111
+ res.json(json);
112
+ return;
113
+ }
114
+ // Includes file(s)
115
+ const form = new FormData();
116
+ form.append("json", JSON.stringify(json));
117
+ form.append("paths", JSON.stringify(paths));
118
+ for (let i = 0; i < files.length; i++) {
119
+ const file = files[i];
120
+ const buffer = Buffer.from(await file.arrayBuffer());
121
+ form.append(`file-${i}`, buffer, {
122
+ filename: file.name || `file-${i}`,
123
+ contentType: file.type || "application/octet-stream",
124
+ });
125
+ }
126
+ res.writeHead(200, form.getHeaders());
127
+ form.pipe(res);
128
+ }
129
+ catch (error) {
130
+ seamSpace.emit("internalError", error, {
131
+ routerName: name,
132
+ procedureName: req.params.procName,
133
+ input,
134
+ validatedInput,
135
+ output,
136
+ validatedOutput,
137
+ request: req,
138
+ response: res,
139
+ next: next,
140
+ });
141
+ res.sendStatus(500); //.send({ error: String(error) });
142
+ }
143
+ });
144
+ seamSpace.app.use(name, this._router);
145
+ }
146
+ get router() {
147
+ return this._router;
148
+ }
149
+ async runMiddleware(req, res) {
150
+ const contentType = req.headers["content-type"] || "";
151
+ const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
152
+ if (contentType.startsWith("application/json")) {
153
+ await runMiddleware(this.seamSpace.jsonParser);
154
+ }
155
+ else if (contentType.startsWith("multipart/form-data")) {
156
+ await runMiddleware(this.seamSpace.fileHandler);
157
+ }
158
+ else {
159
+ throw new Error("Unsupported content type.");
160
+ }
161
+ if (contentType.startsWith("application/json"))
162
+ return req.body;
163
+ // multipart/form-data (already checked before)
164
+ let input = JSON.parse(req.body.json);
165
+ const paths = JSON.parse(req.body.paths);
166
+ const files = (req.files ?? []).map((file, index) => ({
167
+ path: paths[index],
168
+ file: new File([file.buffer], file.originalname, { type: file.mimetype }),
169
+ }));
170
+ injectFiles(input, files);
171
+ return input;
172
+ }
173
+ validateInput(input, inputSchema) {
174
+ return this.validateData(input, z.object(inputSchema));
175
+ }
176
+ validateOutput(output, procOutput) {
177
+ return this.validateData(output, procOutput);
178
+ }
179
+ validateData(data, schema) {
180
+ if (!schema) {
181
+ if (data)
182
+ throw new Error("Received data, but no data expected.");
183
+ return null;
184
+ }
185
+ if (!data)
186
+ throw new Error("No data was received, but data was expected.");
187
+ return schema.parse(data);
188
+ }
189
+ addProcedures(procedures) {
190
+ for (const proc in procedures) {
191
+ this._procedures[proc] = procedures[proc]._def;
192
+ }
193
+ }
194
+ }
195
+ const errorHandler = (code, ...args) => {
196
+ return new ApiError(code, args[0]);
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seam-rpc/server",
3
- "version": "4.3.21",
3
+ "version": "5.0.0",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {