@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.
Files changed (92) hide show
  1. package/.github/workflows/npm-publish.yml +33 -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/jest.config.js +7 -0
  7. package/package.json +2 -5
  8. package/src/assert.ts +4 -0
  9. package/src/cloudformation-dynamodb-table.ts +97 -0
  10. package/src/cloudformation-event-proxy.ts +61 -0
  11. package/src/cloudformation-lambda-http-proxy.ts +125 -0
  12. package/src/cloudformation.ts +95 -0
  13. package/src/config.ts +34 -0
  14. package/src/container.ts +82 -0
  15. package/src/deferred.ts +60 -0
  16. package/src/dev-server.ts +78 -0
  17. package/src/dynamodb.ts +62 -0
  18. package/src/event-proxy.ts +101 -0
  19. package/src/factories.ts +19 -0
  20. package/src/http-server.ts +59 -0
  21. package/src/index.ts +32 -0
  22. package/src/internal-queue.ts +89 -0
  23. package/src/lambda-http-proxy.ts +111 -0
  24. package/src/localstack.ts +74 -0
  25. package/src/mappers.ts +231 -0
  26. package/src/pre-traffic-hooks.ts +24 -0
  27. package/src/responses.ts +28 -0
  28. package/src/s3.ts +24 -0
  29. package/src/scheduled-tasks.ts +31 -0
  30. package/src/services.ts +46 -0
  31. package/src/sns-http-proxy.ts +109 -0
  32. package/src/sns.ts +49 -0
  33. package/src/sqs.ts +46 -0
  34. package/src/stoppable.ts +10 -0
  35. package/src/tunnel.ts +32 -0
  36. package/tsconfig.json +9 -0
  37. package/dist/src/assert.d.ts +0 -1
  38. package/dist/src/assert.js +0 -9
  39. package/dist/src/cloudformation-dynamodb-table.d.ts +0 -13
  40. package/dist/src/cloudformation-dynamodb-table.js +0 -45
  41. package/dist/src/cloudformation-event-proxy.d.ts +0 -13
  42. package/dist/src/cloudformation-event-proxy.js +0 -25
  43. package/dist/src/cloudformation-lambda-http-proxy.d.ts +0 -14
  44. package/dist/src/cloudformation-lambda-http-proxy.js +0 -62
  45. package/dist/src/cloudformation.d.ts +0 -28
  46. package/dist/src/cloudformation.js +0 -50
  47. package/dist/src/config.d.ts +0 -31
  48. package/dist/src/config.js +0 -2
  49. package/dist/src/container.d.ts +0 -18
  50. package/dist/src/container.js +0 -33
  51. package/dist/src/deferred.d.ts +0 -4
  52. package/dist/src/deferred.js +0 -45
  53. package/dist/src/dev-server.d.ts +0 -19
  54. package/dist/src/dev-server.js +0 -63
  55. package/dist/src/dynamodb.d.ts +0 -16
  56. package/dist/src/dynamodb.js +0 -48
  57. package/dist/src/event-proxy.d.ts +0 -13
  58. package/dist/src/event-proxy.js +0 -68
  59. package/dist/src/factories.d.ts +0 -3
  60. package/dist/src/factories.js +0 -16
  61. package/dist/src/http-server.d.ts +0 -25
  62. package/dist/src/http-server.js +0 -37
  63. package/dist/src/index.d.ts +0 -24
  64. package/dist/src/index.js +0 -46
  65. package/dist/src/internal-queue.d.ts +0 -11
  66. package/dist/src/internal-queue.js +0 -53
  67. package/dist/src/lambda-http-proxy.d.ts +0 -27
  68. package/dist/src/lambda-http-proxy.js +0 -49
  69. package/dist/src/localstack.d.ts +0 -11
  70. package/dist/src/localstack.js +0 -62
  71. package/dist/src/mappers.d.ts +0 -25
  72. package/dist/src/mappers.js +0 -158
  73. package/dist/src/pre-traffic-hooks.d.ts +0 -2
  74. package/dist/src/pre-traffic-hooks.js +0 -19
  75. package/dist/src/responses.d.ts +0 -5
  76. package/dist/src/responses.js +0 -22
  77. package/dist/src/s3.d.ts +0 -7
  78. package/dist/src/s3.js +0 -20
  79. package/dist/src/scheduled-tasks.d.ts +0 -6
  80. package/dist/src/scheduled-tasks.js +0 -20
  81. package/dist/src/services.d.ts +0 -22
  82. package/dist/src/services.js +0 -26
  83. package/dist/src/sns-http-proxy.d.ts +0 -28
  84. package/dist/src/sns-http-proxy.js +0 -66
  85. package/dist/src/sns.d.ts +0 -15
  86. package/dist/src/sns.js +0 -35
  87. package/dist/src/sqs.d.ts +0 -13
  88. package/dist/src/sqs.js +0 -33
  89. package/dist/src/stoppable.d.ts +0 -2
  90. package/dist/src/stoppable.js +0 -15
  91. package/dist/src/tunnel.d.ts +0 -10
  92. package/dist/src/tunnel.js +0 -52
@@ -0,0 +1,33 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-npm:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
29
+ registry-url: https://registry.npmjs.org/
30
+ - run: npm ci
31
+ - run: npm publish
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { DevServer, allOf } from "../src";
3
+ import { Service } from "../src/services";
4
+ import { fail } from "assert";
5
+
6
+ describe("dev server", () => {
7
+ it("bootstraps with defaults and an empty service", async () => {
8
+ const name = 'bootstrap';
9
+ const tested = await new DevServer(name, new Service({
10
+ name: 'service',
11
+ start: async () => Promise.resolve(),
12
+ stop: async () => Promise.resolve(),
13
+ })).start()
14
+ return expect(tested.name).toEqual(name)
15
+ })
16
+
17
+ it("calls service cleanup when init fails", async () => {
18
+ let called = false;
19
+ try {
20
+ await new DevServer('bootstrap', new Service({
21
+ name: 'failing-root',
22
+ start: async () => Promise.reject(new Error('fail!')),
23
+ stop: async () => Promise.resolve(called = true).then(),
24
+ })).start()
25
+ fail('should have failed')
26
+ } catch (e) {
27
+ return expect(called)
28
+ .toStrictEqual(true)
29
+ }
30
+ })
31
+
32
+ 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 DevServer('bootstrap', allOf([
38
+ new Service({
39
+ name: 'parent',
40
+ start: async () => Promise.resolve(),
41
+ stop: async () => Promise.resolve(calledOnParent = true).then(),
42
+ }), allOf([
43
+ new Service({
44
+ name: 'failing-nested',
45
+ start: async () => Promise.reject(new Error('fail!')),
46
+ stop: async () => Promise.resolve(calledOnFailing = true).then(),
47
+ }),
48
+ new Service({
49
+ name: 'sibling',
50
+ start: async () => Promise.resolve(),
51
+ stop: async () => Promise.resolve(calledOnSibling = true).then(),
52
+ }),
53
+ ])
54
+ ])).start()
55
+ fail('should have failed')
56
+ } catch (e) {
57
+ return expect([calledOnParent, calledOnFailing, calledOnSibling])
58
+ .toStrictEqual([true, true, true])
59
+ }
60
+ })
61
+
62
+ it("propagates the exception from the failing service", async () => {
63
+ const error = new Error('fail!')
64
+ try {
65
+ await new DevServer('bootstrap', allOf([
66
+ new Service({
67
+ name: 'parent',
68
+ start: async () => Promise.resolve(),
69
+ stop: async () => Promise.resolve(),
70
+ }), allOf([
71
+ new Service({
72
+ name: 'failing-nested',
73
+ start: async () => Promise.reject(error),
74
+ stop: async () => Promise.resolve(),
75
+ }),
76
+ new Service({
77
+ name: 'sibling',
78
+ start: async () => Promise.resolve(),
79
+ stop: async () => Promise.resolve(),
80
+ }),
81
+ ])
82
+ ])).start()
83
+ fail('should have failed')
84
+ } catch (e) {
85
+ return expect(e)
86
+ .toStrictEqual(error)
87
+ }
88
+ })
89
+ })
@@ -0,0 +1,86 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import { DevServer, DevServerConfig, Service, promised } from "../src";
3
+ import { StartedNetwork } from "testcontainers";
4
+ import { Sqs } from "../src/sqs";
5
+ import { Sns } from "../src/sns";
6
+ import { S3 } from "../src/s3";
7
+ import { DynamoDb } from "../src/dynamodb";
8
+ import { fail } from "assert";
9
+
10
+ describe("a promised service", () => {
11
+ test("calls start with devserver config", () => {
12
+ const aws = {
13
+ region: 'region',
14
+ endpoint: 'http://localhost',
15
+ credentials: {
16
+ accessKeyId: 'aki',
17
+ secretAccessKey: 'sak',
18
+ },
19
+ }
20
+ const c: DevServerConfig = {
21
+ raw: aws,
22
+ network: new StartedNetwork(undefined, 'network', undefined),
23
+ sqs: new Sqs(aws),
24
+ sns: new Sns(aws),
25
+ s3: new S3(aws),
26
+ dynamo: new DynamoDb(aws),
27
+ eventsProxy: {
28
+ topic: {
29
+ arn: 'arn:...',
30
+ },
31
+ },
32
+ }
33
+ return expect(
34
+ promised(async (cfg: DevServerConfig) => Promise.resolve(new Service(
35
+ {
36
+ name: 'resolved',
37
+ start: (config) => Promise.resolve(cfg === config),
38
+ stop: () => Promise.resolve(),
39
+ }
40
+ ))
41
+ ).start(c)
42
+ )
43
+ .resolves.toStrictEqual(true)
44
+ })
45
+
46
+ test("calls the stop routine on failure starting promised service", async () => {
47
+ const error = new Error('failed starting service')
48
+ let stopped = false;
49
+ try {
50
+ await new DevServer('fail-on-start', promised(
51
+ () => Promise.resolve(
52
+ new Service({
53
+ name: 'fail-on-start',
54
+ start: async () => Promise.reject(error),
55
+ stop: async () => { stopped = true },
56
+ }))
57
+ )).start()
58
+ fail('should have thrown exception')
59
+ } catch (e) {
60
+ return expect(stopped)
61
+ .toStrictEqual(true)
62
+ }
63
+ })
64
+
65
+ test("calls the cleanup routine on failure constructing promised service", async () => {
66
+ const error = {
67
+ cleanup: async () => stopped = true
68
+ }
69
+ let stopped = false;
70
+ try {
71
+ await new DevServer('fail-on-construct', promised(
72
+ () => Promise.resolve(
73
+ new Service({
74
+ name: 'fail-on-construct',
75
+ start: async () => Promise.resolve(),
76
+ stop: async () => Promise.resolve(),
77
+ }))
78
+ .then(() => Promise.reject(error)),
79
+ )).start()
80
+ fail('should have thrown exception')
81
+ } catch (e) {
82
+ return expect(stopped)
83
+ .toStrictEqual(true)
84
+ }
85
+ })
86
+ })
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import { allOf, DevServer, eventsProxy, Service } from "../src";
3
+ import { EventHandler } from "../src/event-proxy";
4
+ import { Sns } from "../src/sns";
5
+
6
+ describe("the event proxy", () => {
7
+ test("handles an implementation error gracefully", async () => {
8
+ let sns: Sns;
9
+ let topic: { arn: string; };
10
+ let invoked = false;
11
+ const handler: EventHandler = {
12
+ name: 'fail',
13
+ handler: async () => {
14
+ invoked = true;
15
+ return Promise.reject();
16
+ },
17
+ matcher: () => true,
18
+ }
19
+ const devServer = await new DevServer(
20
+ 'events-proxy',
21
+ allOf([
22
+ new Service({
23
+ name: 'config-capture',
24
+ start: (config) => {
25
+ sns = config.sns;
26
+ topic = config.eventsProxy.topic;
27
+ return Promise.resolve()
28
+ },
29
+ stop: () => Promise.resolve(),
30
+ }),
31
+ eventsProxy('test', {
32
+ handlers: [handler]
33
+ }),
34
+ ])
35
+ ).start();
36
+ return expect(sns.publish(topic, '{}')
37
+ .then(() => new Promise(resolve => setTimeout(resolve, 200)))
38
+ .then(() => invoked)
39
+ .finally(() => devServer.stop())
40
+ ).resolves.toStrictEqual(true)
41
+ })
42
+ })
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { DevServer } from "../src";
3
+ import { UNAUTHORIZED, newLambdaHttpProxy } from "../src/lambda-http-proxy";
4
+ import axios from 'axios';
5
+ import { v2 } from "../src/mappers";
6
+
7
+ describe("the lambda http proxy", () => {
8
+ const payloadVersionV2Mapper = v2().mapper;
9
+
10
+ it("invokes the handler with the heaviest route", async () => {
11
+ let endpoint: string
12
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
13
+ 'service',
14
+ {
15
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
16
+ routes: [
17
+ { method: /GET/, path: /\/a\/.*\/b\/.*\/c/, weight: 3, handler: () => Promise.resolve({ statusCode: 200, body: '' }) },
18
+ { method: /GET/, path: /\/a\/.*\/b\/.*/, weight: 2, handler: () => Promise.resolve({ statusCode: 400, body: '' }) },
19
+ { method: /GET/, path: /\/a\/.*/, weight: 2, handler: () => Promise.resolve({ statusCode: 400, body: '' }) },
20
+ ],
21
+ mapper: payloadVersionV2Mapper,
22
+ },
23
+ async (url) => { endpoint = url; }
24
+ )).start()
25
+ return expect(axios.get(`${endpoint}a/1/b/2/c`)
26
+ .then(r => r.status)
27
+ .finally(async () => tested.stop())
28
+ )
29
+ .resolves
30
+ .toEqual(200)
31
+ })
32
+
33
+ it("supports a single 'catch all' route", async () => {
34
+ let endpoint: string
35
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
36
+ 'service',
37
+ {
38
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
39
+ routes: [
40
+ { method: /.*/, path: /.*/, weight: 0, handler: () => Promise.resolve({ statusCode: 200, body: 'handled!' }) },
41
+ ],
42
+ mapper: payloadVersionV2Mapper,
43
+ },
44
+ async (url) => { endpoint = url; }
45
+ )).start()
46
+ return expect(axios.post(`${endpoint}find-me`)
47
+ .then(r => ({ status: r.status, body: r.data }))
48
+ .finally(async () => tested.stop())
49
+ )
50
+ .resolves
51
+ .toStrictEqual({ status: 200, body: 'handled!' })
52
+ })
53
+
54
+ it("returns unauthorized when the authorizer rejects", async () => {
55
+ let endpoint: string
56
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
57
+ 'service',
58
+ {
59
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
60
+ routes: [
61
+ { method: /.*/, path: /.*/, weight: 0, handler: () => Promise.resolve({ statusCode: 200, body: 'handled!' }), authorizer: () => Promise.reject(UNAUTHORIZED) },
62
+ ],
63
+ mapper: payloadVersionV2Mapper,
64
+ },
65
+ async (url) => { endpoint = url; }
66
+ )).start()
67
+ return expect(axios.get(endpoint, { validateStatus: () => true })
68
+ .then(r => r.status)
69
+ .finally(async () => tested.stop())
70
+ )
71
+ .resolves
72
+ .toEqual(401)
73
+ })
74
+
75
+ it("triggers the fallback handler when no route matches a request", async () => {
76
+ let endpoint: string
77
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
78
+ 'service',
79
+ {
80
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
81
+ routes: [
82
+ { method: /.*/, path: /\a/, weight: 0, handler: () => Promise.resolve({ statusCode: 200, body: 'handled!' }) },
83
+ ],
84
+ mapper: payloadVersionV2Mapper,
85
+ },
86
+ async (url) => { endpoint = url; }
87
+ )).start()
88
+ return expect(axios.get(`${endpoint}b`, { validateStatus: () => true })
89
+ .then(r => ({ status: r.status, body: r.data }))
90
+ .finally(async () => tested.stop())
91
+ )
92
+ .resolves
93
+ .toStrictEqual({ status: 404, body: 'no route found to handle GET to /b' })
94
+ })
95
+
96
+ it("handler has access to the context", async () => {
97
+ const expectedContext = 'hello world!'
98
+ let endpoint: string
99
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
100
+ 'service',
101
+ {
102
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
103
+ routes: [
104
+ {
105
+ method: /.*/,
106
+ path: /\a/,
107
+ weight: 0,
108
+ handler: (_: unknown, context: string) => Promise.resolve({ statusCode: 200, body: context })
109
+ },
110
+ ],
111
+ context: () => expectedContext,
112
+ mapper: payloadVersionV2Mapper,
113
+ },
114
+ async (url) => { endpoint = url; }
115
+ )).start()
116
+ return expect(axios.get(`${endpoint}a`)
117
+ .then(r => r.data)
118
+ .finally(async () => tested.stop())
119
+ )
120
+ .resolves
121
+ .toStrictEqual(expectedContext)
122
+ })
123
+
124
+ it("can set cookies", async () => {
125
+ let endpoint: string
126
+
127
+ const handler = async (event: object, context: unknown) => {
128
+ return {
129
+ statusCode: 200,
130
+ cookies: ['galletita=criollita'],
131
+ }
132
+ };
133
+
134
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
135
+ 'service',
136
+ {
137
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
138
+ routes: [
139
+ { method: /GET/, path: /\/a/, weight: 0, handler: handler },
140
+ ],
141
+ mapper: payloadVersionV2Mapper,
142
+ },
143
+ async (url) => { endpoint = url; }
144
+ )).start();
145
+
146
+ return expect(axios.get(`${endpoint}a`)
147
+ .then(r => r.headers["set-cookie"])
148
+ .finally(async () => tested.stop())
149
+ ).resolves.toContain('galletita=criollita')
150
+ })
151
+
152
+ it("can read cookies", async () => {
153
+ let endpoint: string
154
+
155
+ const handler = async (event: object, context: unknown) => {
156
+ return {
157
+ statusCode: 200,
158
+ body: event["headers"].cookie,
159
+ }
160
+ };
161
+
162
+ const tested = await new DevServer('lambda-http-proxy', newLambdaHttpProxy(
163
+ 'service',
164
+ {
165
+ settings: { protocol: 'http:', host: '127.0.0.1', port: undefined },
166
+ routes: [
167
+ { method: /GET/, path: /\/a/, weight: 0, handler: handler },
168
+ ],
169
+ mapper: payloadVersionV2Mapper,
170
+ },
171
+ async (url) => { endpoint = url; }
172
+ )).start();
173
+
174
+ return expect(axios.get(`${endpoint}a`, { headers: { cookie: ['galletita=criollita'] } })
175
+ .then(r => r.data)
176
+ .finally(async () => tested.stop())
177
+ ).resolves.toContain('galletita=criollita')
178
+ })
179
+ })
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: "ts-jest",
4
+ testPathIgnorePatterns: ["/dist/"],
5
+ testRegex: "/__tests__/.*$",
6
+ testTimeout: 60000,
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yopdev/dev-server",
3
- "version": "3.0.1",
3
+ "version": "3.0.2-RC1",
4
4
  "scripts": {
5
5
  "compile": "tsc",
6
6
  "pretest": "npm run compile",
@@ -35,8 +35,5 @@
35
35
  },
36
36
  "main": "dist/src/index.js",
37
37
  "exports": "./dist/src/index.js",
38
- "types": "dist/src/index.d.ts",
39
- "files": [
40
- "dist/src"
41
- ]
38
+ "types": "dist/src/index.d.ts"
42
39
  }
package/src/assert.ts ADDED
@@ -0,0 +1,4 @@
1
+ export function assertNotUndefined<T>(value: T | undefined, message = 'value is undefined'): T {
2
+ if (value === undefined) throw new Error(message);
3
+ return value;
4
+ }
@@ -0,0 +1,97 @@
1
+ import { Callback, Lifecycle, Service } from './services';
2
+ import { AttributeDefinition, CreateTableCommand, GlobalSecondaryIndex, KeySchemaElement, UpdateTimeToLiveCommand } from '@aws-sdk/client-dynamodb';
3
+ import { CLOUDFORMATION_SCHEMA } from 'js-yaml-cloudformation-schema';
4
+ import { load } from 'js-yaml';
5
+ import { readFileSync } from 'fs';
6
+ import { DevServerConfig } from './config';
7
+ import { Logger, LoggerFactory } from '@yopdev/logging';
8
+ import { newDynamoDbTable } from './dynamodb';
9
+
10
+ export const newDynamoDbTableFromCloudFormationTemplate = (
11
+ name: string,
12
+ config: DynamoDbTableCloudFormationConfig,
13
+ callback?: Callback<string>,
14
+ ): Service<string> => new Service(new DynamoDbTableCloudFormation(
15
+ name,
16
+ config.template,
17
+ config.resource,
18
+ config.tableName,
19
+ config.throughput
20
+ ), callback)
21
+
22
+ class DynamoDbTableCloudFormation implements Lifecycle<string> {
23
+ private readonly LOGGER: Logger
24
+ private readonly tableName: (name: string) => string;
25
+
26
+ constructor(
27
+ readonly name: string,
28
+ private readonly template: (name: string) => string,
29
+ private readonly resource: string,
30
+ tableName: (name: string) => string,
31
+ private readonly throughput: Throughput,
32
+ ) {
33
+ this.LOGGER = LoggerFactory.create(`CFN:DYNAMO[${name}]`)
34
+ this.tableName = tableName ? tableName : (name: string) => { throw new Error(`table name not specified on ${name}`) }
35
+ }
36
+
37
+ start = (config: DevServerConfig) => Promise.resolve(
38
+ this.parseTableDefinition(this.template(this.name))
39
+ .then((definition) => definition[this.resource].Properties)
40
+ .then((resource) => this.createTableCommand(resource, this.throughput)
41
+ .then((command) => newDynamoDbTable(this.name, {
42
+ command: command,
43
+ ttlAttribute: resource.TimeToLiveSpecification?.AttributeName,
44
+ }))))
45
+ .tap(() => this.LOGGER.info('configured'))
46
+ .then((service) => service.start(config))
47
+
48
+ stop = () => Promise.resolve()
49
+
50
+ private createTableCommand = async (definition: DynamoDbTableCloudformationDefinitionProperties, throughput: Throughput) =>
51
+ new CreateTableCommand({
52
+ TableName: definition.TableName ?? this.tableName(this.name),
53
+ KeySchema: definition.KeySchema,
54
+ AttributeDefinitions: definition.AttributeDefinitions,
55
+ GlobalSecondaryIndexes: definition.GlobalSecondaryIndexes?.map((gsi: GlobalSecondaryIndex) => ({
56
+ ...gsi,
57
+ ProvisionedThroughput: throughput,
58
+ })),
59
+ ProvisionedThroughput: throughput,
60
+ });
61
+
62
+ private parseTableDefinition = async (path: string) => (
63
+ load(readFileSync(path).toString(), {
64
+ schema: CLOUDFORMATION_SCHEMA,
65
+ }) as ResourcesCloudformationDefinition
66
+ ).Resources;
67
+ }
68
+
69
+ type DynamoDbTableCloudFormationConfig = {
70
+ template: (name: string) => string;
71
+ resource: string,
72
+ tableName: (name: string) => string;
73
+ throughput: Throughput;
74
+ }
75
+
76
+ type ResourcesCloudformationDefinition = {
77
+ Resources: {
78
+ [name: string]: {
79
+ Properties: DynamoDbTableCloudformationDefinitionProperties
80
+ }
81
+ };
82
+ }
83
+
84
+ type DynamoDbTableCloudformationDefinitionProperties = {
85
+ TableName?: string;
86
+ KeySchema: KeySchemaElement[];
87
+ AttributeDefinitions: AttributeDefinition[];
88
+ GlobalSecondaryIndexes?: GlobalSecondaryIndex[];
89
+ TimeToLiveSpecification?: TimeToLiveSpecification;
90
+ }
91
+
92
+ type TimeToLiveSpecification = {
93
+ AttributeName: string;
94
+ Enabled: boolean;
95
+ };
96
+
97
+ type Throughput = { ReadCapacityUnits: number; WriteCapacityUnits: number }
@@ -0,0 +1,61 @@
1
+ import { SQSEvent, SQSRecord } from 'aws-lambda';
2
+ import { CloudFormationSetup, 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
+
8
+ export const snsEventsFromCloudFormationTemplate = (
9
+ name: string,
10
+ config: CloudFormationEventsProxyConfig,
11
+ ): Startable<EventHandler[]> =>
12
+ new CloudFormationEventsProxy(name, config);
13
+ class CloudFormationEventsProxy extends CloudFormationSetup<
14
+ {
15
+ SqsSubscription: {
16
+ BatchSize: string;
17
+ QueueArn: string;
18
+ QueueUrl: string;
19
+ };
20
+ Topic: string;
21
+ FilterPolicy: {
22
+ [name: string]: string[];
23
+ };
24
+ },
25
+ (event: SQSEvent) => Promise<unknown>,
26
+ EventHandler,
27
+ EventHandler[]
28
+ > {
29
+ constructor(name: string, config: CloudFormationEventsProxyConfig) {
30
+ super(
31
+ name,
32
+ (event) => event.Type === 'SNS',
33
+ async (event, handler) => ({
34
+ name: handler.name,
35
+ handler: handler,
36
+ matcher: (_, attributes) => {
37
+ const required = Object.entries(event.Properties.FilterPolicy ?? []).flatMap((entry) =>
38
+ entry[1].map((value) => value + entry[0]),
39
+ );
40
+ if (required.length == 0) return true;
41
+ const actual = Object.entries(attributes).flatMap((entry) => entry[1].Value + entry[0]);
42
+ return required.filter((e) => actual.includes(e)).length > 0;
43
+ },
44
+ }),
45
+ config.prepare,
46
+ config
47
+ );
48
+ }
49
+
50
+ protected afterStart = (_name: string, _config: DevServerConfig, routes: EventHandler[]) => routes;
51
+
52
+ logHandler(handler: EventHandler): string {
53
+ return `detected sns handler ${handler.name}`;
54
+ }
55
+ }
56
+
57
+ type CloudFormationEventsProxyConfig = {
58
+ topic?: string,
59
+ mapper?: (message: Message) => SQSRecord,
60
+ prepare?: (config: DevServerConfig) => Promise<void>,
61
+ } & CloudFormationSetupConfig;