@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
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const globals_1 = require("@jest/globals");
7
+ const src_1 = require("../src");
8
+ const lambda_http_proxy_1 = require("../src/lambda-http-proxy");
9
+ const axios_1 = __importDefault(require("axios"));
10
+ const mappers_1 = require("../src/mappers");
11
+ (0, globals_1.describe)("the lambda http proxy", () => {
12
+ const payloadVersionV2Mapper = (0, mappers_1.v2)().mapper;
13
+ (0, globals_1.it)("invokes the handler with the heaviest route", async () => {
14
+ let endpoint;
15
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
16
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
17
+ routes: [
18
+ { method: /GET/, path: /\/a\/.*\/b\/.*\/c/, weight: 3, handler: () => Promise.resolve({ statusCode: 200, body: '' }) },
19
+ { method: /GET/, path: /\/a\/.*\/b\/.*/, weight: 2, handler: () => Promise.resolve({ statusCode: 400, body: '' }) },
20
+ { method: /GET/, path: /\/a\/.*/, weight: 2, handler: () => Promise.resolve({ statusCode: 400, body: '' }) },
21
+ ],
22
+ mapper: payloadVersionV2Mapper,
23
+ }, async (url) => { endpoint = url; })).start();
24
+ return (0, globals_1.expect)(axios_1.default.get(`${endpoint}a/1/b/2/c`)
25
+ .then(r => r.status)
26
+ .finally(async () => tested.stop()))
27
+ .resolves
28
+ .toEqual(200);
29
+ });
30
+ (0, globals_1.it)("supports a single 'catch all' route", async () => {
31
+ let endpoint;
32
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
33
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
34
+ routes: [
35
+ { method: /.*/, path: /.*/, weight: 0, handler: () => Promise.resolve({ statusCode: 200, body: 'handled!' }) },
36
+ ],
37
+ mapper: payloadVersionV2Mapper,
38
+ }, async (url) => { endpoint = url; })).start();
39
+ return (0, globals_1.expect)(axios_1.default.post(`${endpoint}find-me`)
40
+ .then(r => ({ status: r.status, body: r.data }))
41
+ .finally(async () => tested.stop()))
42
+ .resolves
43
+ .toStrictEqual({ status: 200, body: 'handled!' });
44
+ });
45
+ (0, globals_1.it)("returns unauthorized when the authorizer rejects", async () => {
46
+ let endpoint;
47
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
48
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
49
+ routes: [
50
+ { method: /.*/, path: /.*/, weight: 0, handler: () => Promise.resolve({ statusCode: 200, body: 'handled!' }), authorizer: () => Promise.reject(lambda_http_proxy_1.UNAUTHORIZED) },
51
+ ],
52
+ mapper: payloadVersionV2Mapper,
53
+ }, async (url) => { endpoint = url; })).start();
54
+ return (0, globals_1.expect)(axios_1.default.get(endpoint, { validateStatus: () => true })
55
+ .then(r => r.status)
56
+ .finally(async () => tested.stop()))
57
+ .resolves
58
+ .toEqual(401);
59
+ });
60
+ (0, globals_1.it)("triggers the fallback handler when no route matches a request", async () => {
61
+ let endpoint;
62
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
63
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
64
+ routes: [
65
+ { method: /.*/, path: /\a/, weight: 0, handler: () => Promise.resolve({ statusCode: 200, body: 'handled!' }) },
66
+ ],
67
+ mapper: payloadVersionV2Mapper,
68
+ }, async (url) => { endpoint = url; })).start();
69
+ return (0, globals_1.expect)(axios_1.default.get(`${endpoint}b`, { validateStatus: () => true })
70
+ .then(r => ({ status: r.status, body: r.data }))
71
+ .finally(async () => tested.stop()))
72
+ .resolves
73
+ .toStrictEqual({ status: 404, body: 'no route found to handle GET to /b' });
74
+ });
75
+ (0, globals_1.it)("handler has access to the context", async () => {
76
+ const expectedContext = 'hello world!';
77
+ let endpoint;
78
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
79
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
80
+ routes: [
81
+ {
82
+ method: /.*/,
83
+ path: /\a/,
84
+ weight: 0,
85
+ handler: (_, context) => Promise.resolve({ statusCode: 200, body: context })
86
+ },
87
+ ],
88
+ context: () => expectedContext,
89
+ mapper: payloadVersionV2Mapper,
90
+ }, async (url) => { endpoint = url; })).start();
91
+ return (0, globals_1.expect)(axios_1.default.get(`${endpoint}a`)
92
+ .then(r => r.data)
93
+ .finally(async () => tested.stop()))
94
+ .resolves
95
+ .toStrictEqual(expectedContext);
96
+ });
97
+ (0, globals_1.it)("can set cookies", async () => {
98
+ let endpoint;
99
+ const handler = async (event, context) => {
100
+ return {
101
+ statusCode: 200,
102
+ cookies: ['galletita=criollita'],
103
+ };
104
+ };
105
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
106
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
107
+ routes: [
108
+ { method: /GET/, path: /\/a/, weight: 0, handler: handler },
109
+ ],
110
+ mapper: payloadVersionV2Mapper,
111
+ }, async (url) => { endpoint = url; })).start();
112
+ return (0, globals_1.expect)(axios_1.default.get(`${endpoint}a`)
113
+ .then(r => r.headers["set-cookie"])
114
+ .finally(async () => tested.stop())).resolves.toContain('galletita=criollita');
115
+ });
116
+ (0, globals_1.it)("can read cookies", async () => {
117
+ let endpoint;
118
+ const handler = async (event, context) => {
119
+ return {
120
+ statusCode: 200,
121
+ body: event["headers"].cookie,
122
+ };
123
+ };
124
+ const tested = await new src_1.DevServer('lambda-http-proxy', (0, lambda_http_proxy_1.newLambdaHttpProxy)('service', {
125
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
126
+ routes: [
127
+ { method: /GET/, path: /\/a/, weight: 0, handler: handler },
128
+ ],
129
+ mapper: payloadVersionV2Mapper,
130
+ }, async (url) => { endpoint = url; })).start();
131
+ return (0, globals_1.expect)(axios_1.default.get(`${endpoint}a`, { headers: { cookie: ['galletita=criollita'] } })
132
+ .then(r => r.data)
133
+ .finally(async () => tested.stop())).resolves.toContain('galletita=criollita');
134
+ });
135
+ });
@@ -0,0 +1 @@
1
+ export declare function assertNotUndefined<T>(value: T | undefined, message?: string): T;
@@ -0,0 +1,9 @@
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;
@@ -0,0 +1,13 @@
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 {};
@@ -0,0 +1,45 @@
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
+ }
@@ -0,0 +1,13 @@
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 {};
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,14 @@
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 {};
@@ -0,0 +1,62 @@
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
+ }
@@ -0,0 +1,28 @@
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 {};
@@ -0,0 +1,50 @@
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;
@@ -0,0 +1,31 @@
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
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,18 @@
1
+ import { BindMount, Environment } from 'testcontainers/build/types';
2
+ import { Callback, Service } from './services';
3
+ import { PortWithOptionalBinding } from 'testcontainers/build/utils/port';
4
+ import { Readable } from 'stream';
5
+ import { WaitStrategy } from 'testcontainers/build/wait-strategies/wait-strategy';
6
+ export declare const newContainer: (name: string, config: {
7
+ image: string;
8
+ networkAlias: string;
9
+ environment?: Environment;
10
+ bindMounts?: BindMount[];
11
+ exposedPorts?: PortWithOptionalBinding[];
12
+ logConsumer?: (stream: Readable) => unknown;
13
+ startup?: {
14
+ waitStrategy: WaitStrategy;
15
+ timeout: number;
16
+ };
17
+ endpointBuilder?: (port: number) => string;
18
+ }, callback?: Callback<string>) => Service<string>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.newContainer = void 0;
4
+ const logging_1 = require("@yopdev/logging");
5
+ const testcontainers_1 = require("testcontainers");
6
+ const services_1 = require("./services");
7
+ const newContainer = (name, config, callback) => new services_1.Service(new Container(name, config.image, config.networkAlias, config.environment, config.bindMounts, config.exposedPorts, config.logConsumer, config.startup, config.endpointBuilder), callback);
8
+ exports.newContainer = newContainer;
9
+ class Container {
10
+ constructor(name, image, networkAlias, environment, bindMounts, exposedPorts, logConsumer, startup, endpointBuilder) {
11
+ this.name = name;
12
+ this.networkAlias = networkAlias;
13
+ this.start = async (config) => Promise.resolve(this.LOGGER.info('start'))
14
+ .then(() => this.container
15
+ .withNetwork(config.network)
16
+ .withNetworkAliases(this.networkAlias)
17
+ .start())
18
+ .then((started) => this.started = started)
19
+ .then(() => this.endpointBuilder(this.started.getFirstMappedPort()))
20
+ .tap((url) => this.LOGGER.debug(url));
21
+ this.stop = async () => (this.started ?? { stop: async () => { this.LOGGER.warn('no container'); } }).stop().then(() => undefined);
22
+ this.LOGGER = logging_1.LoggerFactory.create(`CONTAINER[${name}]`);
23
+ const generic = new testcontainers_1.GenericContainer(image);
24
+ const withEnvironment = environment ? generic.withEnvironment(environment) : generic;
25
+ const withBindMounts = bindMounts ? withEnvironment.withBindMounts(bindMounts) : withEnvironment;
26
+ const withExposedPorts = exposedPorts ? withBindMounts.withExposedPorts(...exposedPorts) : withBindMounts;
27
+ const withLogConsumer = logConsumer ? withExposedPorts.withLogConsumer(logConsumer) : withExposedPorts;
28
+ this.container = startup ? withLogConsumer.withWaitStrategy(startup.waitStrategy).withStartupTimeout(startup.timeout) : withLogConsumer;
29
+ this.networkAlias = networkAlias;
30
+ this.endpointBuilder = endpointBuilder ?? ((port) => `http://localhost:${port}`);
31
+ }
32
+ }
33
+ ;
@@ -0,0 +1,4 @@
1
+ import { DevServerConfig } from "./config";
2
+ import { Service } from "./services";
3
+ export declare const lazy: <I>(lifecycle: (config: DevServerConfig) => Service<I>) => Service<I>;
4
+ export declare const promised: <I>(lifecycle: (config: DevServerConfig) => Promise<Service<I>>) => Service<I>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promised = exports.lazy = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const services_1 = require("./services");
6
+ const logging_1 = require("@yopdev/logging");
7
+ const lazy = (lifecycle) => new services_1.Service(new Lazy(lifecycle));
8
+ exports.lazy = lazy;
9
+ const promised = (lifecycle) => new services_1.Service(new Promised(lifecycle));
10
+ exports.promised = promised;
11
+ class Lazy {
12
+ constructor(configurable) {
13
+ this.configurable = configurable;
14
+ this.stop = async () => this.withStartedOr(async (s) => s.stop(), async () => Promise.reject(new Error('not started')));
15
+ this.started = undefined;
16
+ this.name = (0, crypto_1.randomUUID)();
17
+ this.LOGGER = logging_1.LoggerFactory.create(`LAZY[${this.name}]`);
18
+ }
19
+ async start(config) {
20
+ const local = this.configurable(config);
21
+ this.started = local;
22
+ this.LOGGER.info('DISCOVERY:%s', local.name);
23
+ return local.start(config);
24
+ }
25
+ withStartedOr(task, fallback) {
26
+ const local = this.started;
27
+ if (local !== undefined)
28
+ task(local);
29
+ else
30
+ fallback();
31
+ }
32
+ }
33
+ class Promised {
34
+ constructor(configurable) {
35
+ this.name = (0, crypto_1.randomUUID)();
36
+ this.LOGGER = logging_1.LoggerFactory.create(`PROMISED[${this.name}]`);
37
+ let initialized;
38
+ this.start = async (config) => configurable(config)
39
+ .catch((e) => e?.cleanup?.() ?? Promise.reject(e))
40
+ .then((service) => initialized = service)
41
+ .tap((service) => this.LOGGER.info('DISCOVERY:%s', service.name))
42
+ .then((service) => service.start(config));
43
+ this.stop = async () => (initialized ?? { stop: async () => this.LOGGER.info('not started') }).stop();
44
+ }
45
+ }
@@ -0,0 +1,19 @@
1
+ import { Config } from "./config";
2
+ import { Service } from "./services";
3
+ export declare class DevServer {
4
+ readonly name: string;
5
+ private readonly service;
6
+ private readonly config?;
7
+ private readonly eventProxyTopic;
8
+ private LOGGER;
9
+ private localStack;
10
+ private network;
11
+ constructor(name: string, service: Service<any>, config?: Config);
12
+ start: () => Promise<StartedDevServer>;
13
+ private shutdown;
14
+ }
15
+ type StartedDevServer = {
16
+ name: string;
17
+ stop: () => Promise<void>;
18
+ };
19
+ export {};
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevServer = void 0;
4
+ const testcontainers_1 = require("testcontainers");
5
+ const localstack_1 = require("./localstack");
6
+ const sqs_1 = require("./sqs");
7
+ const dynamodb_1 = require("./dynamodb");
8
+ const logging_1 = require("@yopdev/logging");
9
+ const sns_1 = require("./sns");
10
+ const tunnel_1 = require("./tunnel");
11
+ const s3_1 = require("./s3");
12
+ class DevServer {
13
+ constructor(name, service, config) {
14
+ this.name = name;
15
+ this.service = service;
16
+ this.config = config;
17
+ this.start = async () => Promise.resolve(new testcontainers_1.Network(new testcontainers_1.RandomUuid()).start())
18
+ .tap((network) => this.network = network)
19
+ .then((network) => ({
20
+ localStack: this.localStack = new localstack_1.LocalStack(network, this.config),
21
+ network: network
22
+ }))
23
+ .then((infra) => infra.localStack.start().then((config) => ({
24
+ aws: config,
25
+ network: infra.network
26
+ })))
27
+ .then((infra) => Promise
28
+ .resolve(new sns_1.Sns(infra.aws))
29
+ .then((sns) => sns
30
+ .createTopic(this.eventProxyTopic)
31
+ .then((eventProxyTopicArn) => ({
32
+ raw: infra.aws,
33
+ network: infra.network,
34
+ sqs: new sqs_1.Sqs(infra.aws),
35
+ sns: new sns_1.Sns(infra.aws),
36
+ s3: new s3_1.S3(infra.aws),
37
+ dynamo: new dynamodb_1.DynamoDb(infra.aws),
38
+ eventsProxy: {
39
+ topic: {
40
+ arn: eventProxyTopicArn
41
+ }
42
+ },
43
+ }))))
44
+ .tap((config) => this.LOGGER.debug('config is %o', config))
45
+ .then((aws) => this.service.start(aws))
46
+ .catch((e) => this.shutdown()
47
+ .then(() => this.LOGGER.error('error starting service'))
48
+ .then(() => Promise.reject(e)))
49
+ .then(() => this.LOGGER.info('started'))
50
+ .then(() => ({
51
+ name: this.name,
52
+ stop: async () => this.shutdown().then(() => this.LOGGER.info('stopped')),
53
+ }));
54
+ this.shutdown = async () => this.service.stop()
55
+ .finally(tunnel_1.terminate)
56
+ .then(() => this.localStack?.stop())
57
+ .then(() => this.network?.stop().catch((e) => this.LOGGER.warn(e, 'failed to stop network')));
58
+ this.LOGGER = logging_1.LoggerFactory.create(`DEVSERVER[${name}]`);
59
+ const encodedName = Buffer.from(name).toString('hex').substring(0, 64);
60
+ this.eventProxyTopic = `EventProxyTopic${encodedName}`;
61
+ }
62
+ }
63
+ exports.DevServer = DevServer;
@@ -0,0 +1,16 @@
1
+ import { CreateTableCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { AwsConfig } from "./config";
3
+ import { Service, Callback } from "./services";
4
+ export declare const newDynamoDbTable: (name: string, config: {
5
+ command: CreateTableCommand;
6
+ ttlAttribute?: string;
7
+ }, callback?: Callback<string>) => Service<string>;
8
+ export declare class DynamoDb {
9
+ readonly client: DynamoDBClient;
10
+ constructor(config: AwsConfig);
11
+ createTable: (factory: CreateTableCommand) => Promise<import("@aws-sdk/client-dynamodb").CreateTableCommandOutput | {
12
+ TableDescription: {
13
+ TableName: string;
14
+ };
15
+ }>;
16
+ }