@yopdev/dev-server 3.0.2-RC → 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
|
@@ -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
|
+
}
|