@yopdev/dev-server 3.0.1 → 3.0.2-RC1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +33 -0
- package/__tests__/bootstrap.test.ts +89 -0
- package/__tests__/deferred.test.ts +86 -0
- package/__tests__/event-proxy.test.ts +42 -0
- package/__tests__/lambda-http-proxy.test.ts +179 -0
- package/jest.config.js +7 -0
- package/package.json +2 -5
- package/src/assert.ts +4 -0
- package/src/cloudformation-dynamodb-table.ts +97 -0
- package/src/cloudformation-event-proxy.ts +61 -0
- package/src/cloudformation-lambda-http-proxy.ts +125 -0
- package/src/cloudformation.ts +95 -0
- package/src/config.ts +34 -0
- package/src/container.ts +82 -0
- package/src/deferred.ts +60 -0
- package/src/dev-server.ts +78 -0
- package/src/dynamodb.ts +62 -0
- package/src/event-proxy.ts +101 -0
- package/src/factories.ts +19 -0
- package/src/http-server.ts +59 -0
- package/src/index.ts +32 -0
- package/src/internal-queue.ts +89 -0
- package/src/lambda-http-proxy.ts +111 -0
- package/src/localstack.ts +74 -0
- package/src/mappers.ts +231 -0
- package/src/pre-traffic-hooks.ts +24 -0
- package/src/responses.ts +28 -0
- package/src/s3.ts +24 -0
- package/src/scheduled-tasks.ts +31 -0
- package/src/services.ts +46 -0
- package/src/sns-http-proxy.ts +109 -0
- package/src/sns.ts +49 -0
- package/src/sqs.ts +46 -0
- package/src/stoppable.ts +10 -0
- package/src/tunnel.ts +32 -0
- package/tsconfig.json +9 -0
- package/dist/src/assert.d.ts +0 -1
- package/dist/src/assert.js +0 -9
- package/dist/src/cloudformation-dynamodb-table.d.ts +0 -13
- package/dist/src/cloudformation-dynamodb-table.js +0 -45
- package/dist/src/cloudformation-event-proxy.d.ts +0 -13
- package/dist/src/cloudformation-event-proxy.js +0 -25
- package/dist/src/cloudformation-lambda-http-proxy.d.ts +0 -14
- package/dist/src/cloudformation-lambda-http-proxy.js +0 -62
- package/dist/src/cloudformation.d.ts +0 -28
- package/dist/src/cloudformation.js +0 -50
- package/dist/src/config.d.ts +0 -31
- package/dist/src/config.js +0 -2
- package/dist/src/container.d.ts +0 -18
- package/dist/src/container.js +0 -33
- package/dist/src/deferred.d.ts +0 -4
- package/dist/src/deferred.js +0 -45
- package/dist/src/dev-server.d.ts +0 -19
- package/dist/src/dev-server.js +0 -63
- package/dist/src/dynamodb.d.ts +0 -16
- package/dist/src/dynamodb.js +0 -48
- package/dist/src/event-proxy.d.ts +0 -13
- package/dist/src/event-proxy.js +0 -68
- package/dist/src/factories.d.ts +0 -3
- package/dist/src/factories.js +0 -16
- package/dist/src/http-server.d.ts +0 -25
- package/dist/src/http-server.js +0 -37
- package/dist/src/index.d.ts +0 -24
- package/dist/src/index.js +0 -46
- package/dist/src/internal-queue.d.ts +0 -11
- package/dist/src/internal-queue.js +0 -53
- package/dist/src/lambda-http-proxy.d.ts +0 -27
- package/dist/src/lambda-http-proxy.js +0 -49
- package/dist/src/localstack.d.ts +0 -11
- package/dist/src/localstack.js +0 -62
- package/dist/src/mappers.d.ts +0 -25
- package/dist/src/mappers.js +0 -158
- package/dist/src/pre-traffic-hooks.d.ts +0 -2
- package/dist/src/pre-traffic-hooks.js +0 -19
- package/dist/src/responses.d.ts +0 -5
- package/dist/src/responses.js +0 -22
- package/dist/src/s3.d.ts +0 -7
- package/dist/src/s3.js +0 -20
- package/dist/src/scheduled-tasks.d.ts +0 -6
- package/dist/src/scheduled-tasks.js +0 -20
- package/dist/src/services.d.ts +0 -22
- package/dist/src/services.js +0 -26
- package/dist/src/sns-http-proxy.d.ts +0 -28
- package/dist/src/sns-http-proxy.js +0 -66
- package/dist/src/sns.d.ts +0 -15
- package/dist/src/sns.js +0 -35
- package/dist/src/sqs.d.ts +0 -13
- package/dist/src/sqs.js +0 -33
- package/dist/src/stoppable.d.ts +0 -2
- package/dist/src/stoppable.js +0 -15
- package/dist/src/tunnel.d.ts +0 -10
- package/dist/src/tunnel.js +0 -52
|
@@ -0,0 +1,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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yopdev/dev-server",
|
|
3
|
-
"version": "3.0.
|
|
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,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;
|