@travetto/web-rpc 8.0.0-alpha.2 → 8.0.0-alpha.22
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 +9 -4
- package/package.json +4 -4
- package/src/controller.ts +18 -3
- package/support/cli.web_rpc-client.ts +4 -1
- package/support/client/rpc.ts +84 -34
package/README.md
CHANGED
|
@@ -16,21 +16,26 @@ yarn add @travetto/web-rpc
|
|
|
16
16
|
This module allows for a highly focused scenario, of supporting RPC operations within a [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") application. The module takes care of producing the appropriate handler for the RPC commands along with the ability to generate the appropriate client to be used to interact with the RPC functionality. The generated client uses Proxy-based objects, along with [Typescript](https://typescriptlang.org) magic to create a dynamic client that does not rely on generating a lot of code.
|
|
17
17
|
|
|
18
18
|
## CLI - web:rpc-client
|
|
19
|
-
|
|
19
|
+
Generate web-rpc client artifacts from a specified provider or leveraging local config.
|
|
20
20
|
|
|
21
|
-
**Terminal:
|
|
21
|
+
**Terminal: Help for web:rpc-client**
|
|
22
22
|
```bash
|
|
23
23
|
$ trv web:rpc-client --help
|
|
24
24
|
|
|
25
25
|
Usage: web:rpc-client [options] <type:config|node|web> [output:string]
|
|
26
26
|
|
|
27
|
+
Generate web-rpc client artifacts from a specified provider or leveraging local config.
|
|
28
|
+
|
|
29
|
+
Resolves generator services from DI and writes typed client code for the
|
|
30
|
+
selected provider target.
|
|
31
|
+
|
|
27
32
|
Options:
|
|
28
33
|
-p, --profile <string> Application profiles
|
|
29
34
|
-m, --module <module> Module to run for
|
|
30
|
-
|
|
35
|
+
--help display help for command
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
### Example
|
|
34
39
|
|
|
35
40
|
**Code: Example Controller**
|
|
36
41
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web-rpc",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RPC support for a Web Application",
|
|
6
6
|
"keywords": [
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"directory": "module/web-rpc"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/schema": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/web": "^8.0.0-alpha.
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.18",
|
|
31
|
+
"@travetto/schema": "^8.0.0-alpha.18",
|
|
32
|
+
"@travetto/web": "^8.0.0-alpha.19"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
|
35
35
|
"displayName": "Web RPC Support"
|
package/src/controller.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { IsPrivate } from '@travetto/schema';
|
|
|
4
4
|
import {
|
|
5
5
|
HeaderParam, Controller, ExcludeInterceptors, ControllerRegistryIndex,
|
|
6
6
|
type WebAsyncContext, Body, EndpointUtil, BodyInterceptor, Post, WebCommonUtil,
|
|
7
|
-
RespondInterceptor, DecompressInterceptor, Get
|
|
7
|
+
RespondInterceptor, DecompressInterceptor, Get, QueryParam, Delete, Put, Patch
|
|
8
8
|
} from '@travetto/web';
|
|
9
9
|
|
|
10
10
|
@Controller('/rpc')
|
|
@@ -21,13 +21,28 @@ export class WebRpcController {
|
|
|
21
21
|
ctx: WebAsyncContext;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Allow for
|
|
24
|
+
* Allow for extra method-based requests
|
|
25
25
|
*/
|
|
26
26
|
@Get('/:target')
|
|
27
|
-
|
|
27
|
+
onGetRequest(target: string, @QueryParam('TRV_RPC_INPUTS') paramInput?: string): Promise<unknown> {
|
|
28
28
|
return this.onRequest(target, paramInput);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
@Delete('/:target')
|
|
32
|
+
onDeleteRequest(target: string, @QueryParam('TRV_RPC_INPUTS') paramInput?: string): Promise<unknown> {
|
|
33
|
+
return this.onRequest(target, paramInput);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Put('/:target')
|
|
37
|
+
onPutRequest(target: string, @HeaderParam('X-TRV-RPC-INPUTS') paramInput?: string, @Body() body?: Any): Promise<unknown> {
|
|
38
|
+
return this.onRequest(target, paramInput, body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Patch('/:target')
|
|
42
|
+
onPatchRequest(target: string, @HeaderParam('X-TRV-RPC-INPUTS') paramInput?: string, @Body() body?: Any): Promise<unknown> {
|
|
43
|
+
return this.onRequest(target, paramInput, body);
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* RPC main entrypoint
|
|
33
48
|
*/
|
|
@@ -10,7 +10,10 @@ import type { WebRpcClient } from '../src/config.ts';
|
|
|
10
10
|
import { WebRpcClientGeneratorService } from '../src/service.ts';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Generate
|
|
13
|
+
* Generate web-rpc client artifacts from a specified provider or leveraging local config.
|
|
14
|
+
*
|
|
15
|
+
* Resolves generator services from DI and writes typed client code for the
|
|
16
|
+
* selected provider target.
|
|
14
17
|
*/
|
|
15
18
|
@CliCommand()
|
|
16
19
|
export class CliWebRpcCommand implements CliCommandShape {
|
package/support/client/rpc.ts
CHANGED
|
@@ -3,13 +3,32 @@ type MethodKeys<C extends {}> = {
|
|
|
3
3
|
}[keyof C];
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
5
|
type PromiseFn = (...args: any) => Promise<unknown>;
|
|
6
|
-
type
|
|
6
|
+
type PromiseResult<V extends PromiseFn> = Awaited<ReturnType<V>>;
|
|
7
7
|
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
-
const isBlobMap = (value: any): value is Record<string, Blob> => value && typeof value === 'object' && value[Object.keys(value)[0]] instanceof Blob;
|
|
10
8
|
|
|
11
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
-
const
|
|
10
|
+
const triggerBrowserRedirect = (world: any, url: URL): boolean => {
|
|
11
|
+
const location = world?.window?.location;
|
|
12
|
+
if (location) {
|
|
13
|
+
location.href = url.toString();
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isBlobMap = (value: unknown): value is Record<string, Blob> => {
|
|
20
|
+
if (typeof value !== 'object' || value === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
for (const [key, item] of Object.entries(value)) {
|
|
24
|
+
if (!(item instanceof Blob) || typeof key !== 'string') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isBlobLike = (value: unknown): value is Record<string, Blob> | Blob => value instanceof Blob || isBlobMap(value);
|
|
13
32
|
|
|
14
33
|
const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, string>): Headers => {
|
|
15
34
|
const headers = new Headers(base);
|
|
@@ -18,10 +37,10 @@ const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, strin
|
|
|
18
37
|
};
|
|
19
38
|
|
|
20
39
|
const jsonToString = (input: unknown): string =>
|
|
21
|
-
JSON.stringify(input, (
|
|
40
|
+
JSON.stringify(input, (_, value): unknown => typeof value === 'bigint' ? `${value.toString()}n` : value);
|
|
22
41
|
|
|
23
42
|
const stringToJson = <T = unknown>(input: string): T =>
|
|
24
|
-
JSON.parse(input, (
|
|
43
|
+
JSON.parse(input, (_, value): unknown => {
|
|
25
44
|
if (typeof value === 'string') {
|
|
26
45
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.]\d{3}Z$/.test(value)) {
|
|
27
46
|
return new Date(value);
|
|
@@ -33,16 +52,18 @@ const stringToJson = <T = unknown>(input: string): T =>
|
|
|
33
52
|
});
|
|
34
53
|
|
|
35
54
|
|
|
36
|
-
export type PreRequestHandler = (item: RequestInit) => Promise<
|
|
55
|
+
export type PreRequestHandler = (item: RequestInit) => Promise<RpcRequest['core'] | undefined | void>;
|
|
37
56
|
export type PostResponseHandler = (item: Response) => Promise<Response | undefined | void>;
|
|
38
57
|
|
|
39
58
|
export type RpcRequest = {
|
|
40
|
-
core
|
|
59
|
+
core: Omit<Partial<RequestInit>, 'method'> & {
|
|
41
60
|
timeout?: number;
|
|
42
61
|
retriesOnConnectFailure?: number;
|
|
43
|
-
path
|
|
44
|
-
controller
|
|
45
|
-
endpoint
|
|
62
|
+
path: string;
|
|
63
|
+
controller: string;
|
|
64
|
+
endpoint: string;
|
|
65
|
+
method: string;
|
|
66
|
+
browserRedirect?: boolean;
|
|
46
67
|
};
|
|
47
68
|
url: URL | string;
|
|
48
69
|
consumeJSON?: <T>(text?: unknown) => (T | Promise<T>);
|
|
@@ -57,7 +78,7 @@ export type RpcClient<T extends Record<string, {}>, E extends Record<string, Fun
|
|
|
57
78
|
|
|
58
79
|
export type RpcClientFactory<T extends Record<string, {}>> =
|
|
59
80
|
<R extends Record<string, Function>>(
|
|
60
|
-
baseOpts: RpcRequest,
|
|
81
|
+
baseOpts: Omit<Partial<RpcRequest>, 'url'> & { url: RpcRequest['url'] },
|
|
61
82
|
decorate?: (request: RpcRequest) => R
|
|
62
83
|
) => RpcClient<T, R>;
|
|
63
84
|
|
|
@@ -87,28 +108,49 @@ function registerTimeout<T>(
|
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
function buildRequest<T extends RequestInit>(base: T, controller: string, endpoint: string): T {
|
|
111
|
+
let verb: string;
|
|
112
|
+
switch (endpoint.split(/[A-Z]/)[0]) {
|
|
113
|
+
case 'get': verb = 'GET'; break;
|
|
114
|
+
case 'update': verb = 'PUT'; break;
|
|
115
|
+
case 'delete': verb = 'DELETE'; break;
|
|
116
|
+
case 'create': default: verb = 'POST'; break;
|
|
117
|
+
}
|
|
90
118
|
return {
|
|
91
119
|
...base,
|
|
92
|
-
method:
|
|
120
|
+
method: verb,
|
|
93
121
|
path: `${controller}:${endpoint}`,
|
|
94
122
|
controller,
|
|
95
123
|
endpoint
|
|
96
124
|
};
|
|
97
125
|
}
|
|
98
126
|
|
|
99
|
-
export function getBody(inputs: unknown[],
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
export function getBody(inputs: unknown[], method: string): {
|
|
128
|
+
body: FormData | string | undefined;
|
|
129
|
+
query: Record<string, string> | undefined;
|
|
130
|
+
method: string;
|
|
131
|
+
headers: Record<string, string>;
|
|
132
|
+
} {
|
|
133
|
+
let outMethod = method;
|
|
134
|
+
if (method.toLowerCase() === 'get' || method.toLowerCase() === 'delete') {
|
|
135
|
+
const inputComputed = btoa(encodeURIComponent(jsonToString(inputs)));
|
|
136
|
+
if (inputComputed.length < 1000) {
|
|
137
|
+
return {
|
|
138
|
+
body: undefined,
|
|
139
|
+
method: outMethod,
|
|
140
|
+
query: { TRV_RPC_INPUTS: inputComputed },
|
|
141
|
+
headers: {}
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
outMethod = 'POST';
|
|
145
|
+
}
|
|
107
146
|
}
|
|
147
|
+
|
|
108
148
|
// If we do not have a blob, simple output
|
|
109
149
|
if (!inputs.some(isBlobLike)) {
|
|
110
150
|
return {
|
|
111
151
|
body: jsonToString(inputs),
|
|
152
|
+
query: undefined,
|
|
153
|
+
method: outMethod,
|
|
112
154
|
headers: {
|
|
113
155
|
'Content-Type': 'application/json'
|
|
114
156
|
}
|
|
@@ -130,6 +172,8 @@ export function getBody(inputs: unknown[], isBodyRequest: boolean): { body: Form
|
|
|
130
172
|
|
|
131
173
|
return {
|
|
132
174
|
body: form,
|
|
175
|
+
query: undefined,
|
|
176
|
+
method: outMethod,
|
|
133
177
|
headers: {
|
|
134
178
|
'X-TRV-RPC-INPUTS': btoa(encodeURIComponent(jsonToString(plainInputs)))
|
|
135
179
|
}
|
|
@@ -169,20 +213,16 @@ export async function consumeError(error: unknown): Promise<Error> {
|
|
|
169
213
|
}
|
|
170
214
|
|
|
171
215
|
export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]): Promise<T> {
|
|
172
|
-
let core = request.core
|
|
216
|
+
let core = { ...request.core };
|
|
173
217
|
|
|
174
218
|
try {
|
|
175
|
-
const { body, headers } = getBody(params,
|
|
176
|
-
if (body) {
|
|
177
|
-
|
|
178
|
-
}
|
|
219
|
+
const { method, body, headers, query } = getBody(params, request.core.method);
|
|
220
|
+
if (body) { core.body = body; }
|
|
221
|
+
core.method = method;
|
|
179
222
|
core.headers = extendHeaders(core.headers, headers);
|
|
180
223
|
|
|
181
224
|
for (const fn of request.preRequestHandlers ?? []) {
|
|
182
|
-
|
|
183
|
-
if (computed) {
|
|
184
|
-
core = computed;
|
|
185
|
-
}
|
|
225
|
+
core = (await fn(core)) ?? core;
|
|
186
226
|
}
|
|
187
227
|
|
|
188
228
|
const signals = [];
|
|
@@ -202,8 +242,13 @@ export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]):
|
|
|
202
242
|
}
|
|
203
243
|
|
|
204
244
|
const url = typeof request.url === 'string' ? new URL(request.url) : request.url;
|
|
205
|
-
|
|
206
|
-
|
|
245
|
+
url.pathname = `${url.pathname}/${request.core.path || '/'}`.replaceAll('//', '/');
|
|
246
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
247
|
+
url.searchParams.append(key, value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (core.browserRedirect && triggerBrowserRedirect(globalThis, url)) {
|
|
251
|
+
return Promise.resolve(undefined!);
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
let resolved: Response | undefined;
|
|
@@ -265,7 +310,12 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
|
|
|
265
310
|
consumeJSON,
|
|
266
311
|
consumeError,
|
|
267
312
|
...request,
|
|
268
|
-
core: {
|
|
313
|
+
core: {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
path: undefined!, controller: undefined!, endpoint: undefined!,
|
|
316
|
+
timeout: 0, credentials: 'include', mode: 'cors',
|
|
317
|
+
...request.core
|
|
318
|
+
},
|
|
269
319
|
};
|
|
270
320
|
const cache: Record<string, unknown> = {};
|
|
271
321
|
// @ts-ignore
|
|
@@ -292,7 +342,7 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
|
|
|
292
342
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
293
343
|
export function withConfigFactoryDecorator(request: RpcRequest) {
|
|
294
344
|
return {
|
|
295
|
-
withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<
|
|
345
|
+
withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<PromiseResult<V>> {
|
|
296
346
|
return invokeFetch({ ...request, core: { ...request.core, ...extra } }, ...params);
|
|
297
347
|
}
|
|
298
348
|
};
|