@travetto/web-rpc 8.0.0-alpha.2 → 8.0.0-alpha.21

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
@@ -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
- The library will create the RPC client in one of three flavors: fetch, fetch + node, angular.
19
+ Generate web-rpc client artifacts from a specified provider or leveraging local config.
20
20
 
21
- **Terminal: Command Service**
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
- -h, --help display help for command
35
+ --help display help for command
31
36
  ```
32
37
 
33
- ## Example
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.2",
3
+ "version": "8.0.0-alpha.21",
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.1",
31
- "@travetto/schema": "^8.0.0-alpha.1",
32
- "@travetto/web": "^8.0.0-alpha.1"
30
+ "@travetto/config": "^8.0.0-alpha.17",
31
+ "@travetto/schema": "^8.0.0-alpha.17",
32
+ "@travetto/web": "^8.0.0-alpha.18"
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 get-based requests
24
+ * Allow for extra method-based requests
25
25
  */
26
26
  @Get('/:target')
27
- async onGetRequest(target: string, @HeaderParam('X-TRV-RPC-INPUTS') paramInput?: string): Promise<unknown> {
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 the web-rpc client
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 {
@@ -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 PromiseRes<V extends PromiseFn> = Awaited<ReturnType<V>>;
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 isBlobLike = (value: any): value is Record<string, Blob> | Blob => value instanceof Blob || isBlobMap(value);
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, (key, value): unknown => typeof value === 'bigint' ? `${value.toString()}n` : value);
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, (key, value): unknown => {
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<RequestInit | undefined | void>;
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?: Partial<RequestInit> & {
59
+ core: Omit<Partial<RequestInit>, 'method'> & {
41
60
  timeout?: number;
42
61
  retriesOnConnectFailure?: number;
43
- path?: string;
44
- controller?: string;
45
- endpoint?: string;
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: 'POST',
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[], isBodyRequest: boolean): { body: FormData | string | undefined, headers: Record<string, string> } {
100
- if (!isBodyRequest) {
101
- return {
102
- body: undefined,
103
- headers: {
104
- 'X-TRV-RPC-INPUTS': btoa(encodeURIComponent(jsonToString(inputs)))
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, /^(post|put|patch)$/i.test(request.core?.method ?? 'POST'));
176
- if (body) {
177
- core.body = body;
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
- const computed = await fn(core);
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
- if (request.core?.path) {
206
- url.pathname = `${url.pathname}/${request.core.path}`.replaceAll('//', '/');
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: { timeout: 0, credentials: 'include', mode: 'cors', ...request.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<PromiseRes<V>> {
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
  };