@whook/gcp-functions 8.3.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.
Files changed (45) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +187 -0
  3. package/dist/commands/testHTTPFunction.d.ts +12 -0
  4. package/dist/commands/testHTTPFunction.js +156 -0
  5. package/dist/commands/testHTTPFunction.js.map +1 -0
  6. package/dist/commands/testHTTPFunction.mjs +136 -0
  7. package/dist/commands/testHTTPFunction.mjs.map +1 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.js +299 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/index.mjs +265 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/dist/libs/utils.d.ts +5 -0
  14. package/dist/libs/utils.js +39 -0
  15. package/dist/libs/utils.js.map +1 -0
  16. package/dist/libs/utils.mjs +27 -0
  17. package/dist/libs/utils.mjs.map +1 -0
  18. package/dist/services/_autoload.d.ts +17 -0
  19. package/dist/services/_autoload.js +121 -0
  20. package/dist/services/_autoload.js.map +1 -0
  21. package/dist/services/_autoload.mjs +107 -0
  22. package/dist/services/_autoload.mjs.map +1 -0
  23. package/dist/services/log.d.ts +2 -0
  24. package/dist/services/log.js +14 -0
  25. package/dist/services/log.js.map +1 -0
  26. package/dist/services/log.mjs +4 -0
  27. package/dist/services/log.mjs.map +1 -0
  28. package/dist/services/log.test.d.ts +1 -0
  29. package/dist/services/log.test.js +12 -0
  30. package/dist/services/log.test.js.map +1 -0
  31. package/dist/services/log.test.mjs +7 -0
  32. package/dist/services/log.test.mjs.map +1 -0
  33. package/dist/wrappers/googleHTTPFunction.d.ts +22 -0
  34. package/dist/wrappers/googleHTTPFunction.js +310 -0
  35. package/dist/wrappers/googleHTTPFunction.js.map +1 -0
  36. package/dist/wrappers/googleHTTPFunction.mjs +289 -0
  37. package/dist/wrappers/googleHTTPFunction.mjs.map +1 -0
  38. package/package.json +215 -0
  39. package/src/commands/testHTTPFunction.ts +181 -0
  40. package/src/index.ts +443 -0
  41. package/src/libs/utils.ts +43 -0
  42. package/src/services/_autoload.ts +161 -0
  43. package/src/services/log.test.ts +7 -0
  44. package/src/services/log.ts +4 -0
  45. package/src/wrappers/googleHTTPFunction.ts +468 -0
@@ -0,0 +1,468 @@
1
+ import {
2
+ DEFAULT_DEBUG_NODE_ENVS,
3
+ DEFAULT_BUFFER_LIMIT,
4
+ DEFAULT_PARSERS,
5
+ DEFAULT_STRINGIFYERS,
6
+ DEFAULT_DECODERS,
7
+ DEFAULT_ENCODERS,
8
+ extractOperationSecurityParameters,
9
+ castParameters,
10
+ } from '@whook/http-router';
11
+ import { reuseSpecialProps, alsoInject } from 'knifecycle';
12
+ import Ajv from 'ajv';
13
+ import addAJVFormats from 'ajv-formats';
14
+ import bytes from 'bytes';
15
+ import HTTPError from 'yhttperror';
16
+ import {
17
+ prepareParametersValidators,
18
+ prepareBodyValidator,
19
+ applyValidators,
20
+ filterHeaders,
21
+ extractBodySpec,
22
+ extractResponseSpec,
23
+ checkResponseCharset,
24
+ checkResponseMediaType,
25
+ executeHandler,
26
+ extractProduceableMediaTypes,
27
+ extractConsumableMediaTypes,
28
+ getBody,
29
+ sendBody,
30
+ } from '@whook/http-router';
31
+ import { noop, compose, identity } from '@whook/whook';
32
+ import { lowerCaseHeaders } from '@whook/cors';
33
+ import stream from 'stream';
34
+ import type { WhookQueryStringParser } from '@whook/http-router';
35
+ import type { ServiceInitializer, Dependencies, Service } from 'knifecycle';
36
+ import type {
37
+ WhookRequest,
38
+ WhookResponse,
39
+ WhookHandler,
40
+ ObfuscatorService,
41
+ WhookOperation,
42
+ WhookWrapper,
43
+ } from '@whook/whook';
44
+ import type { TimeService, LogService } from 'common-services';
45
+ import type { OpenAPIV3 } from 'openapi-types';
46
+ import type { Readable } from 'stream';
47
+ import type { CORSConfig } from '@whook/cors';
48
+
49
+ type HTTPWrapperDependencies = {
50
+ NODE_ENV: string;
51
+ DEBUG_NODE_ENVS?: string[];
52
+ OPERATION: WhookOperation;
53
+ DECODERS?: typeof DEFAULT_DECODERS;
54
+ ENCODERS?: typeof DEFAULT_ENCODERS;
55
+ PARSERS?: typeof DEFAULT_PARSERS;
56
+ STRINGIFYERS?: typeof DEFAULT_STRINGIFYERS;
57
+ QUERY_PARSER: WhookQueryStringParser;
58
+ BUFFER_LIMIT?: string;
59
+ obfuscator: ObfuscatorService;
60
+ time?: TimeService;
61
+ log?: LogService;
62
+ WRAPPERS: WhookWrapper<Dependencies, Service>[];
63
+ };
64
+
65
+ const SEARCH_SEPARATOR = '?';
66
+ const PATH_SEPARATOR = '/';
67
+
68
+ export default function wrapHandlerForAWSHTTPFunction<
69
+ D,
70
+ S extends WhookHandler,
71
+ >(
72
+ initHandler: ServiceInitializer<D, S>,
73
+ ): ServiceInitializer<D & HTTPWrapperDependencies, S> {
74
+ return alsoInject<HTTPWrapperDependencies, D, S>(
75
+ [
76
+ 'OPERATION_API',
77
+ 'WRAPPERS',
78
+ '?DEBUG_NODE_ENVS',
79
+ 'NODE_ENV',
80
+ '?DECODERS',
81
+ '?ENCODERS',
82
+ '?PARSERS',
83
+ '?STRINGIFYERS',
84
+ '?BUFFER_LIMIT',
85
+ 'QUERY_PARSER',
86
+ 'obfuscator',
87
+ '?log',
88
+ '?time',
89
+ ],
90
+ reuseSpecialProps(
91
+ initHandler,
92
+ initHandlerForAWSHTTPFunction.bind(
93
+ null,
94
+ initHandler,
95
+ ) as ServiceInitializer<D, S>,
96
+ ),
97
+ );
98
+ }
99
+
100
+ async function initHandlerForAWSHTTPFunction(
101
+ initHandler: ServiceInitializer<unknown, WhookHandler>,
102
+ {
103
+ OPERATION_API,
104
+ WRAPPERS,
105
+ NODE_ENV,
106
+ DEBUG_NODE_ENVS = DEFAULT_DEBUG_NODE_ENVS,
107
+ DECODERS = DEFAULT_DECODERS,
108
+ ENCODERS = DEFAULT_ENCODERS,
109
+ log = noop,
110
+ time = Date.now.bind(Date),
111
+ ...services
112
+ },
113
+ ) {
114
+ const path = Object.keys(OPERATION_API.paths)[0];
115
+ const method = Object.keys(OPERATION_API.paths[path])[0];
116
+ const OPERATION: WhookOperation = {
117
+ path,
118
+ method,
119
+ ...OPERATION_API.paths[path][method],
120
+ };
121
+ const consumableCharsets = Object.keys(DECODERS);
122
+ const produceableCharsets = Object.keys(ENCODERS);
123
+ const consumableMediaTypes = extractConsumableMediaTypes(OPERATION);
124
+ const produceableMediaTypes = extractProduceableMediaTypes(OPERATION);
125
+ const ajv = new Ajv({
126
+ verbose: DEBUG_NODE_ENVS.includes(NODE_ENV),
127
+ strict: true,
128
+ logger: {
129
+ log: (...args) => log('debug', ...args),
130
+ warn: (...args) => log('warning', ...args),
131
+ error: (...args) => log('error', ...args),
132
+ },
133
+ useDefaults: true,
134
+ coerceTypes: true,
135
+ });
136
+ addAJVFormats(ajv);
137
+ const ammendedParameters = extractOperationSecurityParameters(
138
+ OPERATION_API,
139
+ OPERATION,
140
+ );
141
+ const validators = prepareParametersValidators(
142
+ ajv,
143
+ OPERATION.operationId,
144
+ ((OPERATION.parameters || []) as OpenAPIV3.ParameterObject[]).concat(
145
+ ammendedParameters,
146
+ ),
147
+ );
148
+ const bodyValidator = prepareBodyValidator(ajv, OPERATION);
149
+ const applyWrappers = compose(...WRAPPERS) as WhookWrapper<
150
+ Dependencies,
151
+ Service
152
+ >;
153
+
154
+ const handler = await (
155
+ applyWrappers(initHandler) as ServiceInitializer<Dependencies, Service>
156
+ )({
157
+ OPERATION,
158
+ DEBUG_NODE_ENVS,
159
+ NODE_ENV,
160
+ ...services,
161
+ time,
162
+ log,
163
+ });
164
+
165
+ return handleForAWSHTTPFunction.bind(
166
+ null,
167
+ {
168
+ OPERATION,
169
+ NODE_ENV,
170
+ DEBUG_NODE_ENVS,
171
+ DECODERS,
172
+ ENCODERS,
173
+ log,
174
+ time,
175
+ ...services,
176
+ },
177
+ {
178
+ consumableMediaTypes,
179
+ produceableMediaTypes,
180
+ consumableCharsets,
181
+ produceableCharsets,
182
+ validators,
183
+ bodyValidator,
184
+ ammendedParameters,
185
+ },
186
+ handler,
187
+ );
188
+ }
189
+
190
+ async function handleForAWSHTTPFunction(
191
+ {
192
+ OPERATION,
193
+ DEBUG_NODE_ENVS,
194
+ NODE_ENV,
195
+ ENCODERS,
196
+ DECODERS,
197
+ PARSERS = DEFAULT_PARSERS,
198
+ STRINGIFYERS = DEFAULT_STRINGIFYERS,
199
+ BUFFER_LIMIT = DEFAULT_BUFFER_LIMIT,
200
+ QUERY_PARSER,
201
+ CORS,
202
+ log,
203
+ obfuscator,
204
+ }: HTTPWrapperDependencies & { CORS: CORSConfig },
205
+ {
206
+ consumableMediaTypes,
207
+ produceableMediaTypes,
208
+ consumableCharsets,
209
+ produceableCharsets,
210
+ validators,
211
+ bodyValidator,
212
+ },
213
+ handler: WhookHandler,
214
+ req,
215
+ res,
216
+ ) {
217
+ const debugging = DEBUG_NODE_ENVS.includes(NODE_ENV);
218
+ const bufferLimit = bytes.parse(BUFFER_LIMIT);
219
+
220
+ log(
221
+ 'info',
222
+ 'GCP_FUNCTIONS_REQUEST',
223
+ JSON.stringify({
224
+ url: req.originalUrl,
225
+ method: req.method,
226
+ body: req.body,
227
+ // body: obfuscateEventBody(obfuscator, req.body),
228
+ headers: obfuscator.obfuscateSensibleHeaders(req.headers),
229
+ }),
230
+ );
231
+
232
+ const request = await gcpfReqToRequest(req);
233
+ let parameters;
234
+ let response;
235
+ let responseLog;
236
+ let responseSpec;
237
+
238
+ log(
239
+ 'debug',
240
+ 'REQUEST',
241
+ JSON.stringify({
242
+ ...request,
243
+ body: request.body ? 'Stream' : undefined,
244
+ headers: obfuscator.obfuscateSensibleHeaders(request.headers),
245
+ }),
246
+ );
247
+
248
+ try {
249
+ const operation = OPERATION;
250
+ const bodySpec = extractBodySpec(
251
+ request,
252
+ consumableMediaTypes,
253
+ consumableCharsets,
254
+ );
255
+
256
+ responseSpec = extractResponseSpec(
257
+ operation,
258
+ request,
259
+ produceableMediaTypes,
260
+ produceableCharsets,
261
+ );
262
+
263
+ try {
264
+ const body = await getBody(
265
+ {
266
+ DECODERS,
267
+ PARSERS,
268
+ bufferLimit,
269
+ },
270
+ operation,
271
+ request.body as Readable,
272
+ bodySpec,
273
+ );
274
+ const path = request.url.split(SEARCH_SEPARATOR)[0];
275
+ const parts = path.split(PATH_SEPARATOR).filter(identity);
276
+ const search = request.url.substr(
277
+ request.url.split(SEARCH_SEPARATOR)[0].length,
278
+ );
279
+
280
+ const pathParameters = OPERATION.path
281
+ .split(PATH_SEPARATOR)
282
+ .filter(identity)
283
+ .map((part, index) => {
284
+ const matches = /^\{([\d\w]+)\}$/i.exec(part);
285
+
286
+ if (matches) {
287
+ return {
288
+ name: matches[1],
289
+ value: parts[index],
290
+ };
291
+ }
292
+ })
293
+ .filter(identity)
294
+ .reduce(
295
+ (accParameters, { name, value }) => ({
296
+ ...accParameters,
297
+ [name]: value,
298
+ }),
299
+ {},
300
+ );
301
+
302
+ // TODO: Update strictQS to handle OpenAPI 3
303
+ const retroCompatibleQueryParameters = (OPERATION.parameters || [])
304
+ .filter((p) => p.in === 'query')
305
+ .map((p) => ({ ...p, ...p.schema }));
306
+
307
+ parameters = {
308
+ ...pathParameters,
309
+ ...QUERY_PARSER(retroCompatibleQueryParameters as any, search),
310
+ ...filterHeaders(operation.parameters, request.headers),
311
+ };
312
+
313
+ parameters = {
314
+ // TODO: Use the security of the operation to infer
315
+ // authorization parameters, see:
316
+ // https://github.com/nfroidure/whook/blob/06ccae93d1d52d97ff70fd5e19fa826bdabf3968/packages/whook-http-router/src/validation.js#L110
317
+ authorization: parameters.authorization,
318
+ ...castParameters(operation.parameters || [], parameters),
319
+ };
320
+
321
+ applyValidators(operation, validators, parameters);
322
+
323
+ bodyValidator(operation, bodySpec.contentType, body);
324
+
325
+ parameters = {
326
+ ...parameters,
327
+ ...('undefined' !== typeof body ? { body } : {}),
328
+ };
329
+ } catch (err) {
330
+ throw HTTPError.cast(err, 400);
331
+ }
332
+
333
+ response = await executeHandler(operation, handler, parameters);
334
+
335
+ if (response.body) {
336
+ response.headers['content-type'] =
337
+ response.headers['content-type'] || responseSpec.contentTypes[0];
338
+ }
339
+
340
+ // Check the stringifyer only when a schema is
341
+ // specified and it is not a binary one
342
+ const responseObject =
343
+ operation.responses &&
344
+ (operation.responses[response.status] as OpenAPIV3.ResponseObject);
345
+ const responseSchema =
346
+ responseObject &&
347
+ responseObject.content &&
348
+ responseObject.content[response.headers['content-type']] &&
349
+ (responseObject.content[response.headers['content-type']]
350
+ .schema as OpenAPIV3.SchemaObject);
351
+ const responseHasSchema =
352
+ responseSchema &&
353
+ (responseSchema.type !== 'string' || responseSchema.format !== 'binary');
354
+
355
+ if (responseHasSchema && !STRINGIFYERS[response.headers['content-type']]) {
356
+ throw new HTTPError(
357
+ 500,
358
+ 'E_STRINGIFYER_LACK',
359
+ response.headers['content-type'],
360
+ );
361
+ }
362
+ if (response.body) {
363
+ checkResponseMediaType(request, responseSpec, produceableMediaTypes);
364
+ checkResponseCharset(request, responseSpec, produceableCharsets);
365
+ }
366
+ responseLog = {
367
+ type: 'success',
368
+ status: response.status,
369
+ };
370
+ log('debug', JSON.stringify(responseLog));
371
+ } catch (err) {
372
+ const castedError = HTTPError.cast(err);
373
+
374
+ responseLog = {
375
+ type: 'error',
376
+ code: castedError.code,
377
+ statusCode: castedError.httpCode,
378
+ params: castedError.params || [],
379
+ stack: castedError.stack,
380
+ };
381
+
382
+ log('error', JSON.stringify(responseLog));
383
+ response = {
384
+ status: castedError.httpCode,
385
+ headers: {
386
+ ...lowerCaseHeaders(CORS),
387
+ ...(castedError.headers ?? {}),
388
+ 'content-type': 'application/json',
389
+ },
390
+ body: {
391
+ error: {
392
+ code: castedError.code,
393
+ stack: debugging ? responseLog.stack : undefined,
394
+ params: debugging ? responseLog.params : undefined,
395
+ },
396
+ },
397
+ };
398
+ }
399
+
400
+ log(
401
+ 'debug',
402
+ 'RESPONSE',
403
+ JSON.stringify({
404
+ ...response,
405
+ body: obfuscateEventBody(obfuscator, response.body),
406
+ headers: obfuscator.obfuscateSensibleHeaders(response.headers),
407
+ }),
408
+ );
409
+
410
+ await pipeResponseInGCPFResponse(
411
+ await sendBody(
412
+ {
413
+ ENCODERS,
414
+ STRINGIFYERS,
415
+ },
416
+ response,
417
+ ),
418
+ res,
419
+ );
420
+ }
421
+
422
+ async function gcpfReqToRequest(req): Promise<WhookRequest> {
423
+ const request: WhookRequest = {
424
+ method: req.method.toLowerCase(),
425
+ headers: lowerCaseHeaders(req.headers || {}),
426
+ url: req.originalUrl,
427
+ };
428
+
429
+ if (req.rawBody) {
430
+ request.headers['content-length'] = req.rawBody.length.toString();
431
+ const bodyStream = new stream.PassThrough();
432
+
433
+ request.body = bodyStream;
434
+ bodyStream.write(req.rawBody);
435
+ bodyStream.end();
436
+ }
437
+
438
+ return request;
439
+ }
440
+
441
+ async function pipeResponseInGCPFResponse(
442
+ response: WhookResponse,
443
+ res,
444
+ ): Promise<void> {
445
+ Object.keys(response.headers).forEach((headerName) => {
446
+ res.set(headerName, response.headers[headerName]);
447
+ });
448
+ res.status(response.status);
449
+
450
+ if (response.body) {
451
+ (response.body as Readable).pipe(res);
452
+ return;
453
+ }
454
+
455
+ res.end();
456
+ }
457
+
458
+ function obfuscateEventBody(obfuscator, rawBody) {
459
+ if (typeof rawBody === 'string') {
460
+ try {
461
+ const jsonBody = JSON.parse(rawBody);
462
+
463
+ return JSON.stringify(obfuscator.obfuscateSensibleProps(jsonBody));
464
+ // eslint-disable-next-line
465
+ } catch (err) {}
466
+ }
467
+ return rawBody;
468
+ }