@yopdev/dev-server 3.0.2-RC1 → 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 (66) hide show
  1. package/.github/workflows/npm-publish.yml +1 -0
  2. package/dist/__tests__/bootstrap.test.d.ts +1 -0
  3. package/dist/__tests__/bootstrap.test.js +90 -0
  4. package/dist/__tests__/deferred.test.d.ts +1 -0
  5. package/dist/__tests__/deferred.test.js +76 -0
  6. package/dist/__tests__/event-proxy.test.d.ts +1 -0
  7. package/dist/__tests__/event-proxy.test.js +37 -0
  8. package/dist/__tests__/lambda-http-proxy.test.d.ts +1 -0
  9. package/dist/__tests__/lambda-http-proxy.test.js +135 -0
  10. package/dist/src/assert.d.ts +1 -0
  11. package/dist/src/assert.js +9 -0
  12. package/dist/src/cloudformation-dynamodb-table.d.ts +13 -0
  13. package/dist/src/cloudformation-dynamodb-table.js +45 -0
  14. package/dist/src/cloudformation-event-proxy.d.ts +13 -0
  15. package/dist/src/cloudformation-event-proxy.js +25 -0
  16. package/dist/src/cloudformation-lambda-http-proxy.d.ts +14 -0
  17. package/dist/src/cloudformation-lambda-http-proxy.js +62 -0
  18. package/dist/src/cloudformation.d.ts +28 -0
  19. package/dist/src/cloudformation.js +50 -0
  20. package/dist/src/config.d.ts +31 -0
  21. package/dist/src/config.js +2 -0
  22. package/dist/src/container.d.ts +18 -0
  23. package/dist/src/container.js +33 -0
  24. package/dist/src/deferred.d.ts +4 -0
  25. package/dist/src/deferred.js +45 -0
  26. package/dist/src/dev-server.d.ts +19 -0
  27. package/dist/src/dev-server.js +63 -0
  28. package/dist/src/dynamodb.d.ts +16 -0
  29. package/dist/src/dynamodb.js +48 -0
  30. package/dist/src/event-proxy.d.ts +13 -0
  31. package/dist/src/event-proxy.js +68 -0
  32. package/dist/src/factories.d.ts +3 -0
  33. package/dist/src/factories.js +16 -0
  34. package/dist/src/http-server.d.ts +25 -0
  35. package/dist/src/http-server.js +37 -0
  36. package/dist/src/index.d.ts +24 -0
  37. package/dist/src/index.js +46 -0
  38. package/dist/src/internal-queue.d.ts +11 -0
  39. package/dist/src/internal-queue.js +53 -0
  40. package/dist/src/lambda-http-proxy.d.ts +28 -0
  41. package/dist/src/lambda-http-proxy.js +50 -0
  42. package/dist/src/localstack.d.ts +11 -0
  43. package/dist/src/localstack.js +62 -0
  44. package/dist/src/mappers.d.ts +25 -0
  45. package/dist/src/mappers.js +176 -0
  46. package/dist/src/pre-traffic-hooks.d.ts +2 -0
  47. package/dist/src/pre-traffic-hooks.js +19 -0
  48. package/dist/src/responses.d.ts +5 -0
  49. package/dist/src/responses.js +25 -0
  50. package/dist/src/s3.d.ts +7 -0
  51. package/dist/src/s3.js +20 -0
  52. package/dist/src/scheduled-tasks.d.ts +6 -0
  53. package/dist/src/scheduled-tasks.js +20 -0
  54. package/dist/src/services.d.ts +22 -0
  55. package/dist/src/services.js +26 -0
  56. package/dist/src/sns-http-proxy.d.ts +28 -0
  57. package/dist/src/sns-http-proxy.js +66 -0
  58. package/dist/src/sns.d.ts +15 -0
  59. package/dist/src/sns.js +35 -0
  60. package/dist/src/sqs.d.ts +13 -0
  61. package/dist/src/sqs.js +33 -0
  62. package/dist/src/stoppable.d.ts +2 -0
  63. package/dist/src/stoppable.js +15 -0
  64. package/dist/src/tunnel.d.ts +10 -0
  65. package/dist/src/tunnel.js +52 -0
  66. package/package.json +1 -1
@@ -28,6 +28,7 @@ jobs:
28
28
  node-version: 20
29
29
  registry-url: https://registry.npmjs.org/
30
30
  - run: npm ci
31
+ - run: npm run compile
31
32
  - run: npm publish
32
33
  env:
33
34
  NODE_AUTH_TOKEN: ${{secrets.npm_token}}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const src_1 = require("../src");
5
+ const services_1 = require("../src/services");
6
+ const assert_1 = require("assert");
7
+ (0, globals_1.describe)("dev server", () => {
8
+ (0, globals_1.it)("bootstraps with defaults and an empty service", async () => {
9
+ const name = 'bootstrap';
10
+ const tested = await new src_1.DevServer(name, new services_1.Service({
11
+ name: 'service',
12
+ start: async () => Promise.resolve(),
13
+ stop: async () => Promise.resolve(),
14
+ })).start();
15
+ return (0, globals_1.expect)(tested.name).toEqual(name);
16
+ });
17
+ (0, globals_1.it)("calls service cleanup when init fails", async () => {
18
+ let called = false;
19
+ try {
20
+ await new src_1.DevServer('bootstrap', new services_1.Service({
21
+ name: 'failing-root',
22
+ start: async () => Promise.reject(new Error('fail!')),
23
+ stop: async () => Promise.resolve(called = true).then(),
24
+ })).start();
25
+ (0, assert_1.fail)('should have failed');
26
+ }
27
+ catch (e) {
28
+ return (0, globals_1.expect)(called)
29
+ .toStrictEqual(true);
30
+ }
31
+ });
32
+ (0, globals_1.it)("calls service cleanups on all components", async () => {
33
+ let calledOnParent = false;
34
+ let calledOnSibling = false;
35
+ let calledOnFailing = false;
36
+ try {
37
+ await new src_1.DevServer('bootstrap', (0, src_1.allOf)([
38
+ new services_1.Service({
39
+ name: 'parent',
40
+ start: async () => Promise.resolve(),
41
+ stop: async () => Promise.resolve(calledOnParent = true).then(),
42
+ }), (0, src_1.allOf)([
43
+ new services_1.Service({
44
+ name: 'failing-nested',
45
+ start: async () => Promise.reject(new Error('fail!')),
46
+ stop: async () => Promise.resolve(calledOnFailing = true).then(),
47
+ }),
48
+ new services_1.Service({
49
+ name: 'sibling',
50
+ start: async () => Promise.resolve(),
51
+ stop: async () => Promise.resolve(calledOnSibling = true).then(),
52
+ }),
53
+ ])
54
+ ])).start();
55
+ (0, assert_1.fail)('should have failed');
56
+ }
57
+ catch (e) {
58
+ return (0, globals_1.expect)([calledOnParent, calledOnFailing, calledOnSibling])
59
+ .toStrictEqual([true, true, true]);
60
+ }
61
+ });
62
+ (0, globals_1.it)("propagates the exception from the failing service", async () => {
63
+ const error = new Error('fail!');
64
+ try {
65
+ await new src_1.DevServer('bootstrap', (0, src_1.allOf)([
66
+ new services_1.Service({
67
+ name: 'parent',
68
+ start: async () => Promise.resolve(),
69
+ stop: async () => Promise.resolve(),
70
+ }), (0, src_1.allOf)([
71
+ new services_1.Service({
72
+ name: 'failing-nested',
73
+ start: async () => Promise.reject(error),
74
+ stop: async () => Promise.resolve(),
75
+ }),
76
+ new services_1.Service({
77
+ name: 'sibling',
78
+ start: async () => Promise.resolve(),
79
+ stop: async () => Promise.resolve(),
80
+ }),
81
+ ])
82
+ ])).start();
83
+ (0, assert_1.fail)('should have failed');
84
+ }
85
+ catch (e) {
86
+ return (0, globals_1.expect)(e)
87
+ .toStrictEqual(error);
88
+ }
89
+ });
90
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const src_1 = require("../src");
5
+ const testcontainers_1 = require("testcontainers");
6
+ const sqs_1 = require("../src/sqs");
7
+ const sns_1 = require("../src/sns");
8
+ const s3_1 = require("../src/s3");
9
+ const dynamodb_1 = require("../src/dynamodb");
10
+ const assert_1 = require("assert");
11
+ (0, globals_1.describe)("a promised service", () => {
12
+ (0, globals_1.test)("calls start with devserver config", () => {
13
+ const aws = {
14
+ region: 'region',
15
+ endpoint: 'http://localhost',
16
+ credentials: {
17
+ accessKeyId: 'aki',
18
+ secretAccessKey: 'sak',
19
+ },
20
+ };
21
+ const c = {
22
+ raw: aws,
23
+ network: new testcontainers_1.StartedNetwork(undefined, 'network', undefined),
24
+ sqs: new sqs_1.Sqs(aws),
25
+ sns: new sns_1.Sns(aws),
26
+ s3: new s3_1.S3(aws),
27
+ dynamo: new dynamodb_1.DynamoDb(aws),
28
+ eventsProxy: {
29
+ topic: {
30
+ arn: 'arn:...',
31
+ },
32
+ },
33
+ };
34
+ return (0, globals_1.expect)((0, src_1.promised)(async (cfg) => Promise.resolve(new src_1.Service({
35
+ name: 'resolved',
36
+ start: (config) => Promise.resolve(cfg === config),
37
+ stop: () => Promise.resolve(),
38
+ }))).start(c))
39
+ .resolves.toStrictEqual(true);
40
+ });
41
+ (0, globals_1.test)("calls the stop routine on failure starting promised service", async () => {
42
+ const error = new Error('failed starting service');
43
+ let stopped = false;
44
+ try {
45
+ await new src_1.DevServer('fail-on-start', (0, src_1.promised)(() => Promise.resolve(new src_1.Service({
46
+ name: 'fail-on-start',
47
+ start: async () => Promise.reject(error),
48
+ stop: async () => { stopped = true; },
49
+ })))).start();
50
+ (0, assert_1.fail)('should have thrown exception');
51
+ }
52
+ catch (e) {
53
+ return (0, globals_1.expect)(stopped)
54
+ .toStrictEqual(true);
55
+ }
56
+ });
57
+ (0, globals_1.test)("calls the cleanup routine on failure constructing promised service", async () => {
58
+ const error = {
59
+ cleanup: async () => stopped = true
60
+ };
61
+ let stopped = false;
62
+ try {
63
+ await new src_1.DevServer('fail-on-construct', (0, src_1.promised)(() => Promise.resolve(new src_1.Service({
64
+ name: 'fail-on-construct',
65
+ start: async () => Promise.resolve(),
66
+ stop: async () => Promise.resolve(),
67
+ }))
68
+ .then(() => Promise.reject(error)))).start();
69
+ (0, assert_1.fail)('should have thrown exception');
70
+ }
71
+ catch (e) {
72
+ return (0, globals_1.expect)(stopped)
73
+ .toStrictEqual(true);
74
+ }
75
+ });
76
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const src_1 = require("../src");
5
+ (0, globals_1.describe)("the event proxy", () => {
6
+ (0, globals_1.test)("handles an implementation error gracefully", async () => {
7
+ let sns;
8
+ let topic;
9
+ let invoked = false;
10
+ const handler = {
11
+ name: 'fail',
12
+ handler: async () => {
13
+ invoked = true;
14
+ return Promise.reject();
15
+ },
16
+ matcher: () => true,
17
+ };
18
+ const devServer = await new src_1.DevServer('events-proxy', (0, src_1.allOf)([
19
+ new src_1.Service({
20
+ name: 'config-capture',
21
+ start: (config) => {
22
+ sns = config.sns;
23
+ topic = config.eventsProxy.topic;
24
+ return Promise.resolve();
25
+ },
26
+ stop: () => Promise.resolve(),
27
+ }),
28
+ (0, src_1.eventsProxy)('test', {
29
+ handlers: [handler]
30
+ }),
31
+ ])).start();
32
+ return (0, globals_1.expect)(sns.publish(topic, '{}')
33
+ .then(() => new Promise(resolve => setTimeout(resolve, 200)))
34
+ .then(() => invoked)
35
+ .finally(() => devServer.stop())).resolves.toStrictEqual(true);
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -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;