@seam-rpc/server 4.1.0 → 4.2.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/README.md +1 -285
- package/dist/bin/generate.js +7 -4
- package/dist/index.d.ts +25 -20
- package/dist/index.js +27 -15
- package/package.json +1 -1
- package/dist/bin/gen-client.d.ts +0 -8
- package/dist/bin/gen-client.js +0 -154
- package/dist/bin/gen-config.d.ts +0 -1
- package/dist/bin/gen-config.js +0 -46
package/README.md
CHANGED
|
@@ -1,285 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
# SeamRPC
|
|
4
|
-
|
|
5
|
-
## About
|
|
6
|
-
SeamRPC is a simple RPC library for client-server communication using TypeScript using Express for the server.
|
|
7
|
-
|
|
8
|
-
Making requests to the server is as simple as calling a function and SeamRPC sends it to server for you under the hood.
|
|
9
|
-
|
|
10
|
-
## Setup
|
|
11
|
-
### Server
|
|
12
|
-
Implement your API functions in a TypeScript file. It's recommended to split different routes into different files, all inside the same folder. You can also optionally include JSDoc comments for the functions. The returned value of an API function is sent from the server to the client. If an error is thrown in the API function in the server, the function throws an error in the client as well (Seam RPC internally responds with HTTP code 400 which the client interprets as an error).
|
|
13
|
-
|
|
14
|
-
> **Note:** For consistency reasons between server and client API functions, Seam RPC requires all API functions to return a Promise.
|
|
15
|
-
|
|
16
|
-
**Example:**
|
|
17
|
-
```
|
|
18
|
-
server-app
|
|
19
|
-
├─ index.ts
|
|
20
|
-
└─ api
|
|
21
|
-
├─ users.ts
|
|
22
|
-
└─ posts.ts
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`api/users.ts`
|
|
26
|
-
```ts
|
|
27
|
-
import { SeamFile } from "@seam-rpc/server";
|
|
28
|
-
|
|
29
|
-
export interface User {
|
|
30
|
-
id: string;
|
|
31
|
-
name: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const users: User[] = [];
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Creates a new user and returns its ID.
|
|
38
|
-
* @param name The name of the user.
|
|
39
|
-
* @returns ID of the newly created user.
|
|
40
|
-
*/
|
|
41
|
-
export async function createUser(name: string): Promise<string> {
|
|
42
|
-
const user = {
|
|
43
|
-
id: Date.now().toString(),
|
|
44
|
-
name
|
|
45
|
-
};
|
|
46
|
-
users.push(user);
|
|
47
|
-
return user.id;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Gets a user by ID.
|
|
52
|
-
* @param id The ID of the user.
|
|
53
|
-
* @returns The user object.
|
|
54
|
-
*/
|
|
55
|
-
export async function getUser(id: string): Promise<User | undefined> {
|
|
56
|
-
const user = users.find(e => e.id == id);
|
|
57
|
-
if (user)
|
|
58
|
-
return user;
|
|
59
|
-
else
|
|
60
|
-
throw new Error("user not found");
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
#### Create a Seam Space
|
|
65
|
-
|
|
66
|
-
A Seam Space is linked to an Express app and is what you defined routers to. You define one Seam Space for your API, which can then be separated in to different routers. Each router can be any kind of structure with functions (e.g. an object or a module). This example uses files as modules.
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
import express from "express";
|
|
70
|
-
import { createSeamSpace } from "@seam-rpc/server";
|
|
71
|
-
|
|
72
|
-
// Import as modules
|
|
73
|
-
import * as usersRouter from "./api/users.js";
|
|
74
|
-
import * as postsRouter from "./api/posts.js";
|
|
75
|
-
|
|
76
|
-
// Create express app
|
|
77
|
-
const app = express();
|
|
78
|
-
|
|
79
|
-
// Create Seam Space with express app
|
|
80
|
-
const seamSpace = await createSeamSpace(app);
|
|
81
|
-
|
|
82
|
-
// Create routers
|
|
83
|
-
seamSpace.createRouter("/users", usersRouter);
|
|
84
|
-
seamSpace.createRouter("/posts", postsRouter);
|
|
85
|
-
|
|
86
|
-
// Start express server
|
|
87
|
-
app.listen(3000, () => {
|
|
88
|
-
console.log("Listening on port 3000");
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Client
|
|
93
|
-
The client needs to have the same schema as your API so you can call the API functions and have autocomplete. Behind the scenes these functions will send an HTTP requests to the server. SeamRPC can automatically generate the client schema files. To do this, you can either run the command `seam-rpc gen-client <input-files> <output-folder>` or [define a config file](#config-file) and then run the command `seam-rpc gen-client`.
|
|
94
|
-
|
|
95
|
-
- `input-files` - Specify what files to generate the client files from. You can use [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to specify the files.
|
|
96
|
-
- `output-folder` - Specify the folder where to store the generated client api files.
|
|
97
|
-
|
|
98
|
-
**Example:**
|
|
99
|
-
`seam-rpc gen-client ./src/api/* ../server-app/src/api`
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
client-app
|
|
103
|
-
├─ index.ts
|
|
104
|
-
└─ api
|
|
105
|
-
├─ users.ts
|
|
106
|
-
└─ posts.ts
|
|
107
|
-
```
|
|
108
|
-
The api folder in the client contains the generated API client files, and should not be manually edited.
|
|
109
|
-
|
|
110
|
-
The generated `api/users.ts` file:
|
|
111
|
-
> Notice that the JSDoc comments are included in the client files.
|
|
112
|
-
```ts
|
|
113
|
-
import { callApi, SeamFile, ISeamFile } from "@seam-rpc/client";
|
|
114
|
-
export interface User {
|
|
115
|
-
id: string;
|
|
116
|
-
name: string;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Creates a new user and returns its ID.
|
|
120
|
-
* @param name The name of the user.
|
|
121
|
-
* @returns ID of the newly created user.
|
|
122
|
-
*/
|
|
123
|
-
export function createUser(name: string): Promise<string> { return callApi("users", "createUser", [name]); }
|
|
124
|
-
/**
|
|
125
|
-
* Gets a user by ID.
|
|
126
|
-
* @param id The ID of the user.
|
|
127
|
-
* @returns The user object.
|
|
128
|
-
*/
|
|
129
|
-
export function getUser(id: string): Promise<User | undefined> { return callApi("users", "getUser", [id]); }
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
#### Connect client to server
|
|
133
|
-
To establish the connection from the client to the server, you need to specify which URL to call. This example is using a self-hosted server running on port 3000 so it uses `http://localhost:3000`. Just call `createClient` to create the client and specify the URL.
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
createClient("http://localhost:3000");
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Config file
|
|
140
|
-
If you don't want to specify the input files and output folder every time you want to generate the client files, you can create a config file where you define these paths. You can create a `seam-rpc.config.json` file at the root of your project and use the following data:
|
|
141
|
-
```json
|
|
142
|
-
{
|
|
143
|
-
"inputFiles": "./src/api/*",
|
|
144
|
-
"outputFolder": "../client/src/api"
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
or you can automatically generate a file using `seam-rpc gen-config [input-files] [output-folder]`. If you don't specify the input files and output folder, it will use the default paths (see JSON above).
|
|
148
|
-
|
|
149
|
-
## Uploading and downloading files
|
|
150
|
-
Both server and client can send files seamlessly. Just use the SeamFile class for this. You can have a parameter as a file or an array/object containing a file. You can have deeply nested files inside objects.
|
|
151
|
-
|
|
152
|
-
A SeamFile has 3 properties:
|
|
153
|
-
- `data` - binary data
|
|
154
|
-
- `fileName` (optional) - name of the file
|
|
155
|
-
- `mimeType` (optional) - The MIME type of the file ([Learn more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types))
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
**Example:**
|
|
159
|
-
```ts
|
|
160
|
-
interface UserData {
|
|
161
|
-
id: string;
|
|
162
|
-
name: string;
|
|
163
|
-
avatar: SeamFile;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function updateUser(userId: string, userData: UserData): Promise<void> {
|
|
167
|
-
if (userData.avatar.mimeType != "image/png" && userData.avatar.mimeType != "image/jpeg")
|
|
168
|
-
throw new Error("Only PNGs and JPEGs allowed for avatar.");
|
|
169
|
-
|
|
170
|
-
users[userId].name = userData.name;
|
|
171
|
-
users[userId].avatar = userData.avatar.fileName;
|
|
172
|
-
writeFileSync(`../avatars/${userData.avatar.fileName}`, userData.avatar.data);
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Important notices
|
|
177
|
-
- The generated client files contain all imports from the api implementation file in the backend that import from the current relative folder (`./`). This is the simplest way I have to include imports (at least for now). It may import functions and unused symbols but that shouldn't be too worrying.
|
|
178
|
-
- Don't include backend/server functions inside the server api files.
|
|
179
|
-
- Only exported functions will be included in the client generated files.
|
|
180
|
-
|
|
181
|
-
## Supported types
|
|
182
|
-
SeamRPC supports the following types (at least for now):
|
|
183
|
-
- `string`
|
|
184
|
-
- `number`
|
|
185
|
-
- `boolean`
|
|
186
|
-
- `null`
|
|
187
|
-
- `undefined`
|
|
188
|
-
- arrays
|
|
189
|
-
- objects
|
|
190
|
-
|
|
191
|
-
Classes are technically supported, in that the data is serialized to JSON.
|
|
192
|
-
|
|
193
|
-
Other JavaScript types are not supported, although SeamRPC doesn't prevent you from using them, in which case they might lead to unexpected beahviour or even errors.
|
|
194
|
-
|
|
195
|
-
The Date object type is not supported (at least for now). However, you can use `number` and pass `Date.now()` or `string` and pass `new Date().toString()`. This is not different than a normal HTTP request using JSON. SeamRPC also uses JSON behind the scenes, that's why there's these limitations, which could be overcome but I've decided not to because it would probably add more overhead to the logic.
|
|
196
|
-
|
|
197
|
-
## Context parameter
|
|
198
|
-
|
|
199
|
-
### Server
|
|
200
|
-
If you want, you can get access to the request, response and next function from Express. Just add a parameter of type SeamContext at the end of the API function. You can name the parameter whatever you like. This parameter is optional. This parameter is not included in the client generated files.
|
|
201
|
-
|
|
202
|
-
Example:
|
|
203
|
-
```ts
|
|
204
|
-
export async function createUser(name: string, context: SeamContext): Promise<string> {
|
|
205
|
-
// Using the context to read the request path.
|
|
206
|
-
console.log("Request path:", context.request.originalUrl);
|
|
207
|
-
|
|
208
|
-
const user = {
|
|
209
|
-
id: Date.now().toString(),
|
|
210
|
-
name
|
|
211
|
-
};
|
|
212
|
-
users.push(user);
|
|
213
|
-
return user.id;
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Client
|
|
218
|
-
The client currently doesn't support access to the response object from the fetch.
|
|
219
|
-
|
|
220
|
-
## Error handling
|
|
221
|
-
|
|
222
|
-
### Server
|
|
223
|
-
To catch errors across router functions in the server, you can use the `apiError` and `internalError` events.
|
|
224
|
-
- `apiError` - Error ocurred when calling or during execution of your API function.
|
|
225
|
-
- `internalError` - SeamRPC internal error. Please report if you find an error that seems like a bug or requires improvement.
|
|
226
|
-
|
|
227
|
-
Example:
|
|
228
|
-
```ts
|
|
229
|
-
const seamSpace = await createSeamSpace(app);
|
|
230
|
-
|
|
231
|
-
seamSpace.on("apiError", (error, context) => {
|
|
232
|
-
console.error(`API Error at ${context.functionName}!`, error);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
seamSpace.on("internalError", (error, context) => {
|
|
236
|
-
console.error(`Internal Error at ${context.functionName}!`, error);
|
|
237
|
-
});
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
> **Note:** The above example is to illustrate the use of error handlers and is not a complete example. Please consult the rest of the README or the examples in the examples directory.
|
|
241
|
-
|
|
242
|
-
## Middleware
|
|
243
|
-
|
|
244
|
-
### Client
|
|
245
|
-
You can add middleware functions in the client. There's two types:
|
|
246
|
-
- Pre-request - Before the request is sent. You might for example want to add some header to the request.
|
|
247
|
-
- Post-request - After the request was sent. You might for example want to read some header from reponse.
|
|
248
|
-
|
|
249
|
-
There's two ways to add middleware, either when creating the client:
|
|
250
|
-
```ts
|
|
251
|
-
createClient("http://localhost:3000", {
|
|
252
|
-
middleware: {
|
|
253
|
-
request: [
|
|
254
|
-
ctx => {
|
|
255
|
-
ctx.request.headers = {
|
|
256
|
-
...ctx.request.headers,
|
|
257
|
-
"X-MyHeader": "Test"
|
|
258
|
-
};
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
response: [
|
|
262
|
-
ctx => {
|
|
263
|
-
console.log(ctx.response.headers.get("X-SomeHeader"));
|
|
264
|
-
}
|
|
265
|
-
]
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
```
|
|
269
|
-
... or after creating the client:
|
|
270
|
-
```ts
|
|
271
|
-
const client = createClient("http://localhost:3000");
|
|
272
|
-
|
|
273
|
-
client.preRequest(ctx => {
|
|
274
|
-
ctx.request.headers = {
|
|
275
|
-
...ctx.request.headers,
|
|
276
|
-
"X-MyHeader": "Test"
|
|
277
|
-
};
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
client.postRequest(ctx => {
|
|
281
|
-
console.log(ctx.response.headers.get("X-SomeHeader"));
|
|
282
|
-
});
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
> You can add as many middleware functions as you like.
|
|
1
|
+
[Documentation](https://github.com/DanielC49/seam-rpc/blob/main/README.md)
|
package/dist/bin/generate.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function generateClient() {
|
|
|
26
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
27
|
}
|
|
28
28
|
catch (err) {
|
|
29
|
-
console.error("❌ Failed to generate client file:", err.message);
|
|
29
|
+
console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -96,7 +96,7 @@ export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
|
|
|
96
96
|
* +===================================+
|
|
97
97
|
*/
|
|
98
98
|
|
|
99
|
-
import { callApi } from "@seam-rpc/client";
|
|
99
|
+
import { callApi, Result, RpcError } from "@seam-rpc/client";
|
|
100
100
|
|
|
101
101
|
`;
|
|
102
102
|
const functions = [];
|
|
@@ -116,14 +116,17 @@ import { callApi } from "@seam-rpc/client";
|
|
|
116
116
|
hasInput = true;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
const
|
|
119
|
+
const dataType = proc._def.output ? convert(proc._def.output) : "void";
|
|
120
|
+
const errors = Object.entries(proc._def.errors ?? {});
|
|
121
|
+
const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1])}>`).join(" | ");
|
|
122
|
+
const fullReturnType = `Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>`;
|
|
120
123
|
const params = hasInput ? `input: ${inputType}` : "";
|
|
121
124
|
const args = [`"${routerName}"`, `"${name}"`];
|
|
122
125
|
if (hasInput)
|
|
123
126
|
args.push("input");
|
|
124
127
|
const call = `callApi(${args.join(", ")})`;
|
|
125
128
|
const comment = comments[name] ? comments[name] + "\n" : "";
|
|
126
|
-
const func = `${comment}export function ${name}(${params}): Promise<${
|
|
129
|
+
const func = `${comment}export function ${name}(${params}): Promise<${fullReturnType}> {
|
|
127
130
|
return ${call};
|
|
128
131
|
}`;
|
|
129
132
|
functions.push(func);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ import * as z from "zod";
|
|
|
4
4
|
export interface RouterDefinition {
|
|
5
5
|
[procName: string]: (...args: any[]) => Promise<any>;
|
|
6
6
|
}
|
|
7
|
+
type Simplify<T> = T extends File ? File : T extends object ? {
|
|
8
|
+
[K in keyof T]: Simplify<T[K]>;
|
|
9
|
+
} : T;
|
|
7
10
|
export interface SeamContext {
|
|
8
11
|
request: Request;
|
|
9
12
|
response: Response;
|
|
@@ -26,50 +29,52 @@ export interface SeamEvents {
|
|
|
26
29
|
inputValidationError: [error: unknown, context: SeamErrorContext];
|
|
27
30
|
outputValidationError: [error: unknown, context: SeamErrorContext];
|
|
28
31
|
}
|
|
29
|
-
type ProcedureHandler<Input extends ProcedureInput, Output extends
|
|
32
|
+
type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => z.infer<Output> | Promise<z.infer<Output>>;
|
|
33
|
+
interface ProcedureOptions<Input extends ProcedureInput, Errors extends ProcedureErrors> {
|
|
34
|
+
input: Simplify<ProcedureInputData<Input>>;
|
|
35
|
+
ctx: SeamContext;
|
|
36
|
+
error: RpcErrorFactory<Errors>;
|
|
37
|
+
}
|
|
38
|
+
type RpcErrorFactory<Errors extends ProcedureErrors> = <Code extends keyof Errors>(code: Code, ...args: z.infer<Errors[Code]> extends undefined ? [] : [data: z.infer<Errors[Code]>]) => void;
|
|
30
39
|
type ProcedureInput = Record<string, z.ZodType>;
|
|
31
40
|
type ProcedureOutput = z.ZodType;
|
|
41
|
+
type ProcedureErrors = Record<string, z.ZodType>;
|
|
42
|
+
type ProcedureBuilderList = Record<string, ProcedureBuilder<any, any, any>>;
|
|
32
43
|
type ProcedureInputData<T extends ProcedureInput> = {
|
|
33
44
|
[K in keyof T]: z.infer<T[K]>;
|
|
34
45
|
};
|
|
35
|
-
interface
|
|
36
|
-
input: Simplify<ProcedureInputData<T>>;
|
|
37
|
-
ctx: SeamContext;
|
|
38
|
-
}
|
|
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> {
|
|
46
|
+
interface SeamProcedure<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> {
|
|
43
47
|
input?: Input;
|
|
44
48
|
output?: Output;
|
|
45
|
-
|
|
49
|
+
errors?: Errors;
|
|
50
|
+
handler?: ProcedureHandler<Input, Output, Errors>;
|
|
46
51
|
}
|
|
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
|
-
|
|
52
|
+
export type ProcedureBuilder<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = {
|
|
53
|
+
_def: SeamProcedure<Input, Output, Errors>;
|
|
54
|
+
input: <T extends ProcedureInput>(schema: T) => ProcedureBuilder<T, Output, Errors>;
|
|
55
|
+
output: <T extends ProcedureOutput>(schema: T) => ProcedureBuilder<Input, T, Errors>;
|
|
56
|
+
errors: <T extends ProcedureErrors>(errors: T) => ProcedureBuilder<Input, Output, T>;
|
|
57
|
+
handler: (handler: ProcedureHandler<Input, Output, Errors>) => ProcedureBuilder<Input, Output, Errors>;
|
|
52
58
|
};
|
|
53
59
|
export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): Promise<SeamSpace>;
|
|
54
|
-
export declare function seamProcedure(): ProcedureBuilder<ProcedureInput, ProcedureOutput>;
|
|
60
|
+
export declare function seamProcedure(): ProcedureBuilder<ProcedureInput, ProcedureOutput, ProcedureErrors>;
|
|
55
61
|
export declare class SeamRouter {
|
|
56
62
|
private seamSpace;
|
|
57
|
-
private path;
|
|
58
|
-
private router;
|
|
59
63
|
private procedures;
|
|
64
|
+
private router;
|
|
60
65
|
constructor(seamSpace: SeamSpace, path: string);
|
|
61
66
|
private runMiddleware;
|
|
62
67
|
private validateInput;
|
|
63
68
|
private validateOutput;
|
|
64
69
|
private validateData;
|
|
65
|
-
addProcedures(procedures:
|
|
70
|
+
addProcedures(procedures: ProcedureBuilderList): void;
|
|
66
71
|
}
|
|
67
72
|
export declare class SeamSpace extends EventEmitter<SeamEvents> {
|
|
68
73
|
private _app;
|
|
69
74
|
private _fileHandler;
|
|
70
75
|
private _jsonParser;
|
|
71
76
|
constructor(_app: Express, _fileHandler: RequestHandler);
|
|
72
|
-
createRouter(path: string): SeamRouter;
|
|
77
|
+
createRouter(path: string, procedures?: ProcedureBuilderList): SeamRouter;
|
|
73
78
|
get app(): express.Express;
|
|
74
79
|
get jsonParser(): import("connect").NextHandleFunction;
|
|
75
80
|
get fileHandler(): express.RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { extractFiles, injectFiles } from "@seam-rpc/core";
|
|
1
|
+
import { extractFiles, injectFiles, RpcError } 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
6
|
;
|
|
7
7
|
;
|
|
8
|
+
const errorHandler = (code, ...args) => {
|
|
9
|
+
return new RpcError(code, args[0]);
|
|
10
|
+
};
|
|
8
11
|
export async function createSeamSpace(app, fileHandler) {
|
|
9
12
|
if (!fileHandler) {
|
|
10
13
|
let multer;
|
|
@@ -33,6 +36,10 @@ function createProcedureBuilder(definition = {}) {
|
|
|
33
36
|
...definition,
|
|
34
37
|
output: schema,
|
|
35
38
|
}),
|
|
39
|
+
errors: errors => createProcedureBuilder({
|
|
40
|
+
...definition,
|
|
41
|
+
errors
|
|
42
|
+
}),
|
|
36
43
|
handler: handler => createProcedureBuilder({
|
|
37
44
|
...definition,
|
|
38
45
|
handler
|
|
@@ -42,7 +49,6 @@ function createProcedureBuilder(definition = {}) {
|
|
|
42
49
|
export class SeamRouter {
|
|
43
50
|
constructor(seamSpace, path) {
|
|
44
51
|
this.seamSpace = seamSpace;
|
|
45
|
-
this.path = path;
|
|
46
52
|
this.procedures = {};
|
|
47
53
|
this.router = Router();
|
|
48
54
|
this.router.post("/:procName", async (req, res, next) => {
|
|
@@ -86,9 +92,19 @@ export class SeamRouter {
|
|
|
86
92
|
next
|
|
87
93
|
};
|
|
88
94
|
try {
|
|
89
|
-
output = await procedure.handler({ input: validatedInput, ctx });
|
|
95
|
+
output = await procedure.handler({ input: validatedInput, ctx, error: errorHandler });
|
|
90
96
|
}
|
|
91
97
|
catch (error) {
|
|
98
|
+
let errorResult = { rpcError: false };
|
|
99
|
+
if (error instanceof RpcError) {
|
|
100
|
+
errorResult = {
|
|
101
|
+
rpcError: true,
|
|
102
|
+
error: {
|
|
103
|
+
code: error.code,
|
|
104
|
+
data: error.data,
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
92
108
|
seamSpace.emit("apiError", error, {
|
|
93
109
|
routerPath: path,
|
|
94
110
|
procedureName: req.params.procName,
|
|
@@ -100,7 +116,7 @@ export class SeamRouter {
|
|
|
100
116
|
response: res,
|
|
101
117
|
next,
|
|
102
118
|
});
|
|
103
|
-
res.status(400).
|
|
119
|
+
res.status(400).json(errorResult);
|
|
104
120
|
return;
|
|
105
121
|
}
|
|
106
122
|
// Validate output
|
|
@@ -215,16 +231,12 @@ export class SeamSpace extends EventEmitter {
|
|
|
215
231
|
this._fileHandler = _fileHandler;
|
|
216
232
|
this._jsonParser = express.json();
|
|
217
233
|
}
|
|
218
|
-
createRouter(path) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return this._app;
|
|
223
|
-
}
|
|
224
|
-
get jsonParser() {
|
|
225
|
-
return this._jsonParser;
|
|
226
|
-
}
|
|
227
|
-
get fileHandler() {
|
|
228
|
-
return this._fileHandler;
|
|
234
|
+
createRouter(path, procedures = {}) {
|
|
235
|
+
const router = new SeamRouter(this, path);
|
|
236
|
+
router.addProcedures(procedures);
|
|
237
|
+
return router;
|
|
229
238
|
}
|
|
239
|
+
get app() { return this._app; }
|
|
240
|
+
get jsonParser() { return this._jsonParser; }
|
|
241
|
+
get fileHandler() { return this._fileHandler; }
|
|
230
242
|
}
|
package/package.json
CHANGED
package/dist/bin/gen-client.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare function genClient(): Promise<void>;
|
|
2
|
-
interface GenerateOptions {
|
|
3
|
-
tsPath: string;
|
|
4
|
-
jsPath: string;
|
|
5
|
-
outputPath: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function generateClientFile({ tsPath, jsPath, outputPath, }: GenerateOptions): Promise<string>;
|
|
8
|
-
export {};
|
package/dist/bin/gen-client.js
DELETED
|
@@ -1,154 +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 genClient() {
|
|
8
|
-
const args = process.argv;
|
|
9
|
-
const config = loadConfig(args);
|
|
10
|
-
if (!config)
|
|
11
|
-
return;
|
|
12
|
-
const sourceFiles = await fg(config.source);
|
|
13
|
-
const compiledFolder = path.resolve(config.compiledFolder);
|
|
14
|
-
const outputPath = path.resolve(config.outputFolder);
|
|
15
|
-
const rootPath = path.resolve(".");
|
|
16
|
-
function removeRootPath(path) {
|
|
17
|
-
return "." + path.slice(rootPath.length);
|
|
18
|
-
}
|
|
19
|
-
try {
|
|
20
|
-
const outputFiles = [];
|
|
21
|
-
for (let i = 0; i < sourceFiles.length; i++) {
|
|
22
|
-
const sourceFile = sourceFiles[i];
|
|
23
|
-
const path = await generateClientFile({ tsPath: sourceFile, jsPath: compiledFolder, outputPath });
|
|
24
|
-
const symb = i < sourceFiles.length - 1 ? " ├╴" : " └╴";
|
|
25
|
-
outputFiles.push(symb + removeRootPath(path));
|
|
26
|
-
}
|
|
27
|
-
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")}`);
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
console.error("❌ Failed to generate client file:", err.message);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function loadConfig(args) {
|
|
35
|
-
if (args.length == 3) {
|
|
36
|
-
// Use config
|
|
37
|
-
// Check if config exists
|
|
38
|
-
if (!fs.existsSync("./seam-rpc.config.json")) {
|
|
39
|
-
console.error("\x1b[31mCommand arguments omitted and no config file found.\x1b[0m\n"
|
|
40
|
-
+ "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.");
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
// Load config from config file
|
|
44
|
-
return JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
45
|
-
}
|
|
46
|
-
else if (args.length == 5 || args.length == 6) {
|
|
47
|
-
// Use command args
|
|
48
|
-
// Load config from command args
|
|
49
|
-
return {
|
|
50
|
-
source: args[3],
|
|
51
|
-
compiledFolder: args[4],
|
|
52
|
-
outputFolder: args[5]
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
// Invalid command usage
|
|
57
|
-
console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Extracts comments from TS file.
|
|
62
|
-
function getProcedureComments(filePath) {
|
|
63
|
-
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
64
|
-
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
|
|
65
|
-
const comments = {};
|
|
66
|
-
function visit(node) {
|
|
67
|
-
if (ts.isVariableStatement(node)) {
|
|
68
|
-
const comment = getFullComment(node, sourceText);
|
|
69
|
-
for (const decl of node.declarationList.declarations) {
|
|
70
|
-
if (ts.isIdentifier(decl.name))
|
|
71
|
-
comments[decl.name.text] = comment;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
ts.forEachChild(node, visit);
|
|
75
|
-
}
|
|
76
|
-
visit(sourceFile);
|
|
77
|
-
return comments;
|
|
78
|
-
}
|
|
79
|
-
function getFullComment(node, sourceText) {
|
|
80
|
-
const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
|
|
81
|
-
if (!ranges)
|
|
82
|
-
return "";
|
|
83
|
-
for (const range of ranges) {
|
|
84
|
-
const comment = sourceText.slice(range.pos, range.end);
|
|
85
|
-
// Only keep JSDoc-style comments
|
|
86
|
-
if (comment.startsWith("/**")) {
|
|
87
|
-
return comment;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return "";
|
|
91
|
-
}
|
|
92
|
-
function convert(schema) {
|
|
93
|
-
const store = createAuxiliaryTypeStore();
|
|
94
|
-
const { node } = zodToTs(schema, { auxiliaryTypeStore: store });
|
|
95
|
-
return printNode(node);
|
|
96
|
-
}
|
|
97
|
-
export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
|
|
98
|
-
const tsFile = path.resolve(process.cwd(), tsPath);
|
|
99
|
-
const tsFileName = path.basename(tsFile).slice(0, -path.extname(tsFile).length);
|
|
100
|
-
const jsFile = path.resolve(process.cwd(), path.join(jsPath, tsFileName + ".js"));
|
|
101
|
-
if (!existsSync(tsFile)) {
|
|
102
|
-
throw new Error(`TS file not found: ${tsFile}`);
|
|
103
|
-
}
|
|
104
|
-
if (!existsSync(jsFile)) {
|
|
105
|
-
throw new Error(`JS file not found: ${jsFile}`);
|
|
106
|
-
}
|
|
107
|
-
const mod = await import("file://" + jsFile);
|
|
108
|
-
const procedures = mod.default;
|
|
109
|
-
const routerName = path.basename(jsFile, path.extname(jsFile));
|
|
110
|
-
const comments = getProcedureComments(tsFile);
|
|
111
|
-
let output = `/**
|
|
112
|
-
* +===================================+
|
|
113
|
-
* | File auto-generated by SeamRPC. |
|
|
114
|
-
* | --- DO NOT EDIT --- |
|
|
115
|
-
* +===================================+
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
import { callApi } from "@seam-rpc/client";
|
|
119
|
-
|
|
120
|
-
`;
|
|
121
|
-
const functions = [];
|
|
122
|
-
for (const [name, proc] of Object.entries(procedures)) {
|
|
123
|
-
// Input
|
|
124
|
-
let inputType = "void";
|
|
125
|
-
let hasInput = false;
|
|
126
|
-
if (proc._def.input) {
|
|
127
|
-
const fields = [];
|
|
128
|
-
for (const [key, schema] of Object.entries(proc._def.input)) {
|
|
129
|
-
const isOptional = schema.isOptional?.() ?? false;
|
|
130
|
-
const tsType = convert(schema);
|
|
131
|
-
fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
|
|
132
|
-
}
|
|
133
|
-
if (fields.length > 0) {
|
|
134
|
-
inputType = `{ ${fields.join("; ")} }`;
|
|
135
|
-
hasInput = true;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const returnType = proc._def.output ? convert(proc._def.output) : "void";
|
|
139
|
-
const params = hasInput ? `input: ${inputType}` : "";
|
|
140
|
-
const args = [`"${routerName}"`, `"${name}"`];
|
|
141
|
-
if (hasInput)
|
|
142
|
-
args.push("input");
|
|
143
|
-
const call = `callApi(${args.join(", ")})`;
|
|
144
|
-
const comment = comments[name] ? comments[name] + "\n" : "";
|
|
145
|
-
const func = `${comment}export function ${name}(${params}): Promise<${returnType}> {
|
|
146
|
-
return ${call};
|
|
147
|
-
}`;
|
|
148
|
-
functions.push(func);
|
|
149
|
-
}
|
|
150
|
-
output += functions.join("\n\n");
|
|
151
|
-
const outFile = path.join(outputPath, `${routerName}.ts`);
|
|
152
|
-
writeFileSync(outFile, output, "utf-8");
|
|
153
|
-
return outFile;
|
|
154
|
-
}
|
package/dist/bin/gen-config.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function genConfig(): Promise<void>;
|
package/dist/bin/gen-config.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import readline from "readline";
|
|
3
|
-
export async function genConfig() {
|
|
4
|
-
const args = process.argv;
|
|
5
|
-
let source = "./src/api/*";
|
|
6
|
-
let compiledFolder = "./dist/api";
|
|
7
|
-
let outputFolder = "../client/src/api";
|
|
8
|
-
if (args.length == 5) {
|
|
9
|
-
source = args[3];
|
|
10
|
-
outputFolder = args[4];
|
|
11
|
-
}
|
|
12
|
-
else if (args.length > 3) {
|
|
13
|
-
return console.error("Usage: seam-rpc gen-config [input-files] [output-folder]");
|
|
14
|
-
}
|
|
15
|
-
if (fs.existsSync("./seam-rpc.config.json")) {
|
|
16
|
-
const rl = readline.createInterface({
|
|
17
|
-
input: process.stdin,
|
|
18
|
-
output: process.stdout,
|
|
19
|
-
});
|
|
20
|
-
rl.question("Config file already exists. Do you want to overwrite it? [Y/n] ", answer => {
|
|
21
|
-
if (answer && answer.toLowerCase() != "y" && answer.toLowerCase() != "yes") {
|
|
22
|
-
console.log("Operation canceled.");
|
|
23
|
-
process.exit(0);
|
|
24
|
-
}
|
|
25
|
-
rl.close();
|
|
26
|
-
generateConfig();
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
generateConfig();
|
|
31
|
-
}
|
|
32
|
-
function generateConfig() {
|
|
33
|
-
const config = {
|
|
34
|
-
source,
|
|
35
|
-
compiledFolder,
|
|
36
|
-
outputFolder
|
|
37
|
-
};
|
|
38
|
-
try {
|
|
39
|
-
fs.writeFileSync("./seam-rpc.config.json", JSON.stringify(config, null, 4), "utf-8");
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
console.log("\x1b[31mFailed to generate config file ./seam-rpc.config.json\x1b[0m\n" + e);
|
|
43
|
-
}
|
|
44
|
-
console.log(`\x1b[32mSuccessfully generated config file ./seam-rpc.config.json\x1b[0m`);
|
|
45
|
-
}
|
|
46
|
-
}
|