@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 +17 -24
- package/dist/index.js +1 -1
- package/package.json +1 -4
- package/dist/router.d.ts +0 -14
- package/dist/router.js +0 -197
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Result, ApiError
|
|
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
|
|
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
|
|
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,
|
|
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<
|
|
51
|
-
|
|
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 :
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
6
|
+
export { ApiError };
|
|
7
7
|
;
|
|
8
8
|
export async function createSeamSpace(app, fileHandler) {
|
|
9
9
|
if (!fileHandler) {
|
package/package.json
CHANGED
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
|
-
};
|