@yopdev/dev-server 3.0.1 → 3.0.2-RC1

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 (92) hide show
  1. package/.github/workflows/npm-publish.yml +33 -0
  2. package/__tests__/bootstrap.test.ts +89 -0
  3. package/__tests__/deferred.test.ts +86 -0
  4. package/__tests__/event-proxy.test.ts +42 -0
  5. package/__tests__/lambda-http-proxy.test.ts +179 -0
  6. package/jest.config.js +7 -0
  7. package/package.json +2 -5
  8. package/src/assert.ts +4 -0
  9. package/src/cloudformation-dynamodb-table.ts +97 -0
  10. package/src/cloudformation-event-proxy.ts +61 -0
  11. package/src/cloudformation-lambda-http-proxy.ts +125 -0
  12. package/src/cloudformation.ts +95 -0
  13. package/src/config.ts +34 -0
  14. package/src/container.ts +82 -0
  15. package/src/deferred.ts +60 -0
  16. package/src/dev-server.ts +78 -0
  17. package/src/dynamodb.ts +62 -0
  18. package/src/event-proxy.ts +101 -0
  19. package/src/factories.ts +19 -0
  20. package/src/http-server.ts +59 -0
  21. package/src/index.ts +32 -0
  22. package/src/internal-queue.ts +89 -0
  23. package/src/lambda-http-proxy.ts +111 -0
  24. package/src/localstack.ts +74 -0
  25. package/src/mappers.ts +231 -0
  26. package/src/pre-traffic-hooks.ts +24 -0
  27. package/src/responses.ts +28 -0
  28. package/src/s3.ts +24 -0
  29. package/src/scheduled-tasks.ts +31 -0
  30. package/src/services.ts +46 -0
  31. package/src/sns-http-proxy.ts +109 -0
  32. package/src/sns.ts +49 -0
  33. package/src/sqs.ts +46 -0
  34. package/src/stoppable.ts +10 -0
  35. package/src/tunnel.ts +32 -0
  36. package/tsconfig.json +9 -0
  37. package/dist/src/assert.d.ts +0 -1
  38. package/dist/src/assert.js +0 -9
  39. package/dist/src/cloudformation-dynamodb-table.d.ts +0 -13
  40. package/dist/src/cloudformation-dynamodb-table.js +0 -45
  41. package/dist/src/cloudformation-event-proxy.d.ts +0 -13
  42. package/dist/src/cloudformation-event-proxy.js +0 -25
  43. package/dist/src/cloudformation-lambda-http-proxy.d.ts +0 -14
  44. package/dist/src/cloudformation-lambda-http-proxy.js +0 -62
  45. package/dist/src/cloudformation.d.ts +0 -28
  46. package/dist/src/cloudformation.js +0 -50
  47. package/dist/src/config.d.ts +0 -31
  48. package/dist/src/config.js +0 -2
  49. package/dist/src/container.d.ts +0 -18
  50. package/dist/src/container.js +0 -33
  51. package/dist/src/deferred.d.ts +0 -4
  52. package/dist/src/deferred.js +0 -45
  53. package/dist/src/dev-server.d.ts +0 -19
  54. package/dist/src/dev-server.js +0 -63
  55. package/dist/src/dynamodb.d.ts +0 -16
  56. package/dist/src/dynamodb.js +0 -48
  57. package/dist/src/event-proxy.d.ts +0 -13
  58. package/dist/src/event-proxy.js +0 -68
  59. package/dist/src/factories.d.ts +0 -3
  60. package/dist/src/factories.js +0 -16
  61. package/dist/src/http-server.d.ts +0 -25
  62. package/dist/src/http-server.js +0 -37
  63. package/dist/src/index.d.ts +0 -24
  64. package/dist/src/index.js +0 -46
  65. package/dist/src/internal-queue.d.ts +0 -11
  66. package/dist/src/internal-queue.js +0 -53
  67. package/dist/src/lambda-http-proxy.d.ts +0 -27
  68. package/dist/src/lambda-http-proxy.js +0 -49
  69. package/dist/src/localstack.d.ts +0 -11
  70. package/dist/src/localstack.js +0 -62
  71. package/dist/src/mappers.d.ts +0 -25
  72. package/dist/src/mappers.js +0 -158
  73. package/dist/src/pre-traffic-hooks.d.ts +0 -2
  74. package/dist/src/pre-traffic-hooks.js +0 -19
  75. package/dist/src/responses.d.ts +0 -5
  76. package/dist/src/responses.js +0 -22
  77. package/dist/src/s3.d.ts +0 -7
  78. package/dist/src/s3.js +0 -20
  79. package/dist/src/scheduled-tasks.d.ts +0 -6
  80. package/dist/src/scheduled-tasks.js +0 -20
  81. package/dist/src/services.d.ts +0 -22
  82. package/dist/src/services.js +0 -26
  83. package/dist/src/sns-http-proxy.d.ts +0 -28
  84. package/dist/src/sns-http-proxy.js +0 -66
  85. package/dist/src/sns.d.ts +0 -15
  86. package/dist/src/sns.js +0 -35
  87. package/dist/src/sqs.d.ts +0 -13
  88. package/dist/src/sqs.js +0 -33
  89. package/dist/src/stoppable.d.ts +0 -2
  90. package/dist/src/stoppable.js +0 -15
  91. package/dist/src/tunnel.d.ts +0 -10
  92. package/dist/src/tunnel.js +0 -52
@@ -0,0 +1,59 @@
1
+ import { IncomingMessage, Server, ServerResponse, createServer } from "http";
2
+ import { once } from "events";
3
+ import { Logger, LoggerFactory } from "@yopdev/logging";
4
+ import { AddressInfo } from "net";
5
+ import { assertNotUndefined } from "./assert";
6
+ import { Lifecycle, Service, Callback } from "./services";
7
+
8
+ export const newHttpServer = (
9
+ name: string,
10
+ config: {
11
+ settings: HttpSettings,
12
+ handler: (request: IncomingMessage, requestBody: string, response: ServerResponse) => void,
13
+ },
14
+ callback?: Callback<string>,
15
+ ) => new Service(new HttpServer(name, config.settings, config.handler), callback)
16
+
17
+ export class HttpServer implements Lifecycle<string> {
18
+ private LOGGER: Logger
19
+ private readonly protocol: string
20
+ private address: AddressInfo | undefined
21
+ private readonly server: Promise<Server>
22
+
23
+ constructor(
24
+ readonly name: string,
25
+ readonly settings: HttpSettings,
26
+ readonly handler: (request: IncomingMessage, requestBody: string, response: ServerResponse) => void,
27
+ ) {
28
+ const server = createServer((req, res) => {
29
+ req.setEncoding('utf-8')
30
+
31
+ let body = ''
32
+
33
+ req.on('data', (chunks) => body = body.concat(chunks));
34
+ req.on('end', () => handler(req, body, res));
35
+ })
36
+ .listen(settings.port, settings.host, () => this.address = server.address() as AddressInfo)
37
+ this.server = once(server, 'listening')
38
+ .then(() => server)
39
+ this.protocol = settings.protocol
40
+ this.LOGGER = LoggerFactory.create(`HTTPSERVER[${name}]`);
41
+ }
42
+
43
+ start = () => this.server
44
+ .then(() => this.LOGGER.debug('listening [%s]', assertNotUndefined(this.address).port))
45
+ .then(() => this.endpointUrl())
46
+
47
+ private endpointUrl = () => {
48
+ const address = assertNotUndefined(this.address, 'server not started')
49
+ return `${this.protocol}//${address.address}:${address.port}/`
50
+ }
51
+
52
+ stop = () => this.server.then((server) => server.close()).then(() => undefined)
53
+ }
54
+
55
+ export type HttpSettings = {
56
+ protocol: string
57
+ host: string
58
+ port: number | undefined
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ export { DevServer } from './dev-server'
2
+ export { newHttpServer as httpServer, HttpSettings } from './http-server'
3
+ export { newLambdaHttpProxy as lambdaHttpProxy, UNAUTHORIZED } from './lambda-http-proxy'
4
+ export { v1 as v1LambdaProxyPayload, v2 as v2LambdaProxyPayload } from './mappers'
5
+ export { newLambdaProxyFromCloudFormationTemplate as cloudFormationLambdaProxy } from './cloudformation-lambda-http-proxy'
6
+ export { newDynamoDbTableFromCloudFormationTemplate as cloudFormationDynamoDbTable } from './cloudformation-dynamodb-table'
7
+ export { newSnsHttpProxy as snsHttpProxy } from './sns-http-proxy'
8
+ export { newInternalQueue as internalQueue } from './internal-queue'
9
+ export { newEventsProxy as eventsProxy } from './event-proxy'
10
+ export { snsEventsFromCloudFormationTemplate as cloudFormationSnsEventHandlers } from './cloudformation-event-proxy'
11
+ export { newScheduledTasks as scheduledTasks } from './scheduled-tasks'
12
+ export { newPreTrafficHooks as preTrafficHooks } from './pre-traffic-hooks'
13
+ export { newDynamoDbTable as dynamoDbTable } from './dynamodb'
14
+ export { newContainer as container } from './container'
15
+ export { newTunnel as tunnel } from './tunnel'
16
+ export { all as allOf, oneThenOther } from './factories'
17
+ export { lazy, promised } from './deferred'
18
+ export { Lifecycle, Service, Startable } from './services'
19
+ export { DevServerConfig } from './config'
20
+
21
+ declare global {
22
+ interface Promise<T> {
23
+ tap(onfulfilled?: (value: T) => void, onrejected?: (reason: unknown) => void): Promise<T>;
24
+ }
25
+ }
26
+
27
+ Promise.prototype.tap = async function <T>(
28
+ onfulfilled?: (value: T) => void,
29
+ onrejected?: (reason: unknown) => void,
30
+ ): Promise<any> {
31
+ return this.then(onfulfilled, onrejected).then(async () => this);
32
+ };
@@ -0,0 +1,89 @@
1
+ import { Logger, LoggerFactory } from "@yopdev/logging";
2
+ import { Queue, Sqs } from "./sqs";
3
+ import { SQSEvent, SQSRecord } from "aws-lambda";
4
+ import { Consumer } from "sqs-consumer";
5
+ import { stopConsumer } from "./stoppable";
6
+ import { mapToLambdaSqsRecord } from "./mappers";
7
+ import { Lifecycle, Service, Callback } from "./services";
8
+ import { DevServerConfig } from "./config";
9
+ import { Message, SQSClient } from "@aws-sdk/client-sqs";
10
+
11
+ export const newInternalQueue = (
12
+ name: string,
13
+ config: {
14
+ name: string,
15
+ visibility: number,
16
+ handler: (queue: Queue) => (e: SQSEvent) => Promise<void>,
17
+ fifo: boolean,
18
+ mapper?: (message: Message) => SQSRecord,
19
+ },
20
+ callback?: Callback<string>
21
+ ) => new Service(new InternalQueue(
22
+ name,
23
+ config.visibility,
24
+ config.handler,
25
+ (sqs: Sqs) => config.fifo ? sqs.createFifoQueue : sqs.createStandardQueue,
26
+ config.mapper ?? mapToLambdaSqsRecord,
27
+ ), callback)
28
+
29
+ class InternalQueue implements Lifecycle<string> {
30
+ private LOGGER: Logger
31
+
32
+ private consumer: Consumer | undefined;
33
+
34
+ constructor(
35
+ readonly name: string,
36
+ private readonly visibility: number,
37
+ private readonly handler: (queue: Queue) => ((e: SQSEvent) => Promise<void>),
38
+ private readonly creator: (sqs: Sqs) => (name: string) => Promise<Queue>,
39
+ private readonly mapper: (message: Message) => SQSRecord,
40
+ ) {
41
+ this.LOGGER = LoggerFactory.create(`INTERNALQUEUE[${name}]`);
42
+ }
43
+
44
+ start = (config: DevServerConfig) => this.create(config.sqs)
45
+
46
+ private create = async (sqs: Sqs) => this.createQueue(sqs, this.name)
47
+ .then((queue) => this
48
+ .createConsumer(sqs.client, this.name, queue.url, this.handler(queue), this.visibility)
49
+ .then(() => queue.url)
50
+ )
51
+
52
+ private async createQueue(sqs: Sqs, name: string) {
53
+ return this.creator(sqs)(name) //
54
+ .then((queue) => {
55
+ this.LOGGER.info('created: %s', queue.url);
56
+ return queue;
57
+ });
58
+ }
59
+
60
+ private createConsumer = async (
61
+ sqs: SQSClient,
62
+ name: string,
63
+ url: string,
64
+ handler: (e: SQSEvent) => Promise<void>,
65
+ visibility: number,
66
+ ): Promise<InternalQueue> => Promise.resolve(() =>
67
+ this.consumer = Consumer.create({
68
+ queueUrl: url,
69
+ waitTimeSeconds: visibility,
70
+ sqs: sqs,
71
+ handleMessage: async (message) => handler({ Records: [this.mapper(message)] }),
72
+ }))
73
+ .then((factory) => factory())
74
+ .then((consumer) => {
75
+ consumer.on('error', (err) => {
76
+ this.LOGGER.error(err, 'failed to handle message');
77
+ });
78
+ consumer.on('processing_error', (err) => {
79
+ this.LOGGER.error(err, 'failed to process message');
80
+ });
81
+ this.LOGGER.info('consumer for %s initialized', name);
82
+ return consumer;
83
+ })
84
+ .then(async (consumer) => consumer.start())
85
+ .then(() => this.LOGGER.info('started'))
86
+ .then(() => this)
87
+
88
+ stop = async () => this.consumer !== undefined ? stopConsumer(this.visibility, this.consumer) : this.LOGGER.warn('no consumer')
89
+ }
@@ -0,0 +1,111 @@
1
+ import { Logger, LoggerFactory } from "@yopdev/logging";
2
+ import { HttpServer, HttpSettings } from "./http-server";
3
+ import { IncomingMessage, ServerResponse } from "http";
4
+ import { LambdaMapperFactory } from "./mappers";
5
+ import { internalServerError, writeResponse } from "./responses";
6
+ import { Lifecycle, Service, Callback } from "./services";
7
+
8
+ export const UNAUTHORIZED = new Error('UNAUTHORIZED');
9
+ export const newLambdaHttpProxy = <Context, AuthorizerContext, Event, HandlerResponse>(
10
+ name: string,
11
+ config: {
12
+ settings: HttpSettings,
13
+ routes: Route<Context, AuthorizerContext, Event, HandlerResponse>[],
14
+ mapper: LambdaMapperFactory<AuthorizerContext, Event, HandlerResponse>,
15
+ authorizer?: Authorizer<AuthorizerContext>
16
+ context?: () => Context
17
+ },
18
+ callback?: Callback<string>,
19
+ ) => new Service(new LambdaHttpProxy<Context, AuthorizerContext, Event, HandlerResponse>(
20
+ name,
21
+ config.settings,
22
+ config.routes,
23
+ config.mapper,
24
+ config.authorizer ?? (() => Promise.resolve(undefined)),
25
+ config.context,
26
+ ), callback)
27
+
28
+ class LambdaHttpProxy<Context, AuthorizerContext, Event, HandlerResponse> implements Lifecycle<string> {
29
+ private LOGGER: Logger
30
+ private readonly server: HttpServer
31
+
32
+ constructor(
33
+ readonly name: string,
34
+ settings: HttpSettings,
35
+ private readonly routes: Route<Context, AuthorizerContext, Event, HandlerResponse>[],
36
+ private readonly mapper: LambdaMapperFactory<AuthorizerContext, Event, HandlerResponse>,
37
+ private readonly defaultAuthorizer: Authorizer<AuthorizerContext>,
38
+ private readonly context: () => Context = () => undefined,
39
+ ) {
40
+ this.server = new HttpServer(name, settings, this.resolveRoute)
41
+ this.LOGGER = LoggerFactory.create(`HTTP->LAMBDA[${this.name}]`);
42
+ this.LOGGER.info('registered %i routes', routes.length)
43
+ }
44
+
45
+ start = () => this.server.start()
46
+
47
+ stop = () => this.server.stop()
48
+
49
+ private handler = (
50
+ authorizer: Authorizer<AuthorizerContext>,
51
+ lambdaHandler: (event: Event, context: Context) => Promise<HandlerResponse>
52
+ ) =>
53
+ (request: IncomingMessage, body: string, response: ServerResponse) =>
54
+ this.mapper.newInstance(request, body)
55
+ .then(async (mapper) =>
56
+ authorizer(mapper.authorization())
57
+ .then(async (context) =>
58
+ lambdaHandler(mapper.event(context), this.context())
59
+ .then((lambda) => {
60
+ const result = mapper.toResponse(lambda)
61
+ return writeResponse(
62
+ response,
63
+ result.statusCode,
64
+ result.body(),
65
+ result.contentType,
66
+ result.location,
67
+ result.cookies,
68
+ )
69
+ })
70
+ .catch((e) => {
71
+ this.LOGGER.error(e, 'request failed to execute');
72
+ internalServerError(response, e.body);
73
+ }),
74
+ (e) => e === UNAUTHORIZED ? writeResponse(response, 401, '') : internalServerError(response, e))
75
+ )
76
+
77
+ private fallback = async (request: IncomingMessage, _: string, response: ServerResponse) =>
78
+ Promise.resolve(this.LOGGER.warn(`FALLBACK: ${request.method} to ${request.url}`)).then(() =>
79
+ writeResponse(
80
+ response,
81
+ 404,
82
+ `no route found to handle ${request.method} to ${request.url}`
83
+ ))
84
+
85
+ private resolveRoute = (request: IncomingMessage, body: string, response: ServerResponse) =>
86
+ (this.routes.filter((r) => r.method.test(request.method))
87
+ .filter((r) => r.path.test(request.url))
88
+ .sort((r1, r2) => r2.weight - r1.weight)
89
+ .map((r) => this.handler(r.authorizer ?? this.defaultAuthorizer, r.handler))
90
+ .find(() => true) ?? this.fallback)(request, body, response)
91
+ }
92
+
93
+ export abstract class Response {
94
+ constructor(
95
+ readonly statusCode: number,
96
+ readonly contentType: string | undefined,
97
+ readonly location: string | undefined,
98
+ readonly cookies: string[] | undefined,
99
+ ) { }
100
+ body: () => string | Buffer | undefined
101
+ }
102
+
103
+ export type Route<Context, AuthorizerContext, Event, HandlerResponse> = {
104
+ method: RegExp;
105
+ path: RegExp;
106
+ weight: number;
107
+ authorizer?: Authorizer<AuthorizerContext>;
108
+ handler: (event: Event, context: Context) => Promise<HandlerResponse>;
109
+ };
110
+
111
+ export type Authorizer<Context> = (authorization: string) => Promise<Context | undefined>;
@@ -0,0 +1,74 @@
1
+ import { LoggerFactory } from "@yopdev/logging";
2
+ import { GenericContainer, StartedNetwork, StartedTestContainer } from "testcontainers";
3
+ import { AwsConfig, Config } from "./config";
4
+ import { Stoppable } from "./services";
5
+
6
+ const LOGGER = LoggerFactory.create('LOCALSTACK');
7
+ const SERVICES_PORT = 4566;
8
+ const NETWORK_ALIAS = 'localstack';
9
+ const PROTOCOL = 'http';
10
+ const DEFAULT_DOCKER_IMAGE = 'localstack/localstack:stable'
11
+ export class LocalStack implements Stoppable {
12
+ private readonly _start: () => Promise<string>
13
+ private started: StartedTestContainer | undefined
14
+
15
+ constructor(network: StartedNetwork, config?: Config) {
16
+ const concreteConfig = this.configOrDefaults(config)
17
+ const container = new GenericContainer(concreteConfig.localStackDockerImage)
18
+ const localStatePath = concreteConfig.localStateBindMount
19
+ const withLocalState = localStatePath !== undefined ?
20
+ container
21
+ .withEnvironment({
22
+ PERSISTENCE: '1',
23
+ DYNAMODB_REMOVE_EXPIRED_ITEMS: '1',
24
+ })
25
+ .withBindMounts([{
26
+ source: localStatePath,
27
+ target: '/var/lib/localstack',
28
+ mode: 'rw',
29
+ }])
30
+ : container
31
+ const ready = withLocalState
32
+ .withExposedPorts(concreteConfig.exposedPort)
33
+ .withNetwork(network)
34
+ .withNetworkAliases(NETWORK_ALIAS)
35
+
36
+ this._start = () => ready
37
+ .start()
38
+ .then((container) => this.started = container)
39
+ .then((container) => `${PROTOCOL}://${container.getHost()}:${container.getMappedPort(SERVICES_PORT)}`)
40
+ }
41
+
42
+ private configOrDefaults(config?: Config): LocalStackConfig {
43
+ const selectedServicesPort = config?.boundServicesPort
44
+ const exposedPort = selectedServicesPort !== undefined ?
45
+ { container: SERVICES_PORT, host: selectedServicesPort } :
46
+ SERVICES_PORT
47
+ return {
48
+ localStackDockerImage: config?.localStackDockerImage || DEFAULT_DOCKER_IMAGE,
49
+ exposedPort: exposedPort,
50
+ localStateBindMount: config?.localStateBindMount,
51
+ }
52
+ }
53
+
54
+ start = async (): Promise<AwsConfig> => this._start()
55
+ .then((endpoint) => ({
56
+ region: 'us-east-1',
57
+ endpoint: endpoint,
58
+ credentials: {
59
+ accessKeyId: 'dummy',
60
+ secretAccessKey: 'dummy'
61
+ }
62
+ }))
63
+ .tap(() => LOGGER.debug('started'))
64
+
65
+ stop = () => Promise.resolve(this.started)
66
+ .then((container) => container ? container.stop().then(() => 'stopped') : 'not started')
67
+ .then((status) => LOGGER.debug(status))
68
+ }
69
+
70
+ type LocalStackConfig = {
71
+ localStackDockerImage: string
72
+ exposedPort: { container: number, host: number } | number
73
+ localStateBindMount: string | undefined
74
+ }
package/src/mappers.ts ADDED
@@ -0,0 +1,231 @@
1
+ import { Message } from "@aws-sdk/client-sqs";
2
+ import { APIGatewayEventRequestContextV2WithAuthorizer, APIGatewayProxyEvent, APIGatewayProxyEventHeaders, APIGatewayProxyEventPathParameters, APIGatewayProxyEventQueryStringParameters, APIGatewayProxyEventV2, APIGatewayProxyEventV2WithRequestContext, APIGatewayProxyResult, APIGatewayProxyStructuredResultV2, SQSRecord } from "aws-lambda";
3
+ import { IncomingMessage } from "http";
4
+ import { URL } from "url";
5
+ import { Response } from "./lambda-http-proxy";
6
+
7
+ export const mapToLambdaSqsRecord = (message: Message): SQSRecord => {
8
+ if (!message.Body) throw new Error('message Body must be present');
9
+
10
+ return {
11
+ messageId: 'N/A',
12
+ receiptHandle: 'N/A',
13
+ body: message.Body,
14
+ attributes: {
15
+ ApproximateReceiveCount: 'N/A',
16
+ SentTimestamp: 'N/A',
17
+ SenderId: 'N/A',
18
+ ApproximateFirstReceiveTimestamp: 'N/A',
19
+ },
20
+ messageAttributes: {},
21
+ md5OfBody: 'N/A',
22
+ eventSource: 'N/A',
23
+ eventSourceARN: 'N/A',
24
+ awsRegion: 'N/A',
25
+ };
26
+ }
27
+
28
+ const payloadV1JwtAuthorizerLambdaMapper = <AuthorizerContext>(): LambdaMapperFactory<AuthorizerContext, APIGatewayProxyEvent, APIGatewayProxyResult> => ({
29
+ newInstance: (request, body) => Promise.resolve(new DefaultLambdaMapper<AuthorizerContext>(request, body))
30
+ })
31
+
32
+ const payloadV2LambdaAuthorizerLambdaMapper = <AuthorizerContext>(): LambdaMapperFactory<AuthorizerContext, APIGatewayEventRequestContextV2WithGenericAuthorizer<AuthorizerContext>, APIGatewayProxyStructuredResultV2> => ({
33
+ newInstance: (request, body) => Promise.resolve(new DefaultLambdaMapper<AuthorizerContext>(request, body))
34
+ })
35
+
36
+ const payloadV1PathParameterResolver: PathParameterResolver<APIGatewayProxyEvent> = {
37
+ locate: (event: APIGatewayProxyEvent) => event.path,
38
+ store: (event: APIGatewayProxyEvent, params: APIGatewayProxyEventPathParameters) => event.pathParameters = params
39
+ }
40
+
41
+ const payloadV2PathParameterResolver: PathParameterResolver<APIGatewayProxyEventV2> = {
42
+ locate: (event: APIGatewayProxyEventV2) => event.rawPath,
43
+ store: (event: APIGatewayProxyEventV2, params: APIGatewayProxyEventPathParameters) => event.pathParameters = params
44
+ }
45
+
46
+ export type LambdaPayloadVersion<AuthorizerContext, Event, HandlerResponse> = {
47
+ mapper: LambdaMapperFactory<AuthorizerContext, Event, HandlerResponse>,
48
+ resolver: PathParameterResolver<Event>,
49
+ }
50
+
51
+ export const v1 = <AuthorizerContext>(): LambdaPayloadVersion<AuthorizerContext, APIGatewayProxyEvent, APIGatewayProxyResult> => ({
52
+ mapper: payloadV1JwtAuthorizerLambdaMapper<AuthorizerContext>(),
53
+ resolver: payloadV1PathParameterResolver,
54
+ })
55
+
56
+ export const v2 = <AuthorizerContext>(): LambdaPayloadVersion<AuthorizerContext, APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2> => ({
57
+ mapper: payloadV2LambdaAuthorizerLambdaMapper<AuthorizerContext>(),
58
+ resolver: payloadV2PathParameterResolver,
59
+ })
60
+
61
+ class DefaultLambdaMapper<AuthorizerContext> implements LambdaMapper<AuthorizerContext, APIGatewayProxyEvent & APIGatewayEventRequestContextV2WithGenericAuthorizer<AuthorizerContext>, APIGatewayProxyResult & APIGatewayProxyStructuredResultV2> {
62
+ private readonly url: URL
63
+ private readonly authorizationHeaderValue: string
64
+ private readonly time: number
65
+ private readonly method: string
66
+ private readonly queryStringParameters: APIGatewayProxyEventQueryStringParameters
67
+ private readonly headers: APIGatewayProxyEventHeaders
68
+
69
+ constructor(request: IncomingMessage, private readonly body: string) {
70
+ if (!request.url || !request.method) throw new Error('url and method are required');
71
+
72
+ const url = new URL(request.url, `http://${request.headers.host}`);
73
+
74
+ const qsp: APIGatewayProxyEventQueryStringParameters = {};
75
+ url.searchParams.forEach((v, k) => (qsp[k] = decodeURIComponent(v)));
76
+ this.queryStringParameters = qsp;
77
+
78
+ const headers: APIGatewayProxyEventHeaders = {};
79
+ if (request.headers['content-type']) headers['content-type'] = request.headers['content-type'];
80
+ if (request.headers['accept']) headers['accept'] = request.headers['accept'];
81
+ if (request.headers['origin']) headers['origin'] = request.headers['origin'];
82
+ if (request.headers['cookie']) headers['cookie'] = request.headers['cookie'];
83
+ const authorizationHeaderValue = request.headers['authorization']
84
+ if (authorizationHeaderValue) {
85
+ headers['authorization'] = authorizationHeaderValue;
86
+ this.authorizationHeaderValue = authorizationHeaderValue;
87
+ }
88
+ this.headers = headers
89
+ this.url = url
90
+ this.time = 1428582896000;
91
+ this.method = request.method
92
+ }
93
+
94
+ toResponse = (response: APIGatewayProxyResult | APIGatewayProxyStructuredResultV2) => response.isBase64Encoded ?
95
+ new Base64EncodedResponse(response.statusCode, response.headers?.['content-type']?.toString(), response.headers?.['location']?.toString(), response.body, this.readSetCookies(response)) :
96
+ new StringResponse(response.statusCode, response.headers?.['content-type']?.toString(), response.headers?.['location']?.toString(), response.body, this.readSetCookies(response))
97
+
98
+ private readSetCookies = (response: APIGatewayProxyResult | APIGatewayProxyStructuredResultV2): string[] => {
99
+ // V2: structured response with cookies array
100
+ if ('cookies' in response && Array.isArray(response.cookies)) {
101
+ return response.cookies;
102
+ }
103
+
104
+ // V1: check multiValueHeaders first
105
+ if ('multiValueHeaders' in response && response.multiValueHeaders?.['Set-Cookie']) {
106
+ return response.multiValueHeaders['Set-Cookie'].map(c => c.toString());
107
+ }
108
+
109
+ // V1: fallback to single header
110
+ if ('headers' in response && response.headers?.['Set-Cookie']) {
111
+ const cookieHeader = response.headers['Set-Cookie'];
112
+ return Array.isArray(cookieHeader) ? cookieHeader.map(c => c.toString()) : [cookieHeader];
113
+ }
114
+
115
+ return [];
116
+ };
117
+
118
+ event = (context: AuthorizerContext): APIGatewayProxyEvent & APIGatewayEventRequestContextV2WithGenericAuthorizer<AuthorizerContext> => ({
119
+ version: '2.0',
120
+ rawPath: this.url.pathname,
121
+ rawQueryString: this.url.search,
122
+ routeKey: '',
123
+ httpMethod: this.method,
124
+ body: this.body,
125
+ headers: this.headers,
126
+ path: this.url.pathname,
127
+ pathParameters: null,
128
+ queryStringParameters: this.queryStringParameters,
129
+ resource: '',
130
+ multiValueHeaders: {},
131
+ isBase64Encoded: false,
132
+ multiValueQueryStringParameters: null,
133
+ stageVariables: null,
134
+ requestContext: {
135
+ domainName: this.headers.host,
136
+ domainPrefix: '',
137
+ http: {
138
+ method: this.method,
139
+ path: this.url.pathname,
140
+ protocol: 'http',
141
+ sourceIp: '127.0.0.1',
142
+ userAgent: this.headers['user-agent'],
143
+ },
144
+ routeKey: '',
145
+ time: new Date(this.time).toString(),
146
+ timeEpoch: this.time,
147
+ accountId: '',
148
+ apiId: '',
149
+ authorizer: context,
150
+ httpMethod: '',
151
+ identity: {
152
+ accessKey: '',
153
+ accountId: '',
154
+ apiKey: '',
155
+ apiKeyId: '',
156
+ caller: '',
157
+ clientCert: {
158
+ clientCertPem: '',
159
+ issuerDN: '',
160
+ serialNumber: '',
161
+ subjectDN: '',
162
+ validity: { notAfter: '', notBefore: '' },
163
+ },
164
+ cognitoAuthenticationProvider: '',
165
+ cognitoAuthenticationType: '',
166
+ cognitoIdentityId: '',
167
+ cognitoIdentityPoolId: '',
168
+ principalOrgId: '',
169
+ sourceIp: '',
170
+ user: '',
171
+ userAgent: '',
172
+ userArn: '',
173
+ },
174
+ path: '',
175
+ protocol: '',
176
+ requestId: '',
177
+ requestTimeEpoch: this.time,
178
+ resourceId: '',
179
+ resourcePath: '',
180
+ stage: '',
181
+ }
182
+ })
183
+
184
+ authorization = () => this.authorizationHeaderValue
185
+ }
186
+ interface LambdaMapper<AuthorizerContext, Event, HandlerResponse> {
187
+ event(context: AuthorizerContext): Event
188
+ authorization(): string
189
+ toResponse: (response: HandlerResponse) => Response
190
+ }
191
+
192
+ export interface LambdaMapperFactory<AuthorizerContext, Event, LambdaResponse> {
193
+ newInstance(request: IncomingMessage, body: string): Promise<LambdaMapper<AuthorizerContext, Event, LambdaResponse>>;
194
+ }
195
+
196
+ class StringResponse extends Response {
197
+ readonly body: () => string
198
+
199
+ constructor(
200
+ statusCode: number,
201
+ contentType: string | undefined,
202
+ location: string | undefined,
203
+ body: string | undefined,
204
+ cookies: string[] | undefined
205
+ ) {
206
+ super(statusCode, contentType, location, cookies)
207
+ this.body = () => body;
208
+ }
209
+ }
210
+
211
+ class Base64EncodedResponse extends Response {
212
+ readonly body: () => Buffer
213
+
214
+ constructor(
215
+ statusCode: number,
216
+ contentType: string | undefined,
217
+ location: string | undefined,
218
+ body: string,
219
+ cookies: string[] | undefined,
220
+ ) {
221
+ super(statusCode, contentType, location, cookies)
222
+ this.body = () => Buffer.from(body, 'base64')
223
+ }
224
+ }
225
+
226
+ type APIGatewayEventRequestContextV2WithGenericAuthorizer<AuthorizerContext> = APIGatewayProxyEventV2WithRequestContext<APIGatewayEventRequestContextV2WithAuthorizer<AuthorizerContext>>;
227
+
228
+ export type PathParameterResolver<Event> = {
229
+ locate: (event: Event) => string
230
+ store: (event: Event, params: APIGatewayProxyEventPathParameters) => void
231
+ }
@@ -0,0 +1,24 @@
1
+ import { Logger, LoggerFactory } from '@yopdev/logging';
2
+ import { Lifecycle, Service } from './services';
3
+
4
+ export const newPreTrafficHooks = (
5
+ name: string,
6
+ hooks: () => Promise<void>[],
7
+ ) => new Service(new PreTrafficHooks(name, hooks))
8
+
9
+ class PreTrafficHooks implements Lifecycle<void> {
10
+ private LOGGER: Logger
11
+ constructor(
12
+ readonly name: string,
13
+ private readonly hooks: () => Promise<void>[]
14
+ ) {
15
+ this.LOGGER = LoggerFactory.create(`PRETRAFFIC[${name}]`);
16
+ this.LOGGER.info('%i hooks registered', hooks.length);
17
+ }
18
+
19
+ start = async () => Promise
20
+ .all(this.hooks())
21
+ .then(() => undefined)
22
+
23
+ stop = async () => Promise.resolve()
24
+ }
@@ -0,0 +1,28 @@
1
+ import { LoggerFactory } from '@yopdev/logging';
2
+ import { ServerResponse } from 'http';
3
+
4
+ const LOG = LoggerFactory.create('RESPONSES');
5
+ export function internalServerError(res: ServerResponse, body: any) {
6
+ LOG.error(body, 'internal server error handled');
7
+ return writeResponse(res, 500, 'internal server error. check system logs');
8
+ }
9
+
10
+ export function writeResponse(
11
+ res: ServerResponse,
12
+ statusCode: number,
13
+ body: string | Buffer,
14
+ contentType?: string,
15
+ location?: string,
16
+ cookies?: string[],
17
+ ) {
18
+ res.setHeader('Access-Control-Allow-Origin', '*');
19
+ res.setHeader('Access-Control-Allow-Methods', '*');
20
+ res.setHeader('Access-Control-Allow-Headers', '*');
21
+ if (cookies) {
22
+ res.setHeader('Set-Cookie', cookies);
23
+ }
24
+ if (contentType) res.setHeader('Content-Type', contentType);
25
+ if (location) res.setHeader('Location', location);
26
+ res.statusCode = statusCode;
27
+ res.end(body);
28
+ }
package/src/s3.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { CORSConfiguration, CORSRule, CreateBucketCommand, PutBucketCorsCommand, S3Client } from '@aws-sdk/client-s3';
2
+ import { AwsConfig } from './config';
3
+ import { LoggerFactory } from '@yopdev/logging';
4
+
5
+ const LOGGER = LoggerFactory.create('S3')
6
+ export class S3 {
7
+ readonly client: S3Client;
8
+
9
+ constructor(aws: AwsConfig) {
10
+ this.client = new S3Client({ forcePathStyle: true, ...aws });
11
+ }
12
+
13
+ createBucket = async (name: string, cors?: CORSConfiguration) =>
14
+ this.client.send(
15
+ new CreateBucketCommand({
16
+ Bucket: name,
17
+ }),
18
+ )
19
+ .then(() => cors && this.client.send(new PutBucketCorsCommand({
20
+ Bucket: name,
21
+ CORSConfiguration: cors
22
+ })))
23
+ .tap(() => LOGGER.debug('bucket %s created', name))
24
+ }