@toa.io/extensions.origins 0.20.0-dev.9 → 0.20.0

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 (43) hide show
  1. package/package.json +17 -11
  2. package/readme.md +70 -67
  3. package/schemas/annotation.cos.yaml +3 -0
  4. package/schemas/manifest.cos.yaml +4 -0
  5. package/source/Factory.ts +88 -0
  6. package/source/annotation.test.ts +150 -0
  7. package/source/annotation.ts +83 -0
  8. package/source/extension.test.ts +161 -0
  9. package/source/extension.ts +60 -0
  10. package/source/index.ts +2 -0
  11. package/source/manifest.test.ts +30 -0
  12. package/source/manifest.ts +11 -0
  13. package/source/protocols/amqp/.test/aspect.fixtures.js +1 -1
  14. package/source/protocols/amqp/.test/mock.comq.js +2 -2
  15. package/source/protocols/amqp/aspect.js +17 -24
  16. package/source/protocols/amqp/deployment.js +8 -2
  17. package/source/protocols/http/.aspect/permissions.js +13 -10
  18. package/source/protocols/http/aspect.js +16 -37
  19. package/source/protocols/index.ts +16 -0
  20. package/tsconfig.json +12 -0
  21. package/source/.credentials.js +0 -14
  22. package/source/.deployment/index.js +0 -5
  23. package/source/.deployment/uris.js +0 -37
  24. package/source/.test/constants.js +0 -3
  25. package/source/.test/deployment.fixtures.js +0 -20
  26. package/source/.test/factory.fixtures.js +0 -13
  27. package/source/deployment.js +0 -41
  28. package/source/deployment.test.js +0 -185
  29. package/source/factory.js +0 -44
  30. package/source/factory.test.js +0 -140
  31. package/source/index.js +0 -9
  32. package/source/manifest.js +0 -41
  33. package/source/manifest.test.js +0 -82
  34. package/source/protocols/amqp/aspect.test.js +0 -119
  35. package/source/protocols/http/.aspect/permissions.test.js +0 -23
  36. package/source/protocols/http/aspect.test.js +0 -220
  37. package/source/protocols/index.js +0 -6
  38. package/source/schemas/annotations.cos.yaml +0 -1
  39. package/source/schemas/index.js +0 -8
  40. package/source/schemas/manifest.cos.yaml +0 -2
  41. package/types/amqp.d.ts +0 -9
  42. package/types/deployment.d.ts +0 -7
  43. package/types/http.d.ts +0 -28
@@ -1,41 +0,0 @@
1
- 'use strict'
2
-
3
- const { merge } = require('@toa.io/generic')
4
- const schemas = require('./schemas')
5
- const protocols = require('./protocols')
6
- const create = require('./.deployment')
7
- const credentials = require('./.credentials')
8
-
9
- /**
10
- * @param {toa.norm.context.dependencies.Instance[]} instances
11
- * @param {toa.origins.Annotations} annotations
12
- * @returns {toa.deployment.dependency.Declaration}
13
- */
14
- function deployment (instances, annotations = {}) {
15
- validate(annotations)
16
-
17
- const uris = create.uris(instances, annotations)
18
- const variables = { ...uris }
19
-
20
- protocols.reduce((variables, provider) => {
21
- const specifics = provider.deployment?.(instances)
22
-
23
- return merge(variables, specifics)
24
- }, variables)
25
-
26
- return { variables }
27
- }
28
-
29
- /**
30
- * @param {toa.origins.Annotations} annotations
31
- * @return {void}
32
- */
33
- function validate (annotations) {
34
- schemas.annotations.validate(annotations)
35
-
36
- for (const component of Object.values(annotations)) {
37
- Object.values(component).forEach(credentials.check)
38
- }
39
- }
40
-
41
- exports.deployment = deployment
@@ -1,185 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { sample, letters: { up } } = require('@toa.io/generic')
5
-
6
- const fixtures = require('./.test/deployment.fixtures')
7
- const { deployment } = require('../')
8
-
9
- it('should be', async () => {
10
- expect(deployment).toBeInstanceOf(Function)
11
- })
12
-
13
- /** @type {toa.norm.context.dependencies.Instance[]} */
14
- let components
15
-
16
- /** @type {string} */
17
- let origin
18
-
19
- /** @type {toa.norm.context.dependencies.Instance} */
20
- let component
21
-
22
- beforeEach(() => {
23
- components = fixtures.components()
24
-
25
- component = sample(components)
26
-
27
- const origins = Object.keys(component.manifest)
28
-
29
- origin = sample(origins)
30
- })
31
-
32
- describe('validation', () => {
33
- it('should throw on annotation component mismatch', async () => {
34
- const id = generate()
35
-
36
- const annotations = {
37
- [id]: {
38
- [origin]: 'dev://' + generate()
39
- }
40
- }
41
-
42
- expect(() => deployment(components, annotations))
43
- .toThrow(`Origins annotations error: component '${id}' is not found`)
44
- })
45
-
46
- it('should throw on annotation origin mismatch', async () => {
47
- const id = component.locator.id
48
- const origin = generate()
49
-
50
- const annotations = {
51
- [id]: {
52
- [origin]: 'dev://' + generate()
53
- }
54
- }
55
-
56
- expect(() => deployment(components, annotations))
57
- .toThrow(`Origins annotations error: component '${id}' doesn't have '${origin}' origin`)
58
- })
59
-
60
- it('should throw if annotation is not valid', async () => {
61
- const annotations = /** @type {toa.origins.Annotations} */ { [component.locator.id]: generate() }
62
-
63
- expect(() => deployment(components, annotations)).toThrow('must be object')
64
- })
65
-
66
- it('should throw if annotation is URI is not valid', async () => {
67
- const annotations = {
68
- [component.locator.id]: {
69
- [origin]: 'hello!'
70
- }
71
- }
72
-
73
- expect(() => deployment(components, annotations)).toThrow('must match format')
74
- })
75
- })
76
-
77
- it('should create variables', () => {
78
- const value = 'dev://' + generate()
79
-
80
- /** @type {toa.origins.Annotations} */
81
- const annotations = {
82
- [component.locator.id]: {
83
- [origin]: value
84
- }
85
- }
86
-
87
- const output = deployment(components, annotations)
88
-
89
- expect(output.variables).not.toBeUndefined()
90
-
91
- const variables = output.variables[component.locator.label]
92
- const varName = 'TOA_ORIGINS_' + component.locator.uppercase
93
- const variable = findVariable(variables, varName)
94
-
95
- expect(variable).toBeDefined()
96
-
97
- const json = JSON.stringify(annotations[component.locator.id])
98
- const base64 = btoa(json)
99
-
100
- expect(variable.value).toStrictEqual(base64)
101
- })
102
-
103
- it.each(['http', 'amqp'])('should throw if %s annotation contains credentials',
104
- async (protocol) => {
105
- /** @type {toa.origins.Annotations} */
106
- const annotations = {
107
- [component.locator.id]: {
108
- [origin]: protocol + '://dev:sec@host-' + generate()
109
- }
110
- }
111
-
112
- expect(() => deployment(components, annotations)).toThrow('Origins must not contain credentials')
113
- })
114
-
115
- describe('amqp', () => {
116
- beforeEach(() => {
117
- const amqpComponents = components.filter(
118
- (component) => {
119
- const origin = Object.keys(component.manifest)
120
- const url = new URL(component.manifest[origin])
121
-
122
- return url.protocol === 'amqp:' || url.protocol === 'amqps:'
123
- }
124
- )
125
-
126
- component = sample(amqpComponents)
127
-
128
- const origins = Object.keys(component.manifest)
129
-
130
- origin = sample(origins)
131
- })
132
-
133
- it('should create credential secrets', () => {
134
- /** @type {toa.origins.Annotations} */
135
- const annotations = {
136
- [component.locator.id]: {
137
- [origin]: 'amqps://whatever'
138
- }
139
- }
140
-
141
- const output = deployment(components, annotations)
142
- const variables = output.variables[component.locator.label]
143
-
144
- expect(variables).toBeDefined()
145
-
146
- const envPrefix = `TOA_ORIGINS_${component.locator.uppercase}_${up(origin)}_`
147
- const secretName = `toa-origins-${component.locator.label}-${origin}`
148
-
149
- for (const property of ['username', 'password']) {
150
- const variableName = envPrefix + up(property)
151
- const variable = findVariable(variables, variableName)
152
-
153
- expect(variable).toBeDefined()
154
-
155
- expect(variable.secret).toStrictEqual({
156
- name: secretName,
157
- key: property
158
- })
159
- }
160
- })
161
- })
162
-
163
- describe('http', () => {
164
- it('should not throw on properties', async () => {
165
- const annotations = {
166
- [component.locator.id]: {
167
- '.http': {
168
- null: true
169
- },
170
- [origin]: 'amqps://whatever'
171
- }
172
- }
173
-
174
- expect(() => deployment(components, annotations)).not.toThrow()
175
- })
176
- })
177
-
178
- /**
179
- * @param {toa.deployment.dependency.Variable[]} variables
180
- * @param {string} name
181
- * @returns {toa.deployment.dependency.Variable}
182
- */
183
- function findVariable (variables, name) {
184
- return variables.find((variable) => variable.name === name)
185
- }
package/source/factory.js DELETED
@@ -1,44 +0,0 @@
1
- 'use strict'
2
-
3
- const protocols = require('./protocols')
4
- const env = require('./env')
5
-
6
- class Factory {
7
- /**
8
- * @param {toa.core.Locator} locator
9
- * @param {toa.origins.Manifest} manifest
10
- * @return {toa.core.extensions.Aspect[]}
11
- */
12
- aspect (locator, manifest) {
13
- env.apply(locator, manifest)
14
-
15
- return protocols.map((protocol) => this.#createAspect(protocol, manifest))
16
- }
17
-
18
- /**
19
- * @param {object} protocol
20
- * @param {toa.origins.Manifest} manifest
21
- * @return {toa.core.extensions.Aspect}
22
- */
23
- #createAspect (protocol, manifest) {
24
- const protocolManifest = {}
25
-
26
- let properties
27
-
28
- for (const [origin, reference] of Object.entries(manifest)) {
29
- if (origin[0] === '.') {
30
- if (origin.substring(1) === protocol.id) properties = reference
31
-
32
- continue
33
- }
34
-
35
- const url = new URL(reference)
36
-
37
- if (protocol.protocols.includes(url.protocol)) protocolManifest[origin] = reference
38
- }
39
-
40
- return protocol.create(protocolManifest, properties)
41
- }
42
- }
43
-
44
- exports.Factory = Factory
@@ -1,140 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { Locator } = require('@toa.io/core')
5
- const { sample, overwrite, letters: { up } } = require('@toa.io/generic')
6
-
7
- jest.mock('./protocols/http/aspect')
8
- jest.mock('./protocols/amqp/aspect')
9
-
10
- const http = require('./protocols/http/aspect')
11
- const amqp = require('./protocols/amqp/aspect')
12
-
13
- const fixtures = require('./.test/factory.fixtures')
14
- const { Factory } = require('../')
15
-
16
- let factory
17
-
18
- beforeEach(() => {
19
- jest.clearAllMocks()
20
-
21
- factory = new Factory()
22
- })
23
-
24
- it('should create aspects', () => {
25
- factory.aspect(new Locator(generate(), generate()), fixtures.manifest)
26
-
27
- const httpManifest = filterManifest(fixtures.manifest, 'http')
28
- const amqpManifest = filterManifest(fixtures.manifest, 'amqp')
29
-
30
- expect(http.create).toHaveBeenCalledWith(httpManifest, undefined)
31
- expect(amqp.create).toHaveBeenCalledWith(amqpManifest, undefined)
32
- })
33
-
34
- describe('env', () => {
35
- it('should overwrites URLs from environment', async () => {
36
- const httpManifest = filterManifest(fixtures.manifest, 'http')
37
- const key = sample(Object.keys(httpManifest))
38
- const override = { [key]: 'http://' + generate() }
39
- const json = JSON.stringify(override)
40
- const base64 = btoa(json)
41
- const locator = new Locator(generate(), generate())
42
-
43
- process.env['TOA_ORIGINS_' + locator.uppercase] = base64
44
-
45
- factory.aspect(locator, fixtures.manifest)
46
-
47
- const expected = overwrite(httpManifest, override)
48
-
49
- expect(http.create.mock.calls[0][0]).toStrictEqual(expected)
50
- })
51
-
52
- describe('amqp', () => {
53
- /** @type {toa.origins.Manifest} */
54
- let amqpManifest
55
-
56
- beforeEach(() => {
57
- amqpManifest = filterManifest(fixtures.manifest, 'amqp')
58
- })
59
-
60
- it('should add credentials from environment', async () => {
61
- const key = sample(Object.keys(amqpManifest))
62
- const locator = new Locator(generate(), generate())
63
- const envPrefix = 'TOA_ORIGINS_' + locator.uppercase + '_' + up(key) + '_'
64
- const username = generate()
65
- const password = generate()
66
-
67
- process.env[envPrefix + 'USERNAME'] = username
68
- process.env[envPrefix + 'PASSWORD'] = password
69
-
70
- factory.aspect(locator, amqpManifest)
71
-
72
- const manifest = amqp.create.mock.calls[0][0]
73
- const origin = manifest[key]
74
- const url = new URL(origin)
75
-
76
- expect(url.username).toStrictEqual(username)
77
- expect(url.password).toStrictEqual(password)
78
- })
79
-
80
- it('should add credentials to URLs from environment', async () => {
81
- const key = sample(Object.keys(amqpManifest))
82
- const hostname = generate().toLowerCase()
83
- const override = { [key]: 'amqp://' + hostname }
84
- const json = JSON.stringify(override)
85
- const base64 = btoa(json)
86
- const locator = new Locator(generate(), generate())
87
- const envPrefix = 'TOA_ORIGINS_' + locator.uppercase + '_' + up(key) + '_'
88
- const username = generate()
89
- const password = generate()
90
-
91
- process.env['TOA_ORIGINS_' + locator.uppercase] = base64
92
- process.env[envPrefix + 'USERNAME'] = username
93
- process.env[envPrefix + 'PASSWORD'] = password
94
-
95
- factory.aspect(locator, fixtures.manifest)
96
-
97
- const manifest = amqp.create.mock.calls[0][0]
98
- const origin = manifest[key]
99
- const url = new URL(origin)
100
-
101
- expect(url.hostname).toStrictEqual(hostname)
102
- expect(url.username).toStrictEqual(username)
103
- expect(url.password).toStrictEqual(password)
104
- })
105
- })
106
-
107
- describe('http', () => {
108
- it('should read properties', async () => {
109
- const properties = {
110
- '.http': {
111
- [generate()]: generate()
112
- }
113
- }
114
-
115
- const locator = new Locator(generate(), generate())
116
- const json = JSON.stringify(properties)
117
-
118
- process.env['TOA_ORIGINS_' + locator.uppercase] = btoa(json)
119
-
120
- factory.aspect(locator, fixtures.manifest)
121
-
122
- expect(http.create).toHaveBeenCalledWith(expect.anything(), properties['.http'])
123
- })
124
- })
125
- })
126
-
127
- /**
128
- * @param {toa.origins.Manifest} manifest
129
- * @param {string} protocol
130
- * @return {toa.origins.Manifest}
131
- */
132
- function filterManifest (manifest, protocol) {
133
- const result = {}
134
-
135
- for (const [origin, reference] of Object.entries(manifest)) {
136
- if (reference.slice(0, protocol.length) === protocol) result[origin] = reference
137
- }
138
-
139
- return result
140
- }
package/source/index.js DELETED
@@ -1,9 +0,0 @@
1
- 'use strict'
2
-
3
- const { manifest } = require('./manifest')
4
- const { deployment } = require('./deployment')
5
- const { Factory } = require('./factory')
6
-
7
- exports.manifest = manifest
8
- exports.deployment = deployment
9
- exports.Factory = Factory
@@ -1,41 +0,0 @@
1
- 'use strict'
2
-
3
- const { remap, echo, shards } = require('@toa.io/generic')
4
- const schemas = require('./schemas')
5
- const protocols = require('./protocols')
6
- const credentials = require('./.credentials')
7
-
8
- /**
9
- * @param {toa.origins.Manifest} manifest
10
- * @returns {toa.origins.Manifest}
11
- */
12
- function manifest (manifest) {
13
- if (manifest === null) return {}
14
-
15
- manifest = remap(manifest, (origin) => echo(origin))
16
- validate(manifest)
17
-
18
- for (const url of Object.values(manifest)) {
19
- const supported = protocols.find((provider) => supports(provider, url))
20
-
21
- if (supported === undefined) throw new Error(`'${url}' protocol is not supported`)
22
- }
23
-
24
- return manifest
25
- }
26
-
27
- /**
28
- * @param {toa.origins.Manifest} manifest
29
- */
30
- function validate (manifest) {
31
- manifest = remap(manifest, (value) => shards(value)[0])
32
- schemas.manifest.validate(manifest)
33
-
34
- Object.values(manifest).forEach(credentials.check)
35
- }
36
-
37
- function supports (provider, url) {
38
- return provider.protocols.findIndex((protocol) => url.substring(0, protocol.length) === protocol) !== -1
39
- }
40
-
41
- exports.manifest = manifest
@@ -1,82 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { PROTOCOLS } = require('./.test/constants')
5
-
6
- const { manifest } = require('../')
7
-
8
- it('should be', async () => {
9
- expect(manifest).toBeInstanceOf(Function)
10
- })
11
-
12
- it('should return manifest', async () => {
13
- const input = { [generate()]: 'http://' + generate() }
14
- const output = manifest(input)
15
-
16
- expect(output).toStrictEqual(input)
17
- })
18
-
19
- it('should fail if not Record<string, string>', async () => {
20
- const input = /** @type {toa.origins.Manifest} */ {
21
- foo: {
22
- bar: 'dev://null'
23
- }
24
- }
25
-
26
- expect(() => manifest(input)).toThrow()
27
- })
28
-
29
- it('should pass if valid', async () => {
30
- const input = { foo: 'amqp://' + generate() }
31
-
32
- expect(() => manifest(input)).not.toThrow()
33
- })
34
-
35
- it('should fail if not uri', async () => {
36
- const input = { [generate()]: generate() }
37
-
38
- expect(() => manifest(input)).toThrow('must match format')
39
- })
40
-
41
- it('should throw if protocol is not supported', async () => {
42
- const input = { foo: 'wat://' + generate() }
43
-
44
- expect(() => manifest(input)).toThrow('is not supported')
45
- })
46
-
47
- it('should convert null to {}', async () => {
48
- const output = manifest(null)
49
-
50
- expect(output).toStrictEqual({})
51
- })
52
-
53
- it.each(PROTOCOLS)('should support %s protocol', async (protocol) => {
54
- const input = { foo: protocol + '//' + generate() }
55
-
56
- expect(() => manifest(input)).not.toThrow()
57
- })
58
-
59
- it('should handle placeholders', async () => {
60
- const input = { foo: 'http://${FOO}' + generate() + ':${BAR}/' } // eslint-disable-line no-template-curly-in-string
61
-
62
- expect(() => manifest(input)).not.toThrow()
63
- })
64
-
65
- it('should handle host shards', async () => {
66
- const input = { foo: 'http://{0-3}' + generate() }
67
-
68
- expect(() => manifest(input)).not.toThrow()
69
- })
70
-
71
- it('should handle port shards', async () => {
72
- const input = { foo: 'http://' + generate() + ':888{0-9}' }
73
-
74
- expect(() => manifest(input)).not.toThrow()
75
- })
76
-
77
- it.each(['dev:sec', 'dev'])('should throw if url contains credentials (%s)',
78
- async (credentials) => {
79
- const input = { foo: `http://${credentials}@${generate()}:888{0-9}` }
80
-
81
- expect(() => manifest(input)).toThrow('must not contain credentials')
82
- })
@@ -1,119 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { Connector } = require('@toa.io/core')
5
- const { sample } = require('@toa.io/generic')
6
-
7
- const comq = require('./.test/mock.comq')
8
- const mock = { comq }
9
-
10
- jest.mock('comq', () => mock.comq)
11
-
12
- const fixtures = require('./.test/aspect.fixtures')
13
- const { create } = require('./aspect')
14
-
15
- it('should be', async () => {
16
- expect(create).toBeInstanceOf(Function)
17
- })
18
-
19
- /** @type {toa.origins.amqp.Aspect} */
20
- let aspect
21
-
22
- /** @type {toa.origins.Manifest} */
23
- let manifest
24
-
25
- beforeEach(() => {
26
- jest.clearAllMocks()
27
-
28
- manifest = fixtures.manifest
29
- aspect = create(manifest)
30
- })
31
-
32
- it('should be instance of Connector', async () => {
33
- expect(aspect).toBeInstanceOf(Connector)
34
- })
35
-
36
- it('should expose name', async () => {
37
- expect(aspect.name).toStrictEqual('amqp')
38
- })
39
-
40
- it('should connect', async () => {
41
- await aspect.open()
42
-
43
- for (const reference of Object.values(manifest)) {
44
- expect(comq.connect).toHaveBeenCalledWith(reference)
45
- }
46
- })
47
-
48
- it('should connect to shards', async () => {
49
- jest.clearAllMocks()
50
-
51
- manifest = {
52
- test: 'amqps://host{0-2}.domain.com'
53
- }
54
-
55
- aspect = create(manifest)
56
-
57
- const shards = []
58
-
59
- for (let i = 0; i < 3; i++) shards.push(`amqps://host${i}.domain.com`)
60
-
61
- await aspect.open()
62
-
63
- expect(comq.connect).toHaveBeenCalledWith(...shards)
64
- })
65
-
66
- it('should disconnect', async () => {
67
- await aspect.open()
68
- await aspect.close()
69
-
70
- for (const reference of Object.values(manifest)) {
71
- const index = comq.connect.mock.calls.findIndex((call) => call[0] === reference)
72
- const io = await comq.connect.mock.results[index].value
73
-
74
- expect(io.close).toHaveBeenCalled()
75
- }
76
- })
77
-
78
- describe('invoke', () => {
79
- /** @type {jest.MockedObject<comq.IO>} */
80
- let io
81
-
82
- /** @type {string} */
83
- let origin
84
-
85
- let args
86
-
87
- beforeEach(async () => {
88
- await aspect.open()
89
-
90
- const origins = Object.keys(manifest)
91
-
92
- origin = sample(origins)
93
-
94
- const reference = manifest[origin]
95
- const index = comq.connect.mock.calls.findIndex((call) => call[0] === reference)
96
-
97
- io = await comq.connect.mock.results[index].value
98
- args = [generate(), generate(), generate()]
99
- })
100
-
101
- it('should be', async () => {
102
- expect(aspect.invoke).toBeInstanceOf(Function)
103
- })
104
-
105
- it.each(['emit', 'request'])('should %s', async (method) => {
106
- await aspect.invoke(origin, method, ...args)
107
-
108
- expect(io[method]).toHaveBeenCalledWith(...args)
109
- })
110
-
111
- it.each(['reply', 'consume', generate()])('should not expose %s',
112
- async (method) => {
113
- await expect(aspect.invoke(origin, method)).rejects.toThrow()
114
- })
115
-
116
- it('should throw if unknown origin', async () => {
117
- await expect(aspect.invoke(generate(), 'emit')).rejects.toThrow()
118
- })
119
- })
@@ -1,23 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { Permissions } = require('./permissions')
5
-
6
- it('should be', async () => {
7
- expect(Permissions).toBeInstanceOf(Function)
8
- })
9
-
10
- it('should substitute env vars', async () => {
11
- const name = 'FOO_VALUE'
12
- const value = generate()
13
-
14
- process.env[name] = value
15
-
16
- const properties = { '/http:\/\/domain.com\/${FOO_VALUE}/': true }
17
- const permissions = new Permissions(properties)
18
-
19
- expect(permissions.test('http://other.domain.com/')).toStrictEqual(false)
20
- expect(permissions.test('http://domain.com/' + value)).toStrictEqual(true)
21
-
22
- delete process.env[name]
23
- })