@seam-rpc/client 2.2.1 → 3.0.1
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 +103 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +69 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,34 @@ export async function getUser(id: string): Promise<User | undefined> {
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
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
|
+
|
|
64
92
|
### Client
|
|
65
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`.
|
|
66
94
|
|
|
@@ -101,6 +129,13 @@ export function createUser(name: string): Promise<string> { return callApi("user
|
|
|
101
129
|
export function getUser(id: string): Promise<User | undefined> { return callApi("users", "getUser", [id]); }
|
|
102
130
|
```
|
|
103
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
|
+
|
|
104
139
|
### Config file
|
|
105
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:
|
|
106
141
|
```json
|
|
@@ -180,4 +215,71 @@ export async function createUser(name: string, context: SeamContext): Promise<st
|
|
|
180
215
|
```
|
|
181
216
|
|
|
182
217
|
### Client
|
|
183
|
-
The client currently doesn't support access to the response object from the fetch.
|
|
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { SeamFile, ISeamFile } from "@seam-rpc/core";
|
|
2
2
|
export { SeamFile, ISeamFile };
|
|
3
|
-
export
|
|
3
|
+
export type SeamRequestMiddleware = (context: SeamRequestMiddlewareContext) => void | Promise<void>;
|
|
4
|
+
export type SeamResponseMiddleware = (context: SeamResponseMiddlewareContext) => void | Promise<void>;
|
|
5
|
+
export interface SeamRequestMiddlewareContext {
|
|
6
|
+
request: RequestInit;
|
|
7
|
+
routerName: string;
|
|
8
|
+
funcName: string;
|
|
9
|
+
args: any[];
|
|
10
|
+
}
|
|
11
|
+
export type SeamResponseMiddlewareContext = SeamRequestMiddlewareContext & {
|
|
12
|
+
response: Response;
|
|
13
|
+
parsedResponse: any;
|
|
14
|
+
};
|
|
15
|
+
export interface SeamClientOptions {
|
|
16
|
+
middleware?: {
|
|
17
|
+
request?: SeamRequestMiddleware[];
|
|
18
|
+
response?: SeamResponseMiddleware[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare class SeamClient {
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
readonly options?: SeamClientOptions | undefined;
|
|
24
|
+
static _instance: SeamClient;
|
|
25
|
+
constructor(baseUrl: string, options?: SeamClientOptions | undefined);
|
|
26
|
+
preRequest(middleware: SeamRequestMiddleware): void;
|
|
27
|
+
postRequest(middleware: SeamResponseMiddleware): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function createClient(baseUrl: string, options?: SeamClientOptions): SeamClient;
|
|
4
30
|
export declare function callApi(routerName: string, funcName: string, args: any[]): Promise<any>;
|
package/dist/index.js
CHANGED
|
@@ -1,40 +1,37 @@
|
|
|
1
1
|
import { SeamFile, extractFiles, injectFiles } from "@seam-rpc/core";
|
|
2
2
|
export { SeamFile };
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export class SeamClient {
|
|
4
|
+
constructor(baseUrl, options) {
|
|
5
|
+
this.baseUrl = baseUrl;
|
|
6
|
+
this.options = options;
|
|
7
|
+
SeamClient._instance = this;
|
|
8
|
+
}
|
|
9
|
+
preRequest(middleware) {
|
|
10
|
+
this.options?.middleware?.request?.push(middleware);
|
|
11
|
+
}
|
|
12
|
+
postRequest(middleware) {
|
|
13
|
+
this.options?.middleware?.response?.push(middleware);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createClient(baseUrl, options) {
|
|
17
|
+
return new SeamClient(baseUrl, options);
|
|
6
18
|
}
|
|
7
19
|
export async function callApi(routerName, funcName, args) {
|
|
8
|
-
if (!
|
|
9
|
-
throw new Error("
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
if (!SeamClient._instance)
|
|
21
|
+
throw new Error("Seam Client not instantiated.");
|
|
22
|
+
const seamClient = SeamClient._instance;
|
|
23
|
+
const req = buildRequest(args);
|
|
24
|
+
const url = `${seamClient.baseUrl}/${routerName}/${funcName}`;
|
|
25
|
+
if (seamClient.options?.middleware?.request) {
|
|
26
|
+
for (const mw of seamClient.options.middleware.request) {
|
|
27
|
+
await mw({
|
|
28
|
+
request: req,
|
|
29
|
+
routerName,
|
|
30
|
+
funcName,
|
|
31
|
+
args,
|
|
20
32
|
});
|
|
21
|
-
formData.append(`file-${i}`, blob, file.fileName || `file-${i}`);
|
|
22
33
|
}
|
|
23
|
-
req = {
|
|
24
|
-
method: "POST",
|
|
25
|
-
body: formData,
|
|
26
|
-
};
|
|
27
34
|
}
|
|
28
|
-
else {
|
|
29
|
-
req = {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers: {
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
},
|
|
34
|
-
body: JSON.stringify(args),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
const url = `${apiUrl}/${routerName}/${funcName}`;
|
|
38
35
|
let res;
|
|
39
36
|
try {
|
|
40
37
|
res = await fetch(url, req);
|
|
@@ -72,6 +69,48 @@ export async function callApi(routerName, funcName, args) {
|
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
injectFiles(jsonPart, responseFiles);
|
|
72
|
+
if (seamClient.options?.middleware?.response) {
|
|
73
|
+
for (const mw of seamClient.options.middleware.response) {
|
|
74
|
+
await mw({
|
|
75
|
+
request: req,
|
|
76
|
+
response: res,
|
|
77
|
+
parsedResponse: jsonPart.result,
|
|
78
|
+
routerName,
|
|
79
|
+
funcName,
|
|
80
|
+
args,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
75
84
|
return jsonPart.result;
|
|
76
85
|
}
|
|
77
86
|
}
|
|
87
|
+
function buildRequest(args) {
|
|
88
|
+
let req;
|
|
89
|
+
const { json, files, paths } = extractFiles(args);
|
|
90
|
+
if (files.length > 0) {
|
|
91
|
+
const formData = new FormData();
|
|
92
|
+
formData.append("json", JSON.stringify(json));
|
|
93
|
+
formData.append("paths", JSON.stringify(paths));
|
|
94
|
+
for (let i = 0; i < files.length; i++) {
|
|
95
|
+
const file = files[i];
|
|
96
|
+
const blob = new Blob([new Uint8Array(file.data)], {
|
|
97
|
+
type: file.mimeType || "application/octet-stream",
|
|
98
|
+
});
|
|
99
|
+
formData.append(`file-${i}`, blob, file.fileName || `file-${i}`);
|
|
100
|
+
}
|
|
101
|
+
req = {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: formData,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
req = {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify(args),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return req;
|
|
116
|
+
}
|