@toa.io/pointer 0.2.1-dev.3

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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@toa.io/pointer",
3
+ "version": "0.2.1-dev.3",
4
+ "description": "Toa Pointer",
5
+ "homepage": "https://toa.io",
6
+ "author": {
7
+ "name": "Artem Gurtovoi",
8
+ "url": "https://github.com/temich"
9
+ },
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/toa-io/toa.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/toa-io/toa/issues"
17
+ },
18
+ "engines": {
19
+ "node": ">= 12.0.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "main": "source/index.js",
25
+ "dependencies": {
26
+ "@toa.io/console": "*",
27
+ "@toa.io/generic": "*",
28
+ "@toa.io/schema": "*",
29
+ "@toa.io/yaml": "*"
30
+ }
31
+ }
package/readme.md ADDED
@@ -0,0 +1,126 @@
1
+ # Pointer
2
+
3
+ Library to declare, deploy and resolve URL to connect to at the runtime. Used by connectors.
4
+
5
+ ## Pointer class
6
+
7
+ Pointer builds URL for a given locator, protocol and a package prefix. Prefix must be unique value
8
+ for a given package conforming [label format](#) (ex.: `bindings-ampq`).
9
+
10
+ ### Constructor signature
11
+
12
+ <dl>
13
+ <dt><code>prefix</code></dt>
14
+ <dd><code>string</code> unique for each package</dd>
15
+ <dt><code>locator</code></dt>
16
+ <dd><code>toa.core.Locator</code> component for which Pointer is intended</dd>
17
+ <dt><code>options</code></dt>
18
+ <dd><code><a href="./types/pointer.d.ts">toa.pointer.Options</a></code> Pointer options</dd>
19
+ </dl>
20
+
21
+ ### Local Environment
22
+
23
+ If `TOA_ENV` is `local` then these values are used:
24
+
25
+ | Property | Value |
26
+ |----------|-------------|
27
+ | hostname | `localhost` |
28
+ | username | `developer` |
29
+ | password | `secret` |
30
+
31
+ > In the local environment there is no way to provide values for `protocol` or `port`.
32
+
33
+ See [types](types/pointer.d.ts) and [tests](test/pointer.test.js) for details.
34
+
35
+ ## Annotation
36
+
37
+ Declaration of a set of hosts, matching exact components, namespaces with a default value for
38
+ non-matched ones.
39
+
40
+ ```yaml
41
+ something:
42
+ default: protocol://host1
43
+ namespace1: protocol://host2
44
+ namespace1.component1: protocol://host3
45
+ namespace2.component2: protocol://host4
46
+ ```
47
+
48
+ See the [schema](source/uris/.construct/schema.yaml).
49
+
50
+ ### Concise Declaration
51
+
52
+ Next two declarations are equivalent.
53
+
54
+ ```yaml
55
+ something: host1
56
+ ```
57
+
58
+ ```yaml
59
+ something:
60
+ default: host1
61
+ ```
62
+
63
+ ### Usage
64
+
65
+ ```javascript
66
+ const { uris } = require('@toa.io/pointer')
67
+
68
+ const annotation = uris.construct(declaration)
69
+ const url = uris.resolve(annotation, locator)
70
+ ```
71
+
72
+ See [types](types/uris.d.ts) and [tests](test/uris.test.js) for details.
73
+
74
+ ### Custom Extensions
75
+
76
+ Packages using Pointer Annotation may use or require additional properties.
77
+ See [Deployment Extensions](#extensions) below.
78
+
79
+ ## Deployment
80
+
81
+ Deployment function builds global[^1] variables: containing Annotation and a set of variables for
82
+ pointer credentials.
83
+
84
+ See [types](types/deployment.d.ts) and [tests](test/deployment.test.js) for details.
85
+
86
+ ### Credentials
87
+
88
+ Each entry of the Annotation requires values for `username` and `password`. These values are being
89
+ deployed as global[^1] secret variables. Secret names are following the
90
+ convention: `toa-package-prefix-entry-label` and `key` names match corresponding URL
91
+ properties (`username` and `password`).
92
+
93
+ > Note that declaration is made of `Locator.id` while secret names contain `Locator.label`.
94
+ > See [Locator](#).
95
+
96
+ #### Example
97
+
98
+ ```yaml
99
+ # context.toa.yaml
100
+ amqp:
101
+ default: amqp://host0
102
+ dummies.dummy: amqps://host1:5671
103
+ ```
104
+
105
+ ```shell
106
+ $ toa conceal bindings-amqp-default username admin
107
+ $ toa conceal bindings-amqp-default password ibreakthings
108
+ $ toa conceal bindings-amqp-dummies-dummy username developer
109
+ $ toa conceal bindings-amqp-dummies-dummy password iluvtests
110
+ ```
111
+
112
+ See [`toa conceal`](../../runtime/cli/readme.md#conceal).
113
+
114
+ ### Extensions
115
+
116
+ For each custom extension additional proxy is being deployed.
117
+
118
+ #### Example
119
+
120
+ AMQP binding is a systematic binding used by the runtime. That is a system proxy must be deployed
121
+ and compositions require variables to establish connection through that proxy.
122
+
123
+ Therefore, AMQP binding declares a `system` extension and deployment function declares additional
124
+ proxy `bindings-amqp-system`.
125
+
126
+ [^1]: [#174](https://github.com/toa-io/toa/issues/174)
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const { concat, letters: { up, down } } = require('@toa.io/generic')
4
+
5
+ /**
6
+ * @param {string} prefix
7
+ * @param {toa.pointer.URIs} uris
8
+ * @returns {toa.deployment.dependency.Variable[]}
9
+ */
10
+ const credentials = (prefix, uris) => {
11
+ const variables = []
12
+
13
+ for (const key of Object.keys(uris)) {
14
+ const [namespace, name] = key.split('.')
15
+ const entry = namespace + concat('-', name)
16
+
17
+ const env = `TOA_${up(prefix)}_${up(entry)}_`
18
+ const sec = `toa-${down(prefix)}-${down(entry)}`
19
+
20
+ for (const property of ['username', 'password']) {
21
+ const name = env + up(property)
22
+
23
+ const secret = {
24
+ name: sec,
25
+ key: property
26
+ }
27
+
28
+ variables.push({ name, secret })
29
+ }
30
+ }
31
+
32
+ return variables
33
+ }
34
+
35
+ exports.credentials = credentials
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { credentials } = require('./credentials')
4
+ const { pointer } = require('./pointer')
5
+
6
+ exports.credentials = credentials
7
+ exports.pointer = pointer
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const { encode, letters: { up } } = require('@toa.io/generic')
4
+
5
+ /**
6
+ * @param {string} prefix
7
+ * @param {toa.pointer.URIs} uris
8
+ * @returns {toa.deployment.dependency.Variable}
9
+ */
10
+ const pointer = (prefix, uris) => {
11
+ const name = `TOA_${up(prefix)}_POINTER`
12
+ const json = JSON.stringify(uris)
13
+ const value = encode(json)
14
+
15
+ return { name, value }
16
+ }
17
+
18
+ exports.pointer = pointer
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ const { validate } = require('./validate')
4
+ const { variables } = require('./variables')
5
+
6
+ exports.validate = validate
7
+ exports.variables = variables
8
+
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @param {toa.pointer.URIs} uris
5
+ */
6
+ const validate = (uris) => {
7
+ Object.values(uris).forEach(check)
8
+ }
9
+
10
+ /**
11
+ * @param {string} uri
12
+ */
13
+ const check = (uri) => {
14
+ const url = new URL(uri)
15
+
16
+ if (url.hostname === '') throw new Error(`URI ${url.href} must contain a hostname`)
17
+ }
18
+
19
+ exports.validate = validate
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const declare = require('./.variables')
4
+
5
+ /**
6
+ * @param {toa.pointer.URIs} uris
7
+ * @param {string} prefix
8
+ * @returns {toa.deployment.dependency.Variable[]}
9
+ */
10
+ const variables = (uris, prefix) => {
11
+ /** @type {toa.deployment.dependency.Variable[]} */
12
+ const variables = []
13
+ const pointer = declare.pointer(prefix, uris)
14
+ const credentials = declare.credentials(prefix, uris)
15
+
16
+ variables.push(pointer)
17
+ variables.push(...credentials)
18
+
19
+ return variables
20
+ }
21
+
22
+ exports.variables = variables
@@ -0,0 +1,44 @@
1
+ 'use strict'
2
+
3
+ const { concat, decode, letters: { up, capitalize } } = require('@toa.io/generic')
4
+ const { console } = require('@toa.io/console')
5
+
6
+ const { resolve } = require('../uris')
7
+
8
+ /**
9
+ * @param {string} prefix
10
+ * @param {toa.core.Locator} locator
11
+ */
12
+ const env = (prefix, locator) => {
13
+ const key = `TOA_${up(prefix)}_POINTER`
14
+ const value = process.env[key]
15
+
16
+ if (value === undefined) throw new Error(`Environment variable ${key} is not set`)
17
+
18
+ const uris = decode(value)
19
+ const { url, entry } = resolve(locator, uris)
20
+
21
+ const suffix = convert(entry)
22
+ const env = `TOA_${up(prefix)}_${suffix}`
23
+
24
+ for (const property of ['username', 'password']) {
25
+ const value = process.env[env + '_' + up(property)]
26
+
27
+ if (value !== undefined) url[property] = value
28
+ else console.warn(`${capitalize(property)} for ${env} is not set`)
29
+ }
30
+
31
+ return url
32
+ }
33
+
34
+ /**
35
+ * @param {string} entry
36
+ * @returns {string}
37
+ */
38
+ const convert = (entry) => {
39
+ const [namespace, name] = entry.toUpperCase().split('.')
40
+
41
+ return namespace + concat('_', name)
42
+ }
43
+
44
+ exports.env = env
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { env } = require('./env')
4
+ const { local } = require('./local')
5
+
6
+ exports.env = env
7
+ exports.local = local
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @param {string} protocol
5
+ * @returns {URL}
6
+ */
7
+ const local = (protocol) => {
8
+ const url = new URL(protocol + '//')
9
+
10
+ url.hostname = 'localhost'
11
+ url.username = 'developer'
12
+ url.password = 'secret'
13
+
14
+ return url
15
+ }
16
+
17
+ exports.local = local
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const get = require('./.deployment')
4
+
5
+ /** @type {toa.pointer.Deployment} */
6
+ const deployment = (instances, uris, options) => {
7
+ if (typeof uris === 'string') uris = { default: uris }
8
+
9
+ get.validate(uris)
10
+
11
+ const global = get.variables(uris, options.prefix)
12
+ const variables = { global }
13
+
14
+ return { variables }
15
+ }
16
+
17
+ exports.deployment = deployment
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ const { Pointer } = require('./pointer')
4
+ const { deployment } = require('./deployment')
5
+ const { variable } = require('./variable')
6
+
7
+ exports.uris = require('./uris')
8
+
9
+ exports.Pointer = Pointer
10
+ exports.deployment = deployment
11
+ exports.variable = variable
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const get = require('./.pointer')
4
+
5
+ // noinspection JSClosureCompilerSyntax
6
+ /**
7
+ * @implements {toa.pointer.Pointer}
8
+ */
9
+ class Pointer {
10
+ protocol
11
+ host
12
+ port
13
+ hostname
14
+ path
15
+ reference
16
+ label
17
+
18
+ /**
19
+ * @param {string} prefix
20
+ * @param {toa.core.Locator} locator
21
+ * @param {toa.pointer.Options} options
22
+ */
23
+ constructor (prefix, locator, options) {
24
+ const local = process.env.TOA_ENV === 'local'
25
+ const url = local ? get.local(options.protocol) : get.env(prefix, locator)
26
+
27
+ this.protocol = url.protocol
28
+ this.hostname = url.hostname
29
+ this.port = Number(url.port)
30
+ this.path = url.pathname
31
+ this.username = url.username
32
+ this.password = url.password
33
+ this.reference = url.href
34
+ this.label = label(url)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @param {URL} url
40
+ * @returns {string}
41
+ */
42
+ const label = (url) => {
43
+ const safe = new URL(url.href)
44
+
45
+ safe.password = ''
46
+
47
+ return safe.href
48
+ }
49
+
50
+ exports.Pointer = Pointer
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { normalize } = require('./normalize')
4
+ const { validate } = require('./validate')
5
+
6
+ exports.normalize = normalize
7
+ exports.validate = validate
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @param {toa.pointer.URIs | string} declaration
5
+ * @returns {toa.pointer.URIs}
6
+ */
7
+ const normalize = (declaration) => {
8
+ /** @type {toa.pointer.URIs} */
9
+ let annotation
10
+
11
+ if (typeof declaration === 'string') annotation = { default: declaration }
12
+ else annotation = declaration
13
+
14
+ return annotation
15
+ }
16
+
17
+ exports.normalize = normalize
@@ -0,0 +1,19 @@
1
+ $schema: https://json-schema.org/draft/2019-09/schema
2
+ $id: https://schemas.toa.io/0.0.0/storages/mongodb/annotations
3
+
4
+ definitions:
5
+ node:
6
+ oneOf:
7
+ - $ref: '#/definitions/locator'
8
+ - type: object
9
+ patternProperties:
10
+ '.*':
11
+ $ref: '#/definitions/node'
12
+ locator:
13
+ type: string
14
+ format: uri
15
+
16
+ type: object
17
+ patternProperties:
18
+ '.*':
19
+ $ref: '#/definitions/node'
@@ -0,0 +1,17 @@
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) => {
12
+ const error = validator.fit(declaration)
13
+
14
+ if (error) throw new TypeError(error.message)
15
+ }
16
+
17
+ exports.validate = validate
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const { normalize, validate } = require('./.construct')
4
+
5
+ /** @type {toa.pointer.uris.Constructor} */
6
+ const construct = (declaration) => {
7
+ const normalized = normalize(declaration)
8
+
9
+ validate(normalized)
10
+
11
+ return normalized
12
+ }
13
+
14
+ exports.construct = construct
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { construct } = require('./construct')
4
+ const { resolve } = require('./resolve')
5
+
6
+ exports.construct = construct
7
+ exports.resolve = resolve
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ /** @type {toa.pointer.uris.Resolver} */
4
+ const resolve = (locator, uris) => {
5
+ let entry
6
+
7
+ /** @type {string} */
8
+ let uri
9
+
10
+ const entries = [locator.id, locator.namespace, 'default']
11
+
12
+ for (const key of entries) {
13
+ if (typeof uris[key] === 'string') {
14
+ uri = uris[key]
15
+ entry = key
16
+
17
+ break
18
+ }
19
+ }
20
+
21
+ if (uri === undefined) throw new Error(`URI annotation for '${locator.id}' is not found`)
22
+
23
+ const url = new URL(uri)
24
+
25
+ if (url.hostname === '') throw new Error(`URI annotation for '${locator.id}' must contain hostname`)
26
+
27
+ return { url, entry }
28
+ }
29
+
30
+ exports.resolve = resolve
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const { letters: { up } } = require('@toa.io/generic')
4
+
5
+ /** @type {toa.pointer.Variable} */
6
+ const variable = (scope, locator, label, value) => {
7
+ const name = `TOA_${up(scope)}_${locator.uppercase}_${up(label)}`
8
+
9
+ return { name, value }
10
+ }
11
+
12
+ exports.variable = variable
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ const { encode, letters: { up, down } } = require('@toa.io/generic')
4
+ const mock = require('@toa.io/mock')
5
+
6
+ const { deployment } = require('../')
7
+
8
+ const prefix = 'foo-bar'
9
+
10
+ /** @type {toa.norm.context.dependencies.Instance[]} */
11
+ let instances
12
+
13
+ /** @type {toa.pointer.deployment.Options} */
14
+ let options
15
+
16
+ beforeEach(() => {
17
+ instances = mock.dependencies.instances()
18
+ options = { prefix }
19
+ })
20
+
21
+ it('should be', () => {
22
+ expect(deployment).toBeInstanceOf(Function)
23
+ })
24
+
25
+ it('should throw if protocol is omitted', () => {
26
+ const annotation = { default: 'no-protocol' }
27
+
28
+ expect(() => deployment(instances, annotation, options)).toThrow('Invalid URL')
29
+ })
30
+
31
+ it('should throw if hostname is omitted', () => {
32
+ const annotation = { default: 'amqps:///' }
33
+
34
+ expect(() => deployment(instances, annotation, options)).toThrow('must contain a hostname')
35
+ })
36
+
37
+ it('should consider string annotation as default', () => {
38
+ const annotation = 'pg://host0'
39
+
40
+ const output = deployment(instances, annotation, options)
41
+
42
+ const name = `TOA_${up(prefix)}_POINTER`
43
+ const value = encode({ default: annotation })
44
+
45
+ expect(output.variables.global).toStrictEqual(expect.arrayContaining([{ name, value }]))
46
+ })
47
+
48
+ describe('variables', () => {
49
+ it('should export POINTER variable', () => {
50
+ const annotation = { default: 'amqps://host0' }
51
+ const json = JSON.stringify(annotation)
52
+ const value = encode(json)
53
+ const output = deployment(instances, annotation, options)
54
+
55
+ expect(output.variables.global).toStrictEqual(expect.arrayContaining([{
56
+ name: `TOA_${up(prefix)}_POINTER`,
57
+ value
58
+ }]))
59
+ })
60
+
61
+ it('should export USERNAME and PASSWORD for each entry of URI Set', () => {
62
+ const annotation = {
63
+ default: 'amqps://host0',
64
+ system: 'amqps://host2',
65
+ dummies: 'amqps://host3',
66
+ 'dummies.one': 'amqps://host4'
67
+ }
68
+
69
+ const expected = []
70
+
71
+ const env = `TOA_${up(prefix)}_`
72
+ const sec = `toa-${down(prefix)}-`
73
+
74
+ for (const key of Object.keys(annotation)) {
75
+ const label = key.replaceAll('.', '-').toLowerCase()
76
+
77
+ for (const property of ['username', 'password']) {
78
+ expected.push({
79
+ name: `${env}${up(label)}_${up(property)}`,
80
+ secret: {
81
+ name: `${sec}${down(label)}`,
82
+ key: property
83
+ }
84
+ })
85
+ }
86
+ }
87
+
88
+ const output = deployment(instances, annotation, options)
89
+
90
+ expect(output.variables.global).toStrictEqual(expect.arrayContaining(expected))
91
+ })
92
+ })
@@ -0,0 +1,213 @@
1
+ 'use strict'
2
+
3
+ const mock = { console: { info: jest.fn(), warn: jest.fn() } }
4
+
5
+ jest.mock('@toa.io/console', () => mock)
6
+
7
+ const { generate } = require('randomstring')
8
+ const { Locator } = require('@toa.io/core')
9
+ const { encode, letters: { up } } = require('@toa.io/generic')
10
+
11
+ const { Pointer } = require('../')
12
+
13
+ it('should be', () => {
14
+ expect(Pointer).toBeDefined()
15
+ expect(Pointer).toBeInstanceOf(Function)
16
+ })
17
+
18
+ const prefix = 'bindings-amqp'
19
+
20
+ /** @type {toa.core.Locator} */
21
+ let locator
22
+
23
+ /** @type {toa.pointer.Options} */
24
+ const options = { protocol: 'protocol:' }
25
+
26
+ /** @type {toa.pointer.Pointer} */
27
+ let pointer
28
+
29
+ /** @type {URL} */
30
+ let url
31
+
32
+ const uris = (uris) => (process.env[`TOA_${up(prefix)}_POINTER`] = encode(uris))
33
+
34
+ beforeEach(() => {
35
+ jest.clearAllMocks()
36
+
37
+ const name = generate()
38
+ const namespace = generate()
39
+
40
+ locator = new Locator(name, namespace)
41
+
42
+ url = new URL('amqp://whatever:3000')
43
+
44
+ uris({ default: url.href })
45
+ })
46
+
47
+ it('should throw if env not set', () => {
48
+ const name = `TOA_${up(prefix)}_POINTER`
49
+ delete process.env[name]
50
+
51
+ expect(() => new Pointer(prefix, locator, options)).toThrow(`Environment variable ${name} is not set`)
52
+ })
53
+
54
+ it('should use declared protocol', () => {
55
+ pointer = new Pointer(prefix, locator, options)
56
+
57
+ expect(pointer.protocol).toStrictEqual(url.protocol)
58
+ })
59
+
60
+ it('should point to host', () => {
61
+ pointer = new Pointer(prefix, locator, options)
62
+
63
+ expect(pointer.hostname).toStrictEqual(url.hostname)
64
+ })
65
+
66
+ it('should resolve credentials', () => {
67
+ const check = (entry) => {
68
+ const env = `TOA_${up(prefix)}_${up(entry)}_`
69
+ const username = generate()
70
+ const password = generate()
71
+
72
+ process.env[env + 'USERNAME'] = username
73
+ process.env[env + 'PASSWORD'] = password
74
+
75
+ pointer = new Pointer(prefix, locator, options)
76
+
77
+ delete process.env[env + 'USERNAME']
78
+ delete process.env[env + 'PASSWORD']
79
+
80
+ expect(pointer.username).toStrictEqual(username)
81
+ expect(pointer.password).toStrictEqual(password)
82
+ }
83
+
84
+ // default resolution
85
+ check('default')
86
+
87
+ // namespace resolution
88
+ uris({ [locator.namespace]: 'pg://host0/db' })
89
+ check(locator.namespace)
90
+
91
+ // id resolution
92
+ uris({ [locator.id]: 'pg://host0/db' })
93
+ check(locator.label)
94
+ })
95
+
96
+ it('should expose port as number', () => {
97
+ pointer = new Pointer(prefix, locator, options)
98
+
99
+ expect(pointer.port).toStrictEqual(Number(url.port))
100
+ })
101
+
102
+ it('should expose reference', () => {
103
+ pointer = new Pointer(prefix, locator, options)
104
+
105
+ expect(pointer.reference).toStrictEqual(url.href)
106
+ })
107
+
108
+ it('should use localhost and given protocol on local environment', () => {
109
+ process.env.TOA_ENV = 'local'
110
+
111
+ pointer = new Pointer(prefix, locator, options)
112
+
113
+ delete process.env.TOA_ENV
114
+
115
+ expect(pointer.hostname).toStrictEqual('localhost')
116
+ expect(pointer.protocol).toStrictEqual(options.protocol)
117
+ })
118
+
119
+ it('should use default protocol and credentials on local environment', () => {
120
+ process.env.TOA_ENV = 'local'
121
+
122
+ pointer = new Pointer(prefix, locator, options)
123
+
124
+ delete process.env.TOA_ENV
125
+
126
+ expect(pointer.reference).toStrictEqual(`${options.protocol}//developer:secret@localhost`)
127
+ expect(pointer.label).toStrictEqual(`${options.protocol}//developer@localhost`)
128
+ })
129
+
130
+ describe('environment variables', () => {
131
+ const username = generate()
132
+ const password = generate()
133
+
134
+ const env = `TOA_${up(prefix)}_DEFAULT`
135
+
136
+ let set
137
+ let unset
138
+
139
+ beforeEach(() => {
140
+ jest.clearAllMocks()
141
+
142
+ set = (name, value, prefix = env) => {
143
+ const key = prefix + '_' + up(name)
144
+
145
+ process.env[key] = value
146
+ url[name] = value
147
+ }
148
+
149
+ unset = (name, prefix = env) => {
150
+ const key = prefix + '_' + up(name)
151
+
152
+ delete process.env[key]
153
+ url[name] = ''
154
+ }
155
+
156
+ set('username', username)
157
+ set('password', password)
158
+ })
159
+
160
+ afterEach(() => {
161
+ unset('username')
162
+ unset('password')
163
+ })
164
+
165
+ it('should use env variables for credentials', () => {
166
+ pointer = new Pointer(prefix, locator, options)
167
+
168
+ expect(pointer.reference).toStrictEqual(url.href)
169
+
170
+ url.password = ''
171
+
172
+ expect(pointer.label).toStrictEqual(url.href)
173
+ })
174
+
175
+ it('should warn if username is not set', () => {
176
+ url.hostname = locator.hostname(prefix)
177
+
178
+ unset('username')
179
+ url.password = ''
180
+
181
+ const pointer = new Pointer(prefix, locator, options)
182
+
183
+ expect(pointer).toBeDefined()
184
+ expect(mock.console.warn).toHaveBeenCalledWith(`Username for ${env} is not set`)
185
+ })
186
+
187
+ it('should warn if password is not set', () => {
188
+ url.hostname = locator.hostname(prefix)
189
+
190
+ unset('password')
191
+
192
+ const pointer = new Pointer(prefix, locator, options)
193
+
194
+ expect(pointer).toBeDefined()
195
+ expect(mock.console.warn).toHaveBeenCalledWith(`Password for ${env} is not set`)
196
+ })
197
+
198
+ it('should not warn it credentials are set', () => {
199
+ uris({ [locator.id]: 'amqp://host0' })
200
+
201
+ const username = generate()
202
+ const password = generate()
203
+ const env = `TOA_${up(prefix)}_${locator.uppercase}`
204
+
205
+ set('username', username, env)
206
+ set('password', password, env)
207
+
208
+ const pointer = new Pointer(prefix, locator, options)
209
+
210
+ expect(pointer).toBeDefined()
211
+ expect(mock.console.warn).not.toHaveBeenCalled()
212
+ })
213
+ })
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const annotations = {
4
+ default: 'http://host.docker.internal/',
5
+ credits: {
6
+ balance: 'amqp://host.docker.internal'
7
+ }
8
+ }
9
+
10
+ exports.annotations = annotations
@@ -0,0 +1,140 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+ const { generate } = require('randomstring')
5
+ const { random } = require('@toa.io/generic')
6
+ const { Locator } = require('@toa.io/core')
7
+
8
+ const fixtures = require('./uris.fixtures')
9
+ const { uris } = require('../')
10
+
11
+ const gen = () => 'schema://host-' + generate().toLowerCase() + ':' + (random(1000) + 1000)
12
+
13
+ describe('construct', () => {
14
+ const construct = uris.construct
15
+
16
+ let annotations
17
+
18
+ beforeEach(() => {
19
+ annotations = clone(fixtures.annotations)
20
+ })
21
+
22
+ it('should be', () => {
23
+ expect(construct).toBeDefined()
24
+ expect(construct).toBeInstanceOf(Function)
25
+ })
26
+
27
+ describe('normalize', () => {
28
+ it('should treat string value as default', () => {
29
+ const declaration = gen()
30
+ const result = construct(declaration)
31
+
32
+ expect(result).toStrictEqual({ default: declaration })
33
+ })
34
+
35
+ it('should allow dot notation', () => {
36
+ const url = gen()
37
+
38
+ annotations['foo.bar'] = url
39
+
40
+ construct(annotations)
41
+
42
+ expect(annotations['foo.bar']).toStrictEqual(url)
43
+ })
44
+
45
+ it('should allow dot notation with namespace value', () => {
46
+ annotations.foo = gen()
47
+ annotations['foo.bar'] = gen()
48
+
49
+ expect(() => construct(annotations)).not.toThrow()
50
+ })
51
+ })
52
+
53
+ describe('validate', () => {
54
+ it('should throw if declaration is not a string or an object', () => {
55
+ // noinspection JSCheckFunctionSignatures
56
+ expect(() => construct(1)).toThrow(TypeError)
57
+ expect(() => construct(null)).toThrow(TypeError)
58
+ })
59
+
60
+ it('should throw if property is not url', () => {
61
+ annotations.foo = 'not url'
62
+
63
+ expect(() => construct(annotations)).toThrow(TypeError)
64
+ })
65
+ })
66
+ })
67
+
68
+ describe('resolve', () => {
69
+ const resolve = uris.resolve
70
+
71
+ let name
72
+ let namespace
73
+ let uri
74
+
75
+ /** @type {toa.pointer.URIs} */
76
+ let annotation
77
+
78
+ /** @type {toa.core.Locator} */
79
+ let locator
80
+
81
+ beforeEach(() => {
82
+ name = generate()
83
+ namespace = generate()
84
+ uri = gen()
85
+
86
+ annotation = {
87
+ [namespace + '.' + name]: uri
88
+ }
89
+
90
+ locator = new Locator(name, namespace)
91
+ })
92
+
93
+ it('should be', () => {
94
+ expect(uris.resolve).toBeDefined()
95
+ expect(uris.resolve).toBeInstanceOf(Function)
96
+ })
97
+
98
+ it('should resolve by id', () => {
99
+ const { url } = resolve(locator, annotation)
100
+
101
+ expect(url).toBeInstanceOf(URL)
102
+ expect(url.href).toStrictEqual(uri)
103
+ })
104
+
105
+ it('should throw if not found', () => {
106
+ const name = generate()
107
+ const namespace = generate()
108
+ const locator = new Locator(name, namespace)
109
+
110
+ expect(() => resolve(locator, annotation)).toThrow(`URI annotation for '${locator.id}' is not found`)
111
+ })
112
+
113
+ it('should resolve by namespace', () => {
114
+ annotation[namespace] = uri
115
+
116
+ const { url } = resolve(locator, annotation)
117
+
118
+ expect(url.href).toStrictEqual(uri)
119
+ })
120
+
121
+ it('should resolve default', () => {
122
+ annotation = { default: gen() }
123
+
124
+ const { url } = resolve(locator, annotation)
125
+
126
+ expect(url.href).toStrictEqual(annotation.default)
127
+ })
128
+
129
+ it('should resolve global locators', () => {
130
+ const name = generate()
131
+ const locator = new Locator(name)
132
+ const uri = gen()
133
+
134
+ annotation = { [name]: uri }
135
+
136
+ const { url } = resolve(locator, annotation)
137
+
138
+ expect(url.href).toStrictEqual(uri)
139
+ })
140
+ })
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { Locator } = require('@toa.io/core')
5
+ const { random, letters: { up } } = require('@toa.io/generic')
6
+
7
+ const { variable } = require('../')
8
+
9
+ it('should be', () => {
10
+ expect(variable).toBeInstanceOf(Function)
11
+ })
12
+
13
+ it('should create variable', () => {
14
+ const prefix = 'foo-bar'
15
+ const namespace = generate()
16
+ const component = generate()
17
+ const locator = new Locator(component, namespace)
18
+ const name = generate()
19
+
20
+ let value = generate()
21
+
22
+ const string = variable(prefix, locator, name, value)
23
+
24
+ const expected = {
25
+ name: `TOA_${up(prefix)}_${locator.uppercase}_${up(name)}`,
26
+ value
27
+ }
28
+
29
+ expect(string).toStrictEqual(expected)
30
+
31
+ value = random()
32
+ expected.value = value
33
+
34
+ const number = variable(prefix, locator, name, value)
35
+
36
+ expect(number).toStrictEqual(expected)
37
+ })
@@ -0,0 +1,22 @@
1
+ // noinspection ES6UnusedImports
2
+
3
+ import { dependency } from '@toa.io/operations/types/deployment'
4
+ import { dependencies } from '@toa.io/norm/types'
5
+ import { URIs } from './uris'
6
+
7
+ declare namespace toa.pointer {
8
+
9
+ namespace deployment {
10
+
11
+ type Options = {
12
+ prefix: string
13
+ }
14
+
15
+ }
16
+
17
+ type Deployment = (instances: dependencies.Instance[], uris: URIs, options: deployment.Options)
18
+ => dependency.Declaration
19
+
20
+ }
21
+
22
+ export const deployment: toa.pointer.Deployment
@@ -0,0 +1,6 @@
1
+ export type { Pointer } from './pointer'
2
+
3
+ export { deployment } from './deployment'
4
+ export { variable } from './variable'
5
+
6
+ export * as uris from './uris'
@@ -0,0 +1,21 @@
1
+ declare namespace toa.pointer {
2
+
3
+ type Options = {
4
+ protocol: string // for local environment
5
+ }
6
+
7
+ class Pointer {
8
+ protocol: string
9
+ host: string
10
+ port: number
11
+ hostname: string
12
+ path: string
13
+ username: string
14
+ password: string
15
+ reference: string
16
+ label: string // safe for logging
17
+ }
18
+
19
+ }
20
+
21
+ export type Pointer = toa.pointer.Pointer
@@ -0,0 +1,32 @@
1
+ import { Locator } from '@toa.io/core/types'
2
+
3
+ declare namespace toa.pointer {
4
+
5
+ namespace uris {
6
+
7
+ type Node = {
8
+ [key: string]: string | Node
9
+ }
10
+
11
+ type Constructor = (declaration: URIs | string) => URIs
12
+
13
+ type Resolution = {
14
+ url: URL
15
+ entry: string
16
+ }
17
+
18
+ type Resolver = (locator: Locator, uris: URIs) => Resolution
19
+
20
+ }
21
+
22
+ type URIs = {
23
+ default?: string
24
+ [key: string]: string | uris.Node
25
+ } | string
26
+
27
+ }
28
+
29
+ export type URIs = toa.pointer.URIs
30
+
31
+ export const construct: toa.pointer.uris.Constructor
32
+ export const resolve: toa.pointer.uris.Resolver
@@ -0,0 +1,8 @@
1
+ import { Locator } from '@toa.io/core/types'
2
+ import { dependency } from '@toa.io/operations/types/deployment'
3
+
4
+ declare namespace toa.pointer {
5
+ type Variable = (scope: string, locator: Locator, label: string, value: string | number) => dependency.Variable
6
+ }
7
+
8
+ export const variable: toa.pointer.Variable