@yopdev/dev-server 3.0.2-RC → 3.0.2-RC2

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 (100) hide show
  1. package/.github/workflows/npm-publish.yml +34 -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/dist/__tests__/bootstrap.test.d.ts +1 -0
  7. package/dist/__tests__/bootstrap.test.js +90 -0
  8. package/dist/__tests__/deferred.test.d.ts +1 -0
  9. package/dist/__tests__/deferred.test.js +76 -0
  10. package/dist/__tests__/event-proxy.test.d.ts +1 -0
  11. package/dist/__tests__/event-proxy.test.js +37 -0
  12. package/dist/__tests__/lambda-http-proxy.test.d.ts +1 -0
  13. package/dist/__tests__/lambda-http-proxy.test.js +135 -0
  14. package/dist/src/assert.d.ts +1 -0
  15. package/dist/src/assert.js +9 -0
  16. package/dist/src/cloudformation-dynamodb-table.d.ts +13 -0
  17. package/dist/src/cloudformation-dynamodb-table.js +45 -0
  18. package/dist/src/cloudformation-event-proxy.d.ts +13 -0
  19. package/dist/src/cloudformation-event-proxy.js +25 -0
  20. package/dist/src/cloudformation-lambda-http-proxy.d.ts +14 -0
  21. package/dist/src/cloudformation-lambda-http-proxy.js +62 -0
  22. package/dist/src/cloudformation.d.ts +28 -0
  23. package/dist/src/cloudformation.js +50 -0
  24. package/dist/src/config.d.ts +31 -0
  25. package/dist/src/config.js +2 -0
  26. package/dist/src/container.d.ts +18 -0
  27. package/dist/src/container.js +33 -0
  28. package/dist/src/deferred.d.ts +4 -0
  29. package/dist/src/deferred.js +45 -0
  30. package/dist/src/dev-server.d.ts +19 -0
  31. package/dist/src/dev-server.js +63 -0
  32. package/dist/src/dynamodb.d.ts +16 -0
  33. package/dist/src/dynamodb.js +48 -0
  34. package/dist/src/event-proxy.d.ts +13 -0
  35. package/dist/src/event-proxy.js +68 -0
  36. package/dist/src/factories.d.ts +3 -0
  37. package/dist/src/factories.js +16 -0
  38. package/dist/src/http-server.d.ts +25 -0
  39. package/dist/src/http-server.js +37 -0
  40. package/dist/src/index.d.ts +24 -0
  41. package/dist/src/index.js +46 -0
  42. package/dist/src/internal-queue.d.ts +11 -0
  43. package/dist/src/internal-queue.js +53 -0
  44. package/dist/src/lambda-http-proxy.d.ts +28 -0
  45. package/dist/src/lambda-http-proxy.js +50 -0
  46. package/dist/src/localstack.d.ts +11 -0
  47. package/dist/src/localstack.js +62 -0
  48. package/dist/src/mappers.d.ts +25 -0
  49. package/dist/src/mappers.js +176 -0
  50. package/dist/src/pre-traffic-hooks.d.ts +2 -0
  51. package/dist/src/pre-traffic-hooks.js +19 -0
  52. package/dist/src/responses.d.ts +5 -0
  53. package/dist/src/responses.js +25 -0
  54. package/dist/src/s3.d.ts +7 -0
  55. package/dist/src/s3.js +20 -0
  56. package/dist/src/scheduled-tasks.d.ts +6 -0
  57. package/dist/src/scheduled-tasks.js +20 -0
  58. package/dist/src/services.d.ts +22 -0
  59. package/dist/src/services.js +26 -0
  60. package/dist/src/sns-http-proxy.d.ts +28 -0
  61. package/dist/src/sns-http-proxy.js +66 -0
  62. package/dist/src/sns.d.ts +15 -0
  63. package/dist/src/sns.js +35 -0
  64. package/dist/src/sqs.d.ts +13 -0
  65. package/dist/src/sqs.js +33 -0
  66. package/dist/src/stoppable.d.ts +2 -0
  67. package/dist/src/stoppable.js +15 -0
  68. package/dist/src/tunnel.d.ts +10 -0
  69. package/dist/src/tunnel.js +52 -0
  70. package/jest.config.js +7 -0
  71. package/package.json +2 -5
  72. package/src/assert.ts +4 -0
  73. package/src/cloudformation-dynamodb-table.ts +97 -0
  74. package/src/cloudformation-event-proxy.ts +61 -0
  75. package/src/cloudformation-lambda-http-proxy.ts +125 -0
  76. package/src/cloudformation.ts +95 -0
  77. package/src/config.ts +34 -0
  78. package/src/container.ts +82 -0
  79. package/src/deferred.ts +60 -0
  80. package/src/dev-server.ts +78 -0
  81. package/src/dynamodb.ts +62 -0
  82. package/src/event-proxy.ts +101 -0
  83. package/src/factories.ts +19 -0
  84. package/src/http-server.ts +59 -0
  85. package/src/index.ts +32 -0
  86. package/src/internal-queue.ts +89 -0
  87. package/src/lambda-http-proxy.ts +111 -0
  88. package/src/localstack.ts +74 -0
  89. package/src/mappers.ts +231 -0
  90. package/src/pre-traffic-hooks.ts +24 -0
  91. package/src/responses.ts +28 -0
  92. package/src/s3.ts +24 -0
  93. package/src/scheduled-tasks.ts +31 -0
  94. package/src/services.ts +46 -0
  95. package/src/sns-http-proxy.ts +109 -0
  96. package/src/sns.ts +49 -0
  97. package/src/sqs.ts +46 -0
  98. package/src/stoppable.ts +10 -0
  99. package/src/tunnel.ts +32 -0
  100. package/tsconfig.json +9 -0
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
+ }
@@ -0,0 +1,31 @@
1
+ import { Logger, LoggerFactory } from '@yopdev/logging';
2
+ import { clearInterval } from 'timers';
3
+ import { Lifecycle, Service } from './services';
4
+
5
+ export const newScheduledTasks = (
6
+ name: string,
7
+ schedules: Rate[],
8
+ ) => new Service(new ScheduledTasks(name, schedules))
9
+
10
+ class ScheduledTasks implements Lifecycle<void> {
11
+ private LOGGER: Logger
12
+
13
+ constructor(readonly name: string, private readonly schedules: Rate[]) {
14
+ this.LOGGER = LoggerFactory.create(`SCHEDULER[${name}]`)
15
+ this.LOGGER.info('registered %i scheduled tasks', schedules.length);
16
+ }
17
+
18
+ intervals: NodeJS.Timeout[] = [];
19
+
20
+ start = async () => Promise.resolve(
21
+ this.schedules.forEach((schedule) => this.intervals.push(setInterval(schedule.task, schedule.frequency * 1000)))
22
+ )
23
+ .then(() => undefined)
24
+
25
+ stop = async () => Promise.resolve(this.intervals.forEach((interval) => clearInterval(interval)))
26
+ }
27
+
28
+ export type Rate = {
29
+ frequency: number;
30
+ task: () => Promise<unknown>;
31
+ }
@@ -0,0 +1,46 @@
1
+ import { Logger, LoggerFactory } from "@yopdev/logging";
2
+ import { DevServerConfig } from "./config";
3
+
4
+ export interface Lifecycle<I> extends Startable<I>, Stoppable {
5
+ name: string
6
+ }
7
+
8
+ export interface Startable<T> {
9
+ start(config: DevServerConfig): Promise<T>
10
+ }
11
+ export interface Stoppable {
12
+ stop(): Promise<void>;
13
+ }
14
+
15
+ export type Callback<I> = (instance: I) => Promise<void>
16
+
17
+ export class Service<I> implements Lifecycle<I> {
18
+ private readonly DEFAULT_CALLBACK: Callback<unknown> = () => Promise.resolve(this.LOGGER.debug('no callback'));
19
+
20
+ readonly name: string
21
+ readonly start: (config: DevServerConfig) => Promise<I>
22
+ readonly stop: () => Promise<void>
23
+ private doStop: () => Promise<void>
24
+ private readonly LOGGER: Logger
25
+
26
+ constructor(service: Lifecycle<I>, private readonly callback?: Callback<I>) {
27
+ this.name = service.name
28
+ this.LOGGER = LoggerFactory.create(`SERVICE[${this.name}]`)
29
+ const stop = async () => service.stop()
30
+ this.start = (config: DevServerConfig) => service.start(config)
31
+ .catch((error) => stop()
32
+ .then(() => this.doStop = () => Promise.resolve(this.LOGGER.error(error, 'failed')))
33
+ .then(() => Promise.reject(error))
34
+ )
35
+ .then((started) => this
36
+ .callbackOrDefault(started)
37
+ .then(() => this.LOGGER.info('started'))
38
+ .then(() => started)
39
+ )
40
+ this.doStop = () => stop()
41
+ .then(() => this.LOGGER.info('stopped'))
42
+ this.stop = () => this.doStop();
43
+ }
44
+
45
+ private callbackOrDefault = async (instance: I) => (this.callback ?? this.DEFAULT_CALLBACK)(instance)
46
+ }
@@ -0,0 +1,109 @@
1
+ import { Logger, LoggerFactory } from "@yopdev/logging";
2
+ import { HttpServer, HttpSettings } from "./http-server";
3
+ import { Sns } from "./sns";
4
+ import { IncomingMessage, ServerResponse } from "http";
5
+ import { MessageAttributeValue } from "@aws-sdk/client-sns";
6
+ import { internalServerError, writeResponse } from "./responses";
7
+ import { Lifecycle, Service, Callback } from "./services";
8
+ import { DevServerConfig } from "./config";
9
+ import { assertNotUndefined } from "./assert";
10
+
11
+ export const newSnsHttpProxy = (
12
+ name: string,
13
+ config: {
14
+ settings: HttpSettings,
15
+ method: string,
16
+ subject?: string,
17
+ topic?: string,
18
+ },
19
+ callback?: Callback<string>,
20
+ ) => new Service(new SnsHttpProxy(name, config.settings, config.method, config.subject, config.topic), callback)
21
+
22
+ export class SnsHttpProxy implements Lifecycle<string> {
23
+ private LOGGER: Logger
24
+ private server: HttpServer
25
+ private config: {
26
+ topic: string
27
+ sns: Sns
28
+ } | undefined
29
+
30
+ constructor(
31
+ readonly name: string,
32
+ private readonly settings: HttpSettings,
33
+ private readonly method: string,
34
+ private readonly subject?: string,
35
+ private readonly topic?: string,
36
+ ) {
37
+ this.server = new HttpServer(
38
+ this.name,
39
+ this.settings,
40
+ this.handler(this.method, this.subject)
41
+ )
42
+ this.LOGGER = LoggerFactory.create(`HTTP->SNS[${name}]`)
43
+ this.LOGGER.info(
44
+ 'forwarding %s %s events to %s',
45
+ subject ? `events with ${subject} subject` : 'all',
46
+ method,
47
+ topic ?? 'the event bus',
48
+ );
49
+ }
50
+
51
+ start = (config: DevServerConfig) => Promise.resolve(this.server)
52
+ .then(server => server.start())
53
+ .tap(() => {
54
+ this.config = {
55
+ topic: this.topic ?? config.eventsProxy.topic.arn,
56
+ sns: config.sns
57
+ }
58
+ })
59
+ .then((endpoint) => endpoint)
60
+
61
+ stop = () => this.onlyWhenStarted(this.server).stop()
62
+ .then(() => this.LOGGER.debug('stopped'))
63
+
64
+ handler = (method: string, subject?: string) =>
65
+ (request: IncomingMessage, body: string, response: ServerResponse) =>
66
+ Promise.resolve((assertNotUndefined(this.config)))
67
+ .then((config) =>
68
+ config.sns
69
+ .publish(
70
+ { arn: config.topic },
71
+ this.extractMessage(method, request, body),
72
+ subject,
73
+ this.extractMessageAttributes(request)
74
+ )
75
+ .then(() => writeResponse(response, 200, ''))
76
+ .catch((e) => {
77
+ this.LOGGER.error(e, 'request failed to execute');
78
+ internalServerError(response, e.body);
79
+ })
80
+ )
81
+
82
+ extractMessage = (method: string, request: IncomingMessage, body: string) => {
83
+ switch (method) {
84
+ case 'GET':
85
+ return this.extractQueryString(request) || '';
86
+ case 'POST':
87
+ return body;
88
+ default:
89
+ throw new Error();
90
+ }
91
+ }
92
+
93
+ private onlyWhenStarted = (server: HttpServer | undefined) => assertNotUndefined(server, 'not started')
94
+
95
+ private extractQueryString = (request: IncomingMessage) => request.url?.substring(request.url?.indexOf('?') + 1)
96
+
97
+ private extractMessageAttributes = (request: IncomingMessage): Record<string, MessageAttributeValue> => {
98
+ const initialValue = {};
99
+ return Object.keys(request.headers).reduce((obj, item) => {
100
+ return {
101
+ ...obj,
102
+ [item]: {
103
+ DataType: 'String',
104
+ StringValue: request.headers[item],
105
+ },
106
+ };
107
+ }, initialValue);
108
+ }
109
+ }
package/src/sns.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { CreateTopicCommand, MessageAttributeValue, PublishCommand, SNSClient, SubscribeCommand } from "@aws-sdk/client-sns";
2
+ import { AwsConfig } from "./config";
3
+ import { assertNotUndefined } from "./assert";
4
+ import { LoggerFactory } from "@yopdev/logging";
5
+
6
+ const LOGGER = LoggerFactory.create('SNS')
7
+ export class Sns {
8
+ readonly client: SNSClient;
9
+
10
+ constructor(aws: AwsConfig) {
11
+ this.client = new SNSClient(aws);
12
+ }
13
+
14
+ createTopic = async (topic: string) => this.client
15
+ .send(
16
+ new CreateTopicCommand({
17
+ Name: topic,
18
+ }),
19
+ )
20
+ .then((o) => assertNotUndefined(o.TopicArn, 'TopicArn is not present'))
21
+ .tap((arn) => LOGGER.debug('topic %o created', arn))
22
+
23
+ createSubscription = async (topic: { arn: string }, queue: { arn: string }) => this.client
24
+ .send(
25
+ new SubscribeCommand({
26
+ TopicArn: topic.arn,
27
+ Protocol: 'sqs',
28
+ Endpoint: queue.arn,
29
+ }),
30
+ )
31
+ .then(() => LOGGER.debug('subscription %s->%s created', topic.arn, queue.arn))
32
+
33
+ publish = async (
34
+ topic: { arn: string },
35
+ body: string,
36
+ subject?: string,
37
+ attributes?: Record<string, MessageAttributeValue>,
38
+ ) => this.client
39
+ .send(
40
+ new PublishCommand({
41
+ TopicArn: topic.arn,
42
+ Message: body,
43
+ MessageAttributes: attributes,
44
+ Subject: subject,
45
+ }),
46
+ )
47
+ .then(() => LOGGER.trace('published %s[%s]', topic.arn, subject))
48
+ .then()
49
+ }
package/src/sqs.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { CreateQueueCommand, GetQueueAttributesCommand, SQSClient } from "@aws-sdk/client-sqs";
2
+ import { assertNotUndefined } from "./assert";
3
+ import { AwsConfig } from "./config";
4
+ import { LoggerFactory } from "@yopdev/logging";
5
+
6
+ const QUEUE_NAME_ATTRIBUTE_NAME = 'QueueArn'
7
+ const LOGGER = LoggerFactory.create('SQS')
8
+ export class Sqs {
9
+ readonly client: SQSClient;
10
+
11
+ constructor(config: AwsConfig) {
12
+ this.client = new SQSClient(config)
13
+ }
14
+
15
+ createStandardQueue = async (name: string) => this.createNamedQueue(name, false);
16
+ createFifoQueue = async (name: string) => this.createNamedQueue(name, true);
17
+
18
+ private createNamedQueue = async (name: string, fifo: boolean): Promise<Queue> => this.client
19
+ .send(
20
+ new CreateQueueCommand({
21
+ QueueName: fifo ? `${name}.fifo` : name,
22
+ Attributes: {
23
+ FifoQueue: fifo ? 'true' : undefined,
24
+ }
25
+ }),
26
+ )
27
+ .then(
28
+ (create) => this.client
29
+ .send(
30
+ new GetQueueAttributesCommand({
31
+ QueueUrl: create.QueueUrl,
32
+ AttributeNames: [QUEUE_NAME_ATTRIBUTE_NAME],
33
+ }),
34
+ )
35
+ .then((attributes) => ({
36
+ url: assertNotUndefined(create.QueueUrl),
37
+ arn: assertNotUndefined(attributes.Attributes)[QUEUE_NAME_ATTRIBUTE_NAME]!,
38
+ }))
39
+ )
40
+ .tap((queue) => LOGGER.debug('created %s', queue.arn))
41
+ }
42
+
43
+ export type Queue = {
44
+ arn: string;
45
+ url: string;
46
+ };
@@ -0,0 +1,10 @@
1
+ import { Consumer } from "sqs-consumer";
2
+
3
+ export const stopConsumer = async (queueWaitTimeSecs: number, target?: Consumer): Promise<void> => {
4
+ if (target) {
5
+ target.stop();
6
+ return new Promise((resolve) => {
7
+ setTimeout(resolve, queueWaitTimeSecs * 1000 + 1);
8
+ });
9
+ } else { return Promise.resolve() }
10
+ }
package/src/tunnel.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { Lifecycle, Service } from './services';
2
+ import { Logger, LoggerFactory } from '@yopdev/logging';
3
+ import * as ngrok from '@ngrok/ngrok';
4
+ import { assertNotUndefined } from './assert';
5
+
6
+ export const terminate = async () => ngrok.kill();
7
+ export const newTunnel = (config: { name: string; port: number; fixed?: { name: string; token: string } }) =>
8
+ new Service(new Tunnel(config.name, config.port, config.fixed));
9
+ class Tunnel implements Lifecycle<string> {
10
+ private logger: Logger;
11
+ constructor(
12
+ readonly name: string,
13
+ private readonly port: number,
14
+ private readonly domain?: {
15
+ name: string,
16
+ token: string,
17
+ },
18
+ ) {
19
+ this.logger = LoggerFactory.create(`TUNNEL[${name}]`);
20
+ }
21
+ start = async () =>
22
+ ngrok
23
+ .forward({
24
+ proto: 'http',
25
+ domain: this.domain?.name,
26
+ addr: this.port,
27
+ authtoken: this.domain?.token,
28
+ })
29
+ .then((listener) => assertNotUndefined(listener.url()))
30
+ .tap((url) => this.logger.debug(url))
31
+ stop = async () => ngrok.disconnect();
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "NodeNext",
4
+ "target": "ES2021",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "./dist"
8
+ }
9
+ }