@seam-rpc/server 5.0.0 → 5.0.2

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,8 +1,8 @@
1
- import { Result, ApiError, ApiErrorInterface } from "@seam-rpc/core";
1
+ import { Result, ApiError } 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 { ApiError as RpcError };
5
+ export { ApiError };
6
6
  export type { Result };
7
7
  type Simplify<T> = T extends File ? File : T extends object ? {
8
8
  [K in keyof T]: Simplify<T[K]>;
@@ -29,14 +29,14 @@ export interface SeamEvents {
29
29
  inputValidationError: [error: unknown, context: SeamErrorContext];
30
30
  outputValidationError: [error: unknown, context: SeamErrorContext];
31
31
  }
32
- type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput> = (options: ProcedureOptions<Input>) => Result<z.infer<Output>, ApiError> | Promise<Result<z.infer<Output>, ApiError>>;
32
+ type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput> = (options: ProcedureOptions<Input>) => Result<z.infer<Output>, ApiError<any, any>> | Promise<Result<z.infer<Output>, ApiError<any, any>>>;
33
33
  interface ProcedureOptions<Input extends ProcedureInput> {
34
34
  input: Simplify<ProcedureInputData<Input>>;
35
35
  ctx: SeamContext;
36
36
  }
37
37
  type ProcedureInput = Record<string, z.ZodType>;
38
38
  type ProcedureOutput = z.ZodType;
39
- type ProcedureErrors = Record<string, z.ZodType | undefined>;
39
+ type ProcedureErrors = Record<string, any | undefined>;
40
40
  type ProcedureInputData<T extends ProcedureInput> = {
41
41
  [K in keyof T]: z.infer<T[K]>;
42
42
  };
@@ -46,10 +46,12 @@ interface SeamProcedure<Input extends ProcedureInput = {}, Output extends Proced
46
46
  errors?: Errors;
47
47
  handler?: ProcedureHandler<Input, Output>;
48
48
  }
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
- };
49
+ type ExtractResultError<T> = Awaited<T> extends Result<any, undefined> ? never : Awaited<T> extends Result<any, infer E> ? E : never;
50
+ type ErrorUnionToMap<T> = [
51
+ T
52
+ ] extends [never] ? {} : [T] extends [ApiError<infer ErrorMap, infer Code>] ? {
53
+ [K in Code]: ErrorMap[K];
54
+ } : never;
53
55
  export type ProcedureBuilder<Input extends ProcedureInput = {}, Output extends ProcedureOutput = z.ZodUndefined, Errors extends ProcedureErrors = {}> = {
54
56
  _def: SeamProcedure<Input, Output, Errors>;
55
57
  input: <T extends ProcedureInput>(schema: T) => ProcedureBuilder<T, Output, Errors>;
@@ -69,24 +71,15 @@ export declare class SeamSpace extends EventEmitter<SeamEvents> {
69
71
  get jsonParser(): import("connect").NextHandleFunction;
70
72
  get fileHandler(): express.RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
71
73
  }
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
74
  type InferInput<P> = P extends ProcedureBuilder<infer I, any, any> ? {
85
75
  [K in keyof I]: I[K] extends z.ZodType ? z.infer<I[K]> : never;
86
76
  } : never;
87
77
  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]>;
78
+ type InferErrors<P> = P extends ProcedureBuilder<any, any, infer E> ? [keyof E] extends [never] ? undefined : {
79
+ [K in keyof E]: ApiError<E, K>;
80
+ }[keyof E] : never;
81
+ export type RouterToClient<RouterList extends Record<string, Record<string, ProcedureBuilder<any, any, any>>>> = {
82
+ [R in keyof RouterList]: {
83
+ [P in keyof RouterList[R]]: (input: InferInput<RouterList[R][P]>) => Promise<Result<InferOutput<RouterList[R][P]>, InferErrors<RouterList[R][P]>>>;
84
+ };
92
85
  };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ 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 { ApiError as RpcError };
6
+ export { ApiError };
7
7
  ;
8
8
  export async function createSeamSpace(app, fileHandler) {
9
9
  if (!fileHandler) {
package/package.json CHANGED
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "name": "@seam-rpc/server",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
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"
package/dist/router.d.ts DELETED
@@ -1,14 +0,0 @@
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 DELETED
@@ -1,197 +0,0 @@
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
- };