@toa.io/extensions.origins 0.7.1 → 0.8.0-dev.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 (46) hide show
  1. package/package.json +9 -8
  2. package/readme.md +57 -0
  3. package/source/.deployment/index.js +5 -0
  4. package/source/.deployment/uris.js +35 -0
  5. package/source/.test/constants.js +3 -0
  6. package/source/.test/deployment.fixtures.js +20 -0
  7. package/source/.test/factory.fixtures.js +13 -0
  8. package/source/constants.js +3 -0
  9. package/source/deployment.js +28 -0
  10. package/source/deployment.test.js +159 -0
  11. package/source/env.js +50 -0
  12. package/source/factory.js +34 -0
  13. package/source/factory.test.js +122 -0
  14. package/{src → source}/index.js +2 -0
  15. package/source/manifest.js +23 -0
  16. package/source/manifest.test.js +51 -0
  17. package/source/protocols/amqp/.test/aspect.fixtures.js +15 -0
  18. package/source/protocols/amqp/.test/mock.comq.js +13 -0
  19. package/source/protocols/amqp/aspect.js +75 -0
  20. package/source/protocols/amqp/aspect.test.js +119 -0
  21. package/source/protocols/amqp/deployment.js +57 -0
  22. package/source/protocols/amqp/index.js +9 -0
  23. package/source/protocols/amqp/protocols.js +3 -0
  24. package/{test → source/protocols/http/.test}/aspect.fixtures.js +6 -6
  25. package/{src → source/protocols/http}/aspect.js +13 -20
  26. package/{test → source/protocols/http}/aspect.test.js +11 -11
  27. package/source/protocols/http/index.js +7 -0
  28. package/source/protocols/http/protocols.js +3 -0
  29. package/source/protocols/index.js +6 -0
  30. package/source/schemas/annotations.cos.yaml +1 -0
  31. package/source/schemas/index.js +8 -0
  32. package/source/schemas/manifest.cos.yaml +1 -0
  33. package/types/amqp.ts +9 -0
  34. package/types/deployment.d.ts +7 -0
  35. package/types/{aspect.ts → http.ts} +1 -2
  36. package/src/.manifest/index.js +0 -7
  37. package/src/.manifest/normalize.js +0 -17
  38. package/src/.manifest/schema.yaml +0 -13
  39. package/src/.manifest/validate.js +0 -13
  40. package/src/factory.js +0 -14
  41. package/src/manifest.js +0 -13
  42. package/test/factory.fixtures.js +0 -5
  43. package/test/factory.test.js +0 -22
  44. package/test/manifest.fixtures.js +0 -11
  45. package/test/manifest.test.js +0 -58
  46. package/types/declaration.d.ts +0 -11
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+
5
+ const connect = jest.fn(async () => ({
6
+ emit: jest.fn(async () => undefined),
7
+ request: jest.fn(async () => generate()),
8
+ reply: jest.fn(async () => undefined),
9
+ consume: jest.fn(async () => undefined),
10
+ close: jest.fn(async () => undefined)
11
+ }))
12
+
13
+ exports.connect = connect
@@ -0,0 +1,75 @@
1
+ 'use strict'
2
+
3
+ const { connect } = require('comq')
4
+ const { Connector } = require('@toa.io/core')
5
+ const { shards } = require('@toa.io/generic')
6
+
7
+ /**
8
+ * @implements {toa.origins.amqp.Aspect}
9
+ */
10
+ class Aspect extends Connector {
11
+ name = 'amqp'
12
+ /** @type {toa.origins.Manifest} */
13
+ #manifest
14
+
15
+ /** @type {Record<string, Partial<comq.IO>>} */
16
+ #origins = {}
17
+
18
+ /**
19
+ * @param {toa.origins.Manifest} manifest
20
+ */
21
+ constructor (manifest) {
22
+ super()
23
+
24
+ this.#manifest = manifest
25
+ }
26
+
27
+ async open () {
28
+ const promises = Object.entries(this.#manifest).map(this.#open)
29
+
30
+ await Promise.all(promises)
31
+ }
32
+
33
+ async close () {
34
+ const promises = Object.values(this.#origins).map(this.#close)
35
+
36
+ await Promise.all(promises)
37
+ }
38
+
39
+ async invoke (origin, method, ...args) {
40
+ return this.#origins[origin][method](...args)
41
+ }
42
+
43
+ #open = async ([origin, reference]) => {
44
+ const references = shards(reference)
45
+ const io = await connect(...references)
46
+
47
+ this.#origins[origin] = restrict(io)
48
+ }
49
+
50
+ #close = async (io) => {
51
+ await io.close()
52
+ }
53
+ }
54
+
55
+ /**
56
+ * @param {comq.IO} io
57
+ * @return {Partial<comq.IO>}
58
+ */
59
+ function restrict (io) {
60
+ // noinspection JSUnresolvedReference
61
+ return {
62
+ request: (...args) => io.request(...args),
63
+ emit: (...args) => io.emit(...args),
64
+ close: () => io.close()
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @param {toa.origins.Manifest} manifest
70
+ */
71
+ function create (manifest) {
72
+ return new Aspect(manifest)
73
+ }
74
+
75
+ exports.create = create
@@ -0,0 +1,119 @@
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
+ })
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const { letters: { up } } = require('@toa.io/generic')
4
+ const protocols = require('./protocols')
5
+
6
+ /**
7
+ * @param {toa.norm.context.dependencies.Instance[]} instances
8
+ * @returns {toa.deployment.dependency.Variables}
9
+ */
10
+ function deployment (instances) {
11
+ /** @type {toa.deployment.dependency.Variables} */
12
+ const variables = {}
13
+
14
+ for (const { locator, manifest } of instances) {
15
+ for (const [origin, reference] of Object.entries(manifest)) {
16
+ const url = new URL(reference)
17
+
18
+ if (protocols.includes(url.protocol)) {
19
+ variables[locator.label] = secrets(locator, origin)
20
+ }
21
+ }
22
+ }
23
+
24
+ return variables
25
+ }
26
+
27
+ /**
28
+ * @param {toa.core.Locator} locator
29
+ * @param {string} origin
30
+ * @return {toa.deployment.dependency.Variable[]}
31
+ */
32
+ function secrets (locator, origin) {
33
+ const properties = ['username', 'password']
34
+
35
+ return properties.map((property) => secret(locator, origin, property))
36
+ }
37
+
38
+ /**
39
+ * @param {toa.core.Locator} locator
40
+ * @param {string} origin
41
+ * @param {string} property
42
+ * @return {toa.deployment.dependency.Variable}
43
+ */
44
+ function secret (locator, origin, property) {
45
+ const variable = `TOA_ORIGINS_${locator.uppercase}_${up(property)}`
46
+ const secret = `toa-origins-${locator.label}-${property}`
47
+
48
+ return {
49
+ name: variable,
50
+ secret: {
51
+ name: secret,
52
+ key: property
53
+ }
54
+ }
55
+ }
56
+
57
+ exports.deployment = deployment
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const protocols = require('./protocols')
4
+ const { create } = require('./aspect')
5
+ const { deployment } = require('./deployment')
6
+
7
+ exports.protocols = protocols
8
+ exports.create = create
9
+ exports.deployment = deployment
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = ['amqp:', 'amqps:']
@@ -2,11 +2,11 @@
2
2
 
3
3
  const { generate } = require('randomstring')
4
4
 
5
- const declaration = {
6
- origins: {
7
- foo: 'https://' + generate().toLowerCase(),
8
- amazon: 'https://*.*.amazon.com:*'
9
- }
5
+ /** @type {toa.origins.Manifest} */
6
+ const manifest = {
7
+ foo: 'https://' + generate().toLowerCase(),
8
+ amazon: 'https://*.*.amazon.com:*',
9
+ deep: 'http://www.domain.com/some/path/'
10
10
  }
11
11
 
12
12
  const responses = []
@@ -30,5 +30,5 @@ fetch.reset = () => {
30
30
  responses.length = 0
31
31
  }
32
32
 
33
- exports.declaration = declaration
33
+ exports.manifest = manifest
34
34
  exports.mock = { fetch }
@@ -6,22 +6,22 @@ const { Connector } = require('@toa.io/core')
6
6
  const { retry } = require('@toa.io/generic')
7
7
 
8
8
  /**
9
- * @implements {toa.extensions.origins.Aspect}
9
+ * @implements {toa.origins.http.Aspect}
10
10
  */
11
11
  class Aspect extends Connector {
12
12
  /** @readonly */
13
- name = 'origins'
13
+ name = 'http'
14
14
 
15
- /** @type {toa.extensions.origins.Origins} */
15
+ /** @type {toa.origins.Manifest} */
16
16
  #origins
17
17
 
18
18
  /**
19
- * @param {toa.extensions.origins.Declaration | Object} declaration
19
+ * @param {toa.origins.Manifest} manifest
20
20
  */
21
- constructor (declaration) {
21
+ constructor (manifest) {
22
22
  super()
23
23
 
24
- this.#origins = declaration.origins
24
+ this.#origins = manifest
25
25
  }
26
26
 
27
27
  async invoke (name, path, request, options) {
@@ -31,9 +31,7 @@ class Aspect extends Connector {
31
31
 
32
32
  if (options?.substitutions !== undefined) origin = substitute(origin, options.substitutions)
33
33
 
34
- const url = new URL(origin)
35
-
36
- if (path !== undefined) append(url, path)
34
+ const url = path === undefined ? new URL(origin) : new URL(path, origin)
37
35
 
38
36
  return this.#request(url.href, request, options?.retry)
39
37
  }
@@ -78,18 +76,13 @@ const substitute = (origin, substitutions) => {
78
76
  return origin.replace(PLACEHOLDER, replace)
79
77
  }
80
78
 
79
+ const PLACEHOLDER = /\*/g
80
+
81
81
  /**
82
- * @param {URL} url
83
- * @param {string} path
82
+ * @param {toa.origins.Manifest} manifest
84
83
  */
85
- const append = (url, path) => {
86
- const [pathname, search] = path.split('?')
87
-
88
- url.pathname = pathname
89
-
90
- if (search !== undefined) url.search = search
84
+ function create (manifest) {
85
+ return new Aspect(manifest)
91
86
  }
92
87
 
93
- const PLACEHOLDER = /\*/g
94
-
95
- exports.Aspect = Aspect
88
+ exports.create = create
@@ -6,19 +6,19 @@ const { random } = require('@toa.io/generic')
6
6
 
7
7
  const { Connector } = require('@toa.io/core')
8
8
 
9
- const fixtures = require('./aspect.fixtures')
9
+ const fixtures = require('./.test/aspect.fixtures')
10
10
  const mock = fixtures.mock
11
11
 
12
12
  jest.mock('node-fetch', () => mock.fetch)
13
13
 
14
- const { Aspect } = require('../src/aspect')
14
+ const { create } = require('./aspect')
15
15
 
16
16
  /** @type {toa.extensions.origins.Aspect} */ let aspect
17
17
 
18
18
  beforeEach(() => {
19
19
  jest.clearAllMocks()
20
20
 
21
- aspect = new Aspect(fixtures.declaration)
21
+ aspect = create(fixtures.manifest)
22
22
  })
23
23
 
24
24
  it('should be instance of core.Connector', () => {
@@ -26,7 +26,7 @@ it('should be instance of core.Connector', () => {
26
26
  })
27
27
 
28
28
  it('should have name \'origins\'', () => {
29
- expect(aspect.name).toStrictEqual('origins')
29
+ expect(aspect.name).toStrictEqual('http')
30
30
  })
31
31
 
32
32
  describe('invoke', () => {
@@ -57,15 +57,15 @@ describe('invoke', () => {
57
57
  await expect(() => aspect.invoke('bar', path, request)).rejects.toThrow(/is not defined/)
58
58
  })
59
59
 
60
- it('should not resolve absolute urls', async () => {
60
+ it('should resolve URL', async () => {
61
61
  jest.clearAllMocks()
62
62
  mock.fetch.respond(200, response)
63
63
 
64
- const path = 'https://toa.io'
64
+ const path = 'ok'
65
65
 
66
- await aspect.invoke(name, path, clone(request))
66
+ await aspect.invoke('deep', path, clone(request))
67
67
 
68
- expect(mock.fetch.mock.calls[0][0]).toStrictEqual(fixtures.declaration.origins.foo + '/' + path)
68
+ expect(mock.fetch.mock.calls[0][0]).toStrictEqual(fixtures.manifest.deep + path)
69
69
  })
70
70
 
71
71
  it('should substitute wildcards', async () => {
@@ -91,7 +91,7 @@ describe('invoke', () => {
91
91
 
92
92
  const url = mock.fetch.mock.calls[0][0]
93
93
 
94
- expect(url).toStrictEqual(fixtures.declaration.origins.foo + '/' + path)
94
+ expect(url).toStrictEqual(fixtures.manifest.foo + '/' + path)
95
95
  })
96
96
 
97
97
  it('should not throw if path is not defined', async () => {
@@ -107,7 +107,7 @@ describe('invoke', () => {
107
107
  })
108
108
 
109
109
  it('should pass url', () => {
110
- expect(call[0]).toStrictEqual(fixtures.declaration.origins.foo + path)
110
+ expect(call[0]).toStrictEqual(fixtures.manifest.foo + path)
111
111
  })
112
112
 
113
113
  it('should pass request', () => {
@@ -131,7 +131,7 @@ describe('invoke', () => {
131
131
 
132
132
  mock.fetch.respond(200, response)
133
133
 
134
- /** @type {toa.extensions.origins.invocation.Options} */
134
+ /** @type {toa.origins.http.invocation.Options} */
135
135
  const options = {
136
136
  retry: { base: 0, retries: attempts }
137
137
  }
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const protocols = require('./protocols')
4
+ const { create } = require('./aspect')
5
+
6
+ exports.protocols = protocols
7
+ exports.create = create
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = ['http:', 'https:']
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ const http = require('./http')
4
+ const amqp = require('./amqp')
5
+
6
+ module.exports = [http, amqp]
@@ -0,0 +1 @@
1
+ ~: ref:manifest
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ const schemas = require('@toa.io/schemas')
4
+
5
+ const namespace = schemas.namespace(__dirname)
6
+
7
+ exports.manifest = namespace.schema('manifest')
8
+ exports.annotations = namespace.schema('annotations')
@@ -0,0 +1 @@
1
+ ~: uri
package/types/amqp.ts ADDED
@@ -0,0 +1,9 @@
1
+ import * as _extensions from '@toa.io/core/types/extensions'
2
+
3
+ declare namespace toa.origins.amqp {
4
+
5
+ interface Aspect extends _extensions.Aspect {
6
+ invoke(origin: string, method: string, ...args: any[]): Promise<any>
7
+ }
8
+
9
+ }
@@ -0,0 +1,7 @@
1
+ declare namespace toa.origins {
2
+
3
+ type Manifest = Record<string, string>
4
+
5
+ type Annotations = Record<string, Manifest>
6
+
7
+ }
@@ -1,9 +1,8 @@
1
1
  import * as fetch from 'node-fetch'
2
-
3
2
  import * as _extensions from '@toa.io/core/types/extensions'
4
3
  import * as _retry from '@toa.io/generic/types/retry'
5
4
 
6
- declare namespace toa.extensions.origins {
5
+ declare namespace toa.origins.http {
7
6
 
8
7
  namespace invocation {
9
8
  type Options = {
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const { normalize } = require('./normalize')
4
- const { validate } = require('./validate')
5
-
6
- exports.normalize = normalize
7
- exports.validate = validate
@@ -1,17 +0,0 @@
1
- 'use strict'
2
-
3
- /**
4
- * @returns {toa.extensions.origins.Declaration}
5
- */
6
- const normalize = (declaration) => {
7
- declaration = origins(declaration)
8
-
9
- return declaration
10
- }
11
-
12
- const origins = (declaration) => {
13
- if (declaration.origins !== undefined) return declaration
14
- else return { origins: { ...declaration } }
15
- }
16
-
17
- exports.normalize = normalize
@@ -1,13 +0,0 @@
1
- $schema: https://json-schema.org/draft/2019-09/schema
2
- $id: https://schemas.toa.io/0.0.0/extensions/origins/declaration
3
-
4
- type: object
5
- properties:
6
- origins:
7
- type: object
8
- minProperties: 1
9
- patternProperties:
10
- '.*':
11
- type: string
12
- pattern: ^https?:\/\/(?=.{1,254}(?::|$))(?:(?![a-z0-9\-]{1,62}-(?:\.|:|$))[a-z0-9\-]{1,63}\b(?!\.$)\.?)+(:\d+)?$
13
- required: [origins]
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const path = require('path')
4
-
5
- const { Schema } = require('@toa.io/schema')
6
- const { load } = require('@toa.io/yaml')
7
-
8
- const schema = load.sync(path.resolve(__dirname, 'schema.yaml'))
9
- const validator = new Schema(schema)
10
-
11
- const validate = (declaration) => validator.validate(declaration)
12
-
13
- exports.validate = validate
package/src/factory.js DELETED
@@ -1,14 +0,0 @@
1
- 'use strict'
2
-
3
- const { Aspect } = require('./aspect')
4
-
5
- /**
6
- * @implements {toa.core.extensions.Factory}
7
- */
8
- class Factory {
9
- aspect (locator, declaration) {
10
- return new Aspect(declaration)
11
- }
12
- }
13
-
14
- exports.Factory = Factory
package/src/manifest.js DELETED
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const { normalize, validate } = require('./.manifest')
4
-
5
- const manifest = (declaration) => {
6
- declaration = normalize(declaration)
7
-
8
- validate(declaration)
9
-
10
- return declaration
11
- }
12
-
13
- exports.manifest = manifest
@@ -1,5 +0,0 @@
1
- 'use strict'
2
-
3
- const { declaration } = require('./manifest.fixtures')
4
-
5
- exports.declaration = declaration
@@ -1,22 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
-
5
- const { Aspect } = require('../src/aspect')
6
- const { Locator } = require('@toa.io/core')
7
-
8
- const fixtures = require('./factory.fixtures')
9
- const { Factory } = require('../src')
10
-
11
- /** @type {toa.core.extensions.Factory} */
12
- let factory
13
-
14
- beforeEach(() => {
15
- factory = new Factory()
16
- })
17
-
18
- it('should create context extension', () => {
19
- const extension = factory.aspect(new Locator(generate(), generate()), fixtures.declaration)
20
-
21
- expect(extension).toBeInstanceOf(Aspect)
22
- })
@@ -1,11 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
-
5
- const declaration = {
6
- origins: {
7
- [generate()]: 'https://toa.io'
8
- }
9
- }
10
-
11
- exports.declaration = declaration