@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.
- package/.github/workflows/npm-publish.yml +33 -0
- package/__tests__/bootstrap.test.ts +89 -0
- package/__tests__/deferred.test.ts +86 -0
- package/__tests__/event-proxy.test.ts +42 -0
- package/__tests__/lambda-http-proxy.test.ts +179 -0
- package/jest.config.js +7 -0
- package/package.json +2 -5
- package/src/assert.ts +4 -0
- package/src/cloudformation-dynamodb-table.ts +97 -0
- package/src/cloudformation-event-proxy.ts +61 -0
- package/src/cloudformation-lambda-http-proxy.ts +125 -0
- package/src/cloudformation.ts +95 -0
- package/src/config.ts +34 -0
- package/src/container.ts +82 -0
- package/src/deferred.ts +60 -0
- package/src/dev-server.ts +78 -0
- package/src/dynamodb.ts +62 -0
- package/src/event-proxy.ts +101 -0
- package/src/factories.ts +19 -0
- package/src/http-server.ts +59 -0
- package/src/index.ts +32 -0
- package/src/internal-queue.ts +89 -0
- package/src/lambda-http-proxy.ts +111 -0
- package/src/localstack.ts +74 -0
- package/src/mappers.ts +231 -0
- package/src/pre-traffic-hooks.ts +24 -0
- package/src/responses.ts +28 -0
- package/src/s3.ts +24 -0
- package/src/scheduled-tasks.ts +31 -0
- package/src/services.ts +46 -0
- package/src/sns-http-proxy.ts +109 -0
- package/src/sns.ts +49 -0
- package/src/sqs.ts +46 -0
- package/src/stoppable.ts +10 -0
- package/src/tunnel.ts +32 -0
- package/tsconfig.json +9 -0
- package/dist/src/assert.d.ts +0 -1
- package/dist/src/assert.js +0 -9
- package/dist/src/cloudformation-dynamodb-table.d.ts +0 -13
- package/dist/src/cloudformation-dynamodb-table.js +0 -45
- package/dist/src/cloudformation-event-proxy.d.ts +0 -13
- package/dist/src/cloudformation-event-proxy.js +0 -25
- package/dist/src/cloudformation-lambda-http-proxy.d.ts +0 -14
- package/dist/src/cloudformation-lambda-http-proxy.js +0 -62
- package/dist/src/cloudformation.d.ts +0 -28
- package/dist/src/cloudformation.js +0 -50
- package/dist/src/config.d.ts +0 -31
- package/dist/src/config.js +0 -2
- package/dist/src/container.d.ts +0 -18
- package/dist/src/container.js +0 -33
- package/dist/src/deferred.d.ts +0 -4
- package/dist/src/deferred.js +0 -45
- package/dist/src/dev-server.d.ts +0 -19
- package/dist/src/dev-server.js +0 -63
- package/dist/src/dynamodb.d.ts +0 -16
- package/dist/src/dynamodb.js +0 -48
- package/dist/src/event-proxy.d.ts +0 -13
- package/dist/src/event-proxy.js +0 -68
- package/dist/src/factories.d.ts +0 -3
- package/dist/src/factories.js +0 -16
- package/dist/src/http-server.d.ts +0 -25
- package/dist/src/http-server.js +0 -37
- package/dist/src/index.d.ts +0 -24
- package/dist/src/index.js +0 -46
- package/dist/src/internal-queue.d.ts +0 -11
- package/dist/src/internal-queue.js +0 -53
- package/dist/src/lambda-http-proxy.d.ts +0 -27
- package/dist/src/lambda-http-proxy.js +0 -49
- package/dist/src/localstack.d.ts +0 -11
- package/dist/src/localstack.js +0 -62
- package/dist/src/mappers.d.ts +0 -25
- package/dist/src/mappers.js +0 -158
- package/dist/src/pre-traffic-hooks.d.ts +0 -2
- package/dist/src/pre-traffic-hooks.js +0 -19
- package/dist/src/responses.d.ts +0 -5
- package/dist/src/responses.js +0 -22
- package/dist/src/s3.d.ts +0 -7
- package/dist/src/s3.js +0 -20
- package/dist/src/scheduled-tasks.d.ts +0 -6
- package/dist/src/scheduled-tasks.js +0 -20
- package/dist/src/services.d.ts +0 -22
- package/dist/src/services.js +0 -26
- package/dist/src/sns-http-proxy.d.ts +0 -28
- package/dist/src/sns-http-proxy.js +0 -66
- package/dist/src/sns.d.ts +0 -15
- package/dist/src/sns.js +0 -35
- package/dist/src/sqs.d.ts +0 -13
- package/dist/src/sqs.js +0 -33
- package/dist/src/stoppable.d.ts +0 -2
- package/dist/src/stoppable.js +0 -15
- package/dist/src/tunnel.d.ts +0 -10
- package/dist/src/tunnel.js +0 -52
|
@@ -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
|
+
}
|
package/src/services.ts
ADDED
|
@@ -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
|
+
};
|
package/src/stoppable.ts
ADDED
|
@@ -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
package/dist/src/assert.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function assertNotUndefined<T>(value: T | undefined, message?: string): T;
|
package/dist/src/assert.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.assertNotUndefined = void 0;
|
|
4
|
-
function assertNotUndefined(value, message = 'value is undefined') {
|
|
5
|
-
if (value === undefined)
|
|
6
|
-
throw new Error(message);
|
|
7
|
-
return value;
|
|
8
|
-
}
|
|
9
|
-
exports.assertNotUndefined = assertNotUndefined;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Callback, Service } from './services';
|
|
2
|
-
export declare const newDynamoDbTableFromCloudFormationTemplate: (name: string, config: DynamoDbTableCloudFormationConfig, callback?: Callback<string>) => Service<string>;
|
|
3
|
-
type DynamoDbTableCloudFormationConfig = {
|
|
4
|
-
template: (name: string) => string;
|
|
5
|
-
resource: string;
|
|
6
|
-
tableName: (name: string) => string;
|
|
7
|
-
throughput: Throughput;
|
|
8
|
-
};
|
|
9
|
-
type Throughput = {
|
|
10
|
-
ReadCapacityUnits: number;
|
|
11
|
-
WriteCapacityUnits: number;
|
|
12
|
-
};
|
|
13
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.newDynamoDbTableFromCloudFormationTemplate = void 0;
|
|
4
|
-
const services_1 = require("./services");
|
|
5
|
-
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
6
|
-
const js_yaml_cloudformation_schema_1 = require("js-yaml-cloudformation-schema");
|
|
7
|
-
const js_yaml_1 = require("js-yaml");
|
|
8
|
-
const fs_1 = require("fs");
|
|
9
|
-
const logging_1 = require("@yopdev/logging");
|
|
10
|
-
const dynamodb_1 = require("./dynamodb");
|
|
11
|
-
const newDynamoDbTableFromCloudFormationTemplate = (name, config, callback) => new services_1.Service(new DynamoDbTableCloudFormation(name, config.template, config.resource, config.tableName, config.throughput), callback);
|
|
12
|
-
exports.newDynamoDbTableFromCloudFormationTemplate = newDynamoDbTableFromCloudFormationTemplate;
|
|
13
|
-
class DynamoDbTableCloudFormation {
|
|
14
|
-
constructor(name, template, resource, tableName, throughput) {
|
|
15
|
-
this.name = name;
|
|
16
|
-
this.template = template;
|
|
17
|
-
this.resource = resource;
|
|
18
|
-
this.throughput = throughput;
|
|
19
|
-
this.start = (config) => Promise.resolve(this.parseTableDefinition(this.template(this.name))
|
|
20
|
-
.then((definition) => definition[this.resource].Properties)
|
|
21
|
-
.then((resource) => this.createTableCommand(resource, this.throughput)
|
|
22
|
-
.then((command) => (0, dynamodb_1.newDynamoDbTable)(this.name, {
|
|
23
|
-
command: command,
|
|
24
|
-
ttlAttribute: resource.TimeToLiveSpecification?.AttributeName,
|
|
25
|
-
}))))
|
|
26
|
-
.tap(() => this.LOGGER.info('configured'))
|
|
27
|
-
.then((service) => service.start(config));
|
|
28
|
-
this.stop = () => Promise.resolve();
|
|
29
|
-
this.createTableCommand = async (definition, throughput) => new client_dynamodb_1.CreateTableCommand({
|
|
30
|
-
TableName: definition.TableName ?? this.tableName(this.name),
|
|
31
|
-
KeySchema: definition.KeySchema,
|
|
32
|
-
AttributeDefinitions: definition.AttributeDefinitions,
|
|
33
|
-
GlobalSecondaryIndexes: definition.GlobalSecondaryIndexes?.map((gsi) => ({
|
|
34
|
-
...gsi,
|
|
35
|
-
ProvisionedThroughput: throughput,
|
|
36
|
-
})),
|
|
37
|
-
ProvisionedThroughput: throughput,
|
|
38
|
-
});
|
|
39
|
-
this.parseTableDefinition = async (path) => (0, js_yaml_1.load)((0, fs_1.readFileSync)(path).toString(), {
|
|
40
|
-
schema: js_yaml_cloudformation_schema_1.CLOUDFORMATION_SCHEMA,
|
|
41
|
-
}).Resources;
|
|
42
|
-
this.LOGGER = logging_1.LoggerFactory.create(`CFN:DYNAMO[${name}]`);
|
|
43
|
-
this.tableName = tableName ? tableName : (name) => { throw new Error(`table name not specified on ${name}`); };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { SQSRecord } from 'aws-lambda';
|
|
2
|
-
import { CloudFormationSetupConfig } from './cloudformation';
|
|
3
|
-
import { EventHandler } from './event-proxy';
|
|
4
|
-
import { DevServerConfig } from './config';
|
|
5
|
-
import { Startable } from './services';
|
|
6
|
-
import { Message } from '@aws-sdk/client-sqs';
|
|
7
|
-
export declare const snsEventsFromCloudFormationTemplate: (name: string, config: CloudFormationEventsProxyConfig) => Startable<EventHandler[]>;
|
|
8
|
-
type CloudFormationEventsProxyConfig = {
|
|
9
|
-
topic?: string;
|
|
10
|
-
mapper?: (message: Message) => SQSRecord;
|
|
11
|
-
prepare?: (config: DevServerConfig) => Promise<void>;
|
|
12
|
-
} & CloudFormationSetupConfig;
|
|
13
|
-
export {};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.snsEventsFromCloudFormationTemplate = void 0;
|
|
4
|
-
const cloudformation_1 = require("./cloudformation");
|
|
5
|
-
const snsEventsFromCloudFormationTemplate = (name, config) => new CloudFormationEventsProxy(name, config);
|
|
6
|
-
exports.snsEventsFromCloudFormationTemplate = snsEventsFromCloudFormationTemplate;
|
|
7
|
-
class CloudFormationEventsProxy extends cloudformation_1.CloudFormationSetup {
|
|
8
|
-
constructor(name, config) {
|
|
9
|
-
super(name, (event) => event.Type === 'SNS', async (event, handler) => ({
|
|
10
|
-
name: handler.name,
|
|
11
|
-
handler: handler,
|
|
12
|
-
matcher: (_, attributes) => {
|
|
13
|
-
const required = Object.entries(event.Properties.FilterPolicy ?? []).flatMap((entry) => entry[1].map((value) => value + entry[0]));
|
|
14
|
-
if (required.length == 0)
|
|
15
|
-
return true;
|
|
16
|
-
const actual = Object.entries(attributes).flatMap((entry) => entry[1].Value + entry[0]);
|
|
17
|
-
return required.filter((e) => actual.includes(e)).length > 0;
|
|
18
|
-
},
|
|
19
|
-
}), config.prepare, config);
|
|
20
|
-
this.afterStart = (_name, _config, routes) => routes;
|
|
21
|
-
}
|
|
22
|
-
logHandler(handler) {
|
|
23
|
-
return `detected sns handler ${handler.name}`;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { CloudFormationSetupConfig } from './cloudformation';
|
|
2
|
-
import { DevServerConfig } from './config';
|
|
3
|
-
import { HttpSettings } from './http-server';
|
|
4
|
-
import { Authorizer, Route } from './lambda-http-proxy';
|
|
5
|
-
import { Callback, Service, Startable } from './services';
|
|
6
|
-
import { LambdaPayloadVersion } from './mappers';
|
|
7
|
-
export declare const newLambdaProxyFromCloudFormationTemplate: <Context, AuthorizerContext, Event_1, HandlerResponse>(name: string, settings: HttpSettings, payloadVersion: LambdaPayloadVersion<AuthorizerContext, Event_1, HandlerResponse>, config: CloudFormationLambdaProxyConfig<Context, AuthorizerContext, Event_1, HandlerResponse>, callback: Callback<string>) => Startable<Service<string>>;
|
|
8
|
-
type CloudFormationLambdaProxyConfig<Context, AuthorizerContext, Event, HandlerResponse> = {
|
|
9
|
-
authorizer?: (config: DevServerConfig) => Authorizer<AuthorizerContext> | undefined;
|
|
10
|
-
extraRoutes: Route<Context, AuthorizerContext, Event, HandlerResponse>[];
|
|
11
|
-
prepare?: (config: DevServerConfig) => Promise<void>;
|
|
12
|
-
context?: () => Context;
|
|
13
|
-
} & CloudFormationSetupConfig;
|
|
14
|
-
export {};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.newLambdaProxyFromCloudFormationTemplate = void 0;
|
|
4
|
-
const cloudformation_1 = require("./cloudformation");
|
|
5
|
-
const lambda_http_proxy_1 = require("./lambda-http-proxy");
|
|
6
|
-
const PATH_VARIABLE_CAPTURE = /{(.*?)}/g;
|
|
7
|
-
const QUERY_STRING_OR_LOCATION_REG_EXP = '(?:([?#].*))?';
|
|
8
|
-
const PROXY_PATH_PARAM = 'proxy';
|
|
9
|
-
const newLambdaProxyFromCloudFormationTemplate = (name, settings, payloadVersion, config, callback) => new CloudFormationLambdaProxy(name, settings, config.extraRoutes, payloadVersion, config, callback);
|
|
10
|
-
exports.newLambdaProxyFromCloudFormationTemplate = newLambdaProxyFromCloudFormationTemplate;
|
|
11
|
-
class CloudFormationLambdaProxy extends cloudformation_1.CloudFormationSetup {
|
|
12
|
-
constructor(name, settings, extraRoutes, payloadVersion, config, callback) {
|
|
13
|
-
super(name, (event) => event.Type === 'HttpApi', async (event, handler) => Promise.resolve(this.pathWithCapturingGroups(event.Properties.Path)).then((pathWithCapturingGroups) => ({
|
|
14
|
-
method: new RegExp(this.method(event.Properties.Method)),
|
|
15
|
-
path: new RegExp(`^${pathWithCapturingGroups}${QUERY_STRING_OR_LOCATION_REG_EXP}?$`),
|
|
16
|
-
weight: this.computeWeight(pathWithCapturingGroups),
|
|
17
|
-
handler: this.pathParameterCapture(new RegExp(pathWithCapturingGroups), handler, config.context),
|
|
18
|
-
})), config.prepare, config);
|
|
19
|
-
this.settings = settings;
|
|
20
|
-
this.extraRoutes = extraRoutes;
|
|
21
|
-
this.callback = callback;
|
|
22
|
-
this.afterStart = (name, cfg, routes) => (0, lambda_http_proxy_1.newLambdaHttpProxy)(`${name}:apigw`, {
|
|
23
|
-
settings: this.settings,
|
|
24
|
-
routes: this.extraRoutes.concat(routes),
|
|
25
|
-
authorizer: (this.authorizer || (() => undefined))(cfg),
|
|
26
|
-
mapper: this.mapper,
|
|
27
|
-
}, this.callback);
|
|
28
|
-
this.computeWeight = (path) => this.segments(path) - this.variables(path);
|
|
29
|
-
this.segments = (path) => path.split('/').length;
|
|
30
|
-
this.variables = (path) => path.split('(?<').length;
|
|
31
|
-
this.pathWithCapturingGroups = (source) => [...source.matchAll(PATH_VARIABLE_CAPTURE)].reduce((prev, curr) => prev.replaceAll(curr[0], `(?<${this.normalizeCaptureGroupName(curr[1]).replaceAll("+", "")}>.*)`), source);
|
|
32
|
-
this.normalizeCaptureGroupName = (original) => (original === 'proxy+' ? PROXY_PATH_PARAM : original);
|
|
33
|
-
this.pathParameterCapture = (pathWithCapturingGroups, handler, context) => async (event) => {
|
|
34
|
-
const pathParameters = pathWithCapturingGroups.exec(this.pathParameterResolver.locate(event))?.groups;
|
|
35
|
-
this.pathParameterResolver.store(event, Object.fromEntries((Object.entries(pathParameters ?? {}))
|
|
36
|
-
.map(([k, v]) => [k, decodeURIComponent(v)])));
|
|
37
|
-
return handler(event, context?.());
|
|
38
|
-
};
|
|
39
|
-
this.method = (cloudFormationValue) => {
|
|
40
|
-
switch (cloudFormationValue) {
|
|
41
|
-
case 'ANY':
|
|
42
|
-
return '.*';
|
|
43
|
-
case 'HEAD':
|
|
44
|
-
case 'OPTIONS':
|
|
45
|
-
case 'GET':
|
|
46
|
-
case 'POST':
|
|
47
|
-
case 'PUT':
|
|
48
|
-
case 'DELETE':
|
|
49
|
-
case 'PATCH':
|
|
50
|
-
return cloudFormationValue;
|
|
51
|
-
default:
|
|
52
|
-
throw new Error(`unsupported method ${cloudFormationValue}`);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
this.mapper = payloadVersion.mapper;
|
|
56
|
-
this.authorizer = config?.authorizer;
|
|
57
|
-
this.pathParameterResolver = payloadVersion.resolver;
|
|
58
|
-
}
|
|
59
|
-
logHandler(handler) {
|
|
60
|
-
return `detected handler for ${handler.method} on ${handler.path} with weight ${handler.weight}`;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { DevServerConfig } from './config';
|
|
2
|
-
import { Startable } from './services';
|
|
3
|
-
export declare abstract class CloudFormationSetup<P, H, R, F> implements Startable<F> {
|
|
4
|
-
readonly name: string;
|
|
5
|
-
private readonly eventFilter;
|
|
6
|
-
private readonly handlerResolver;
|
|
7
|
-
private readonly beforeStart;
|
|
8
|
-
private readonly template;
|
|
9
|
-
private readonly handlers;
|
|
10
|
-
private readonly handlerNameResolver;
|
|
11
|
-
private readonly LOGGER;
|
|
12
|
-
constructor(name: string, eventFilter: (event: Event<P>) => boolean, handlerResolver: (event: Event<P>, handler: H) => Promise<R>, beforeStart: ((config: DevServerConfig) => Promise<void>) | undefined, config: CloudFormationSetupConfig);
|
|
13
|
-
start: (config: DevServerConfig) => Promise<F>;
|
|
14
|
-
protected abstract afterStart: (name: string, config: DevServerConfig, routes: R[]) => F;
|
|
15
|
-
private resolvedHandlers;
|
|
16
|
-
abstract logHandler(handler: R): string;
|
|
17
|
-
private parseApiEvents;
|
|
18
|
-
}
|
|
19
|
-
export type CloudFormationSetupConfig = {
|
|
20
|
-
template: (name: string) => string;
|
|
21
|
-
handlers: (name: string) => string;
|
|
22
|
-
handlerNameResolver: (name: string) => string;
|
|
23
|
-
};
|
|
24
|
-
type Event<P> = {
|
|
25
|
-
Type: string;
|
|
26
|
-
Properties: P;
|
|
27
|
-
};
|
|
28
|
-
export {};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CloudFormationSetup = void 0;
|
|
4
|
-
const js_yaml_cloudformation_schema_1 = require("js-yaml-cloudformation-schema");
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const path_1 = require("path");
|
|
7
|
-
const js_yaml_1 = require("js-yaml");
|
|
8
|
-
const logging_1 = require("@yopdev/logging");
|
|
9
|
-
class CloudFormationSetup {
|
|
10
|
-
constructor(name, eventFilter, handlerResolver, beforeStart, config) {
|
|
11
|
-
this.name = name;
|
|
12
|
-
this.eventFilter = eventFilter;
|
|
13
|
-
this.handlerResolver = handlerResolver;
|
|
14
|
-
this.beforeStart = beforeStart;
|
|
15
|
-
this.start = async (config) => (this.beforeStart || Promise.resolve)(config)
|
|
16
|
-
.then(this.resolvedHandlers)
|
|
17
|
-
.then(async (routes) => this.afterStart(this.name, config, routes));
|
|
18
|
-
this.resolvedHandlers = async () => {
|
|
19
|
-
const template = this.template(this.name);
|
|
20
|
-
this.LOGGER.info('parsing %s', template);
|
|
21
|
-
const definition = this.parseApiEvents(template);
|
|
22
|
-
const functions = this.handlers(this.name);
|
|
23
|
-
const routes = Object.values(definition)
|
|
24
|
-
.filter((resource) => resource.Type === 'AWS::Serverless::Function')
|
|
25
|
-
.filter((resource) => resource.Properties.Events)
|
|
26
|
-
.flatMap((resource) => Object.values(resource.Properties.Events).map((event) => ({
|
|
27
|
-
...event,
|
|
28
|
-
Handler: resource.Properties.Handler,
|
|
29
|
-
})))
|
|
30
|
-
.filter(this.eventFilter)
|
|
31
|
-
.map(async (event) => import(functions).then(async (handlers) => Promise.resolve(handlers[this.handlerNameResolver(event.Handler)])
|
|
32
|
-
.then(async (handler) => (handler !== undefined) ?
|
|
33
|
-
this.handlerResolver(event, handler) :
|
|
34
|
-
Promise.reject(new Error(`function ${event.Handler} defined in ${(0, path_1.resolve)(template)} not found in ${(0, path_1.resolve)(functions)}`)))
|
|
35
|
-
.then((handler) => {
|
|
36
|
-
this.LOGGER.info(this.logHandler(handler));
|
|
37
|
-
return handler;
|
|
38
|
-
})));
|
|
39
|
-
return Promise.all(routes);
|
|
40
|
-
};
|
|
41
|
-
this.parseApiEvents = (path) => (0, js_yaml_1.load)((0, fs_1.readFileSync)(path).toString(), {
|
|
42
|
-
schema: js_yaml_cloudformation_schema_1.CLOUDFORMATION_SCHEMA,
|
|
43
|
-
}).Resources;
|
|
44
|
-
this.LOGGER = logging_1.LoggerFactory.create(`CFN:FUNCTION[${name}]`);
|
|
45
|
-
this.template = config.template;
|
|
46
|
-
this.handlers = config.handlers;
|
|
47
|
-
this.handlerNameResolver = config.handlerNameResolver;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
exports.CloudFormationSetup = CloudFormationSetup;
|
package/dist/src/config.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Sqs } from "./sqs";
|
|
2
|
-
import { Sns } from "./sns";
|
|
3
|
-
import { DynamoDb } from "./dynamodb";
|
|
4
|
-
import { StartedNetwork } from "testcontainers";
|
|
5
|
-
import { S3 } from "./s3";
|
|
6
|
-
export type Config = {
|
|
7
|
-
boundServicesPort?: number;
|
|
8
|
-
localStackDockerImage?: string;
|
|
9
|
-
localStateBindMount?: string;
|
|
10
|
-
};
|
|
11
|
-
export type AwsConfig = {
|
|
12
|
-
region: string;
|
|
13
|
-
endpoint: string;
|
|
14
|
-
credentials: {
|
|
15
|
-
accessKeyId: string;
|
|
16
|
-
secretAccessKey: string;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
export type DevServerConfig = {
|
|
20
|
-
raw: AwsConfig;
|
|
21
|
-
network: StartedNetwork;
|
|
22
|
-
sqs: Sqs;
|
|
23
|
-
sns: Sns;
|
|
24
|
-
s3: S3;
|
|
25
|
-
dynamo: DynamoDb;
|
|
26
|
-
eventsProxy: {
|
|
27
|
-
topic: {
|
|
28
|
-
arn: string;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
};
|
package/dist/src/config.js
DELETED