@seam-rpc/client 2.2.0 → 3.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/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 declare function setApiUrl(url: string): void;
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
- let apiUrl = null;
4
- export function setApiUrl(url) {
5
- apiUrl = url;
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 (!apiUrl)
9
- throw new Error("Missing API URL");
10
- let req;
11
- const { json, files, paths } = extractFiles(args);
12
- if (files.length > 0) {
13
- const formData = new FormData();
14
- formData.append("json", JSON.stringify(json));
15
- formData.append("paths", JSON.stringify(paths));
16
- for (let i = 0; i < files.length; i++) {
17
- const file = files[i];
18
- const blob = new Blob([new Uint8Array(file.data)], {
19
- type: file.mimeType || "application/octet-stream",
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,7 +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
  }
78
- // console.log(JSON.stringify(extractFiles(["John", new SeamFile(new Uint8Array([1, 2, 3, 4]))]), null, 4))
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seam-rpc/client",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "main": "./dist/index.js",
5
5
  "type": "module",
6
6
  "files": [