@travetto/web-rpc 8.0.0-alpha.15 → 8.0.0-alpha.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/web-rpc",
3
- "version": "8.0.0-alpha.15",
3
+ "version": "8.0.0-alpha.17",
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.13",
31
- "@travetto/schema": "^8.0.0-alpha.13",
32
- "@travetto/web": "^8.0.0-alpha.14"
30
+ "@travetto/config": "^8.0.0-alpha.15",
31
+ "@travetto/schema": "^8.0.0-alpha.15",
32
+ "@travetto/web": "^8.0.0-alpha.16"
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
  */
@@ -3,13 +3,21 @@ 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;
8
+ const isBlobMap = (value: unknown): value is Record<string, Blob> => {
9
+ if (typeof value !== 'object' || value === null) {
10
+ return false;
11
+ }
12
+ for (const [key, item] of Object.entries(value)) {
13
+ if (!(item instanceof Blob) || typeof key !== 'string') {
14
+ return false;
15
+ }
16
+ }
17
+ return true;
18
+ };
10
19
 
11
- // 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);
20
+ const isBlobLike = (value: unknown): value is Record<string, Blob> | Blob => value instanceof Blob || isBlobMap(value);
13
21
 
14
22
  const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, string>): Headers => {
15
23
  const headers = new Headers(base);
@@ -18,10 +26,10 @@ const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, strin
18
26
  };
19
27
 
20
28
  const jsonToString = (input: unknown): string =>
21
- JSON.stringify(input, (key, value): unknown => typeof value === 'bigint' ? `${value.toString()}n` : value);
29
+ JSON.stringify(input, (_, value): unknown => typeof value === 'bigint' ? `${value.toString()}n` : value);
22
30
 
23
31
  const stringToJson = <T = unknown>(input: string): T =>
24
- JSON.parse(input, (key, value): unknown => {
32
+ JSON.parse(input, (_, value): unknown => {
25
33
  if (typeof value === 'string') {
26
34
  if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.]\d{3}Z$/.test(value)) {
27
35
  return new Date(value);
@@ -33,16 +41,17 @@ const stringToJson = <T = unknown>(input: string): T =>
33
41
  });
34
42
 
35
43
 
36
- export type PreRequestHandler = (item: RequestInit) => Promise<RequestInit | undefined | void>;
44
+ export type PreRequestHandler = (item: RequestInit) => Promise<RpcRequest['core'] | undefined | void>;
37
45
  export type PostResponseHandler = (item: Response) => Promise<Response | undefined | void>;
38
46
 
39
47
  export type RpcRequest = {
40
- core?: Partial<RequestInit> & {
48
+ core: Omit<Partial<RequestInit>, 'method'> & {
41
49
  timeout?: number;
42
50
  retriesOnConnectFailure?: number;
43
- path?: string;
44
- controller?: string;
45
- endpoint?: string;
51
+ path: string;
52
+ controller: string;
53
+ endpoint: string;
54
+ method: string;
46
55
  };
47
56
  url: URL | string;
48
57
  consumeJSON?: <T>(text?: unknown) => (T | Promise<T>);
@@ -57,7 +66,7 @@ export type RpcClient<T extends Record<string, {}>, E extends Record<string, Fun
57
66
 
58
67
  export type RpcClientFactory<T extends Record<string, {}>> =
59
68
  <R extends Record<string, Function>>(
60
- baseOpts: RpcRequest,
69
+ baseOpts: Omit<Partial<RpcRequest>, 'url'> & { url: RpcRequest['url'] },
61
70
  decorate?: (request: RpcRequest) => R
62
71
  ) => RpcClient<T, R>;
63
72
 
@@ -87,28 +96,49 @@ function registerTimeout<T>(
87
96
  }
88
97
 
89
98
  function buildRequest<T extends RequestInit>(base: T, controller: string, endpoint: string): T {
99
+ let verb: string;
100
+ switch (endpoint.split(/[A-Z]/)[0]) {
101
+ case 'get': verb = 'GET'; break;
102
+ case 'update': verb = 'PUT'; break;
103
+ case 'delete': verb = 'DELETE'; break;
104
+ case 'create': default: verb = 'POST'; break;
105
+ }
90
106
  return {
91
107
  ...base,
92
- method: 'POST',
108
+ method: verb,
93
109
  path: `${controller}:${endpoint}`,
94
110
  controller,
95
111
  endpoint
96
112
  };
97
113
  }
98
114
 
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
- };
115
+ export function getBody(inputs: unknown[], method: string): {
116
+ body: FormData | string | undefined;
117
+ query: Record<string, string> | undefined;
118
+ method: string;
119
+ headers: Record<string, string>;
120
+ } {
121
+ let outMethod = method;
122
+ if (method.toLowerCase() === 'get' || method.toLowerCase() === 'delete') {
123
+ const inputComputed = btoa(encodeURIComponent(jsonToString(inputs)));
124
+ if (inputComputed.length < 1000) {
125
+ return {
126
+ body: undefined,
127
+ method: outMethod,
128
+ query: { TRV_RPC_INPUTS: inputComputed },
129
+ headers: {}
130
+ };
131
+ } else {
132
+ outMethod = 'POST';
133
+ }
107
134
  }
135
+
108
136
  // If we do not have a blob, simple output
109
137
  if (!inputs.some(isBlobLike)) {
110
138
  return {
111
139
  body: jsonToString(inputs),
140
+ query: undefined,
141
+ method: outMethod,
112
142
  headers: {
113
143
  'Content-Type': 'application/json'
114
144
  }
@@ -130,6 +160,8 @@ export function getBody(inputs: unknown[], isBodyRequest: boolean): { body: Form
130
160
 
131
161
  return {
132
162
  body: form,
163
+ query: undefined,
164
+ method: outMethod,
133
165
  headers: {
134
166
  'X-TRV-RPC-INPUTS': btoa(encodeURIComponent(jsonToString(plainInputs)))
135
167
  }
@@ -169,20 +201,16 @@ export async function consumeError(error: unknown): Promise<Error> {
169
201
  }
170
202
 
171
203
  export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]): Promise<T> {
172
- let core = request.core!;
204
+ let core = { ...request.core };
173
205
 
174
206
  try {
175
- const { body, headers } = getBody(params, /^(post|put|patch)$/i.test(request.core?.method ?? 'POST'));
176
- if (body) {
177
- core.body = body;
178
- }
207
+ const { method, body, headers, query } = getBody(params, request.core.method);
208
+ if (body) { core.body = body; }
209
+ core.method = method;
179
210
  core.headers = extendHeaders(core.headers, headers);
180
211
 
181
212
  for (const fn of request.preRequestHandlers ?? []) {
182
- const computed = await fn(core);
183
- if (computed) {
184
- core = computed;
185
- }
213
+ core = (await fn(core)) ?? core;
186
214
  }
187
215
 
188
216
  const signals = [];
@@ -202,8 +230,9 @@ export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]):
202
230
  }
203
231
 
204
232
  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('//', '/');
233
+ url.pathname = `${url.pathname}/${request.core.path || '/'}`.replaceAll('//', '/');
234
+ for (const [key, value] of Object.entries(query ?? {})) {
235
+ url.searchParams.append(key, value);
207
236
  }
208
237
 
209
238
  let resolved: Response | undefined;
@@ -265,7 +294,12 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
265
294
  consumeJSON,
266
295
  consumeError,
267
296
  ...request,
268
- core: { timeout: 0, credentials: 'include', mode: 'cors', ...request.core },
297
+ core: {
298
+ method: 'POST',
299
+ path: undefined!, controller: undefined!, endpoint: undefined!,
300
+ timeout: 0, credentials: 'include', mode: 'cors',
301
+ ...request.core
302
+ },
269
303
  };
270
304
  const cache: Record<string, unknown> = {};
271
305
  // @ts-ignore
@@ -292,7 +326,7 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
292
326
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
293
327
  export function withConfigFactoryDecorator(request: RpcRequest) {
294
328
  return {
295
- withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<PromiseRes<V>> {
329
+ withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<PromiseResult<V>> {
296
330
  return invokeFetch({ ...request, core: { ...request.core, ...extra } }, ...params);
297
331
  }
298
332
  };