@toa.io/extensions.configuration 0.20.0-dev.9 → 0.20.1-alpha.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 (51) hide show
  1. package/package.json +15 -9
  2. package/readme.md +71 -156
  3. package/schemas/annotation.cos.yaml +1 -0
  4. package/schemas/manifest.cos.yaml +2 -0
  5. package/source/Aspect.test.ts +15 -0
  6. package/source/Aspect.ts +23 -0
  7. package/source/Factory.ts +12 -0
  8. package/source/configuration.test.ts +89 -0
  9. package/source/configuration.ts +53 -0
  10. package/source/deployment.test.ts +21 -0
  11. package/source/deployment.ts +69 -0
  12. package/source/index.ts +3 -0
  13. package/source/manifest.test.ts +15 -0
  14. package/source/manifest.ts +15 -0
  15. package/source/schemas.ts +8 -0
  16. package/tsconfig.json +9 -0
  17. package/docs/discussion.md +0 -109
  18. package/source/.deployment/index.js +0 -7
  19. package/source/.deployment/secrets.js +0 -33
  20. package/source/.deployment/variables.js +0 -26
  21. package/source/.manifest/.normalize/verbose.js +0 -51
  22. package/source/.manifest/index.js +0 -7
  23. package/source/.manifest/normalize.js +0 -16
  24. package/source/.manifest/schema.yaml +0 -9
  25. package/source/.manifest/validate.js +0 -13
  26. package/source/.provider/env.js +0 -19
  27. package/source/.provider/form.js +0 -20
  28. package/source/.provider/index.js +0 -7
  29. package/source/annotation.fixtures.js +0 -39
  30. package/source/annotation.js +0 -36
  31. package/source/annotation.test.js +0 -43
  32. package/source/aspect.fixtures.js +0 -32
  33. package/source/aspect.js +0 -36
  34. package/source/aspect.test.js +0 -76
  35. package/source/configuration.js +0 -19
  36. package/source/deployment.fixtures.js +0 -38
  37. package/source/deployment.js +0 -20
  38. package/source/deployment.test.js +0 -70
  39. package/source/factory.js +0 -39
  40. package/source/factory.test.js +0 -22
  41. package/source/index.js +0 -13
  42. package/source/manifest.js +0 -13
  43. package/source/manifest.test.js +0 -123
  44. package/source/provider.js +0 -121
  45. package/source/provider.test.js +0 -130
  46. package/source/secrets.js +0 -28
  47. package/source/secrets.test.js +0 -47
  48. package/types/aspect.ts +0 -11
  49. package/types/factory.d.ts +0 -12
  50. package/types/provider.d.ts +0 -22
  51. /package/{docs → notes}/consistency.md +0 -0
@@ -1,38 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
-
5
- const { Locator } = require('@toa.io/core')
6
- const { random } = require('@toa.io/generic')
7
-
8
- const component = () => {
9
- const namespace = generate()
10
- const name = generate()
11
-
12
- return { locator: new Locator(name, namespace) }
13
- }
14
-
15
- /** @type {toa.norm.context.dependencies.Instance[]} */
16
- const components = []
17
- const annotations = {}
18
-
19
- const annotate = (component) => {
20
- const key = component.locator.id
21
-
22
- annotations[key] = { [generate()]: generate() }
23
- }
24
-
25
- for (let i = 0; i < random(10) + 5; i++) components.push(component())
26
- for (let i = 0; i < components.length; i++) if (i % 2 === 0) annotate(components[i])
27
-
28
- /**
29
- * @param {string} id
30
- * @returns {toa.norm.context.dependencies.Instance}
31
- */
32
- const find = (id) => {
33
- return components.find((component) => component.locator.id === id)
34
- }
35
-
36
- exports.components = components
37
- exports.annotations = annotations
38
- exports.find = find
@@ -1,20 +0,0 @@
1
- 'use strict'
2
-
3
- const { merge } = require('@toa.io/generic')
4
- const get = require('./.deployment')
5
-
6
- /**
7
- * @param {toa.norm.context.dependencies.Instance[]} components
8
- * @param {object} annotations
9
- * @return {toa.deployment.dependency.Declaration}
10
- */
11
- const deployment = (components, annotations) => {
12
- const variables = get.variables(components, annotations)
13
- const secrets = get.secrets(components, annotations)
14
-
15
- merge(variables, secrets)
16
-
17
- return { variables }
18
- }
19
-
20
- exports.deployment = deployment
@@ -1,70 +0,0 @@
1
- 'use strict'
2
-
3
- const clone = require('clone-deep')
4
- const { encode, sample } = require('@toa.io/generic')
5
-
6
- const fixtures = require('./deployment.fixtures')
7
- const { deployment } = require('../')
8
- const { generate } = require('randomstring')
9
-
10
- /** @type {toa.deployment.dependency.Declaration} */
11
- let declaration
12
-
13
- beforeAll(() => {
14
- declaration = deployment(fixtures.components, fixtures.annotations)
15
- })
16
-
17
- it('should exist', () => {
18
- expect(deployment).toBeDefined()
19
- })
20
-
21
- it('should declare variables', () => {
22
- expect(declaration.variables).toBeDefined()
23
- })
24
-
25
- it('should map configurations', () => {
26
- const keys = Object.keys(fixtures.annotations)
27
-
28
- expect(keys.length).toBeGreaterThan(0)
29
-
30
- for (const [id, annotations] of Object.entries(fixtures.annotations)) {
31
- const component = fixtures.find(id)
32
- const variables = declaration.variables[component.locator.label]
33
- const encoded = encode(annotations)
34
-
35
- expect(component).toBeDefined()
36
- expect(variables).toBeDefined()
37
- expect(variables).toBeInstanceOf(Array)
38
- expect(variables.length).toStrictEqual(1)
39
-
40
- const env = variables[0]
41
-
42
- expect(env.name).toBeDefined()
43
- expect(env.name).toStrictEqual('TOA_CONFIGURATION_' + component.locator.uppercase)
44
- expect(env.value).toBeDefined()
45
- expect(env.value).toStrictEqual(encoded)
46
- }
47
- })
48
-
49
- it('should declare secrets', async () => {
50
- const annotations = clone(fixtures.annotations)
51
- const component = sample(fixtures.components)
52
- const id = component.locator.id
53
- const key = generate()
54
- const name = generate().substring(0, 16).toUpperCase()
55
- const value = '$' + name
56
-
57
- if (annotations[id] === undefined) annotations[id] = {}
58
-
59
- annotations[id][key] = value
60
-
61
- declaration = deployment(fixtures.components, annotations)
62
-
63
- const variables = declaration.variables[component.locator.label]
64
-
65
- expect(variables).toBeDefined()
66
-
67
- const secret = variables.find((variable) => variable.name === 'TOA_CONFIGURATION__' + name)
68
-
69
- expect(secret).toBeDefined()
70
- })
package/source/factory.js DELETED
@@ -1,39 +0,0 @@
1
- 'use strict'
2
-
3
- const { Schema } = require('@toa.io/schema')
4
- const { Aspect } = require('./aspect')
5
- const { Configuration } = require('./configuration')
6
- const { Provider } = require('./provider')
7
-
8
- /**
9
- * @implements {toa.extensions.configuration.Factory}
10
- */
11
- class Factory {
12
- /**
13
- * @param {toa.core.Locator} locator
14
- * @param {toa.schema.JSON | Object} annotation
15
- * @return {toa.extensions.configuration.Aspect}
16
- */
17
- aspect (locator, annotation) {
18
- const schema = new Schema(annotation)
19
- const provider = new Provider(locator, schema)
20
- const configuration = new Configuration(provider)
21
-
22
- return new Aspect(configuration)
23
- }
24
-
25
- provider (component) {
26
- const locator = component.locator
27
- const declaration = component.extensions?.[ID]
28
-
29
- if (declaration === undefined) throw new Error(`Configuration extension not found in '${locator.id}'`)
30
-
31
- const schema = new Schema(declaration)
32
-
33
- return new Provider(locator, schema)
34
- }
35
- }
36
-
37
- const ID = require('../package.json').name
38
-
39
- exports.Factory = Factory
@@ -1,22 +0,0 @@
1
- 'use strict'
2
-
3
- const { Factory } = require('../')
4
-
5
- it('should export', () => {
6
- expect(Factory).toBeInstanceOf(Function)
7
- })
8
-
9
- /** @type {toa.extensions.configuration.Factory} */
10
- let factory
11
-
12
- beforeAll(async () => {
13
- factory = new Factory()
14
- })
15
-
16
- it('should expose context', () => {
17
- expect(factory.aspect).toBeInstanceOf(Function)
18
- })
19
-
20
- it('should expose provider', () => {
21
- expect(factory.provider).toBeInstanceOf(Function)
22
- })
package/source/index.js DELETED
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const { manifest } = require('./manifest')
4
- const { annotation } = require('./annotation')
5
- const { deployment } = require('./deployment')
6
-
7
- const { Factory } = require('./factory')
8
-
9
- exports.manifest = manifest
10
- exports.annotation = annotation
11
- exports.deployment = deployment
12
-
13
- exports.Factory = Factory
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const { normalize, validate } = require('./.manifest')
4
-
5
- const manifest = (manifest) => {
6
- const declaration = normalize(manifest)
7
-
8
- validate(declaration)
9
-
10
- return declaration
11
- }
12
-
13
- exports.manifest = manifest
@@ -1,123 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { random } = require('@toa.io/generic')
5
-
6
- const { manifest } = require('../')
7
-
8
- it('should export', () => {
9
- expect(manifest).toBeInstanceOf(Function)
10
- })
11
-
12
- describe('validation', () => {
13
- it('should throw if not an object', () => {
14
- const call = () => manifest(generate())
15
- expect(call).toThrow(/must be object/)
16
- })
17
-
18
- it('should throw if not a valid schema', () => {
19
- const object = { type: generate() }
20
- const call = () => manifest(object)
21
-
22
- expect(call).toThrow(/one of the allowed values/)
23
- })
24
-
25
- it('should throw if schema is not an object type', () => {
26
- const schema = { type: 'number' }
27
- const call = () => manifest(schema)
28
-
29
- expect(call).toThrow(/equal to constant 'object'/)
30
- })
31
- })
32
-
33
- describe('normalization', () => {
34
- it('should expand concise', () => {
35
- const concise = {
36
- foo: generate(),
37
- bar: {
38
- baz: random()
39
- }
40
- }
41
-
42
- const declaration = manifest(concise)
43
-
44
- expect(declaration).toMatchObject({
45
- type: 'object',
46
- properties: {
47
- foo: {
48
- type: 'string',
49
- default: concise.foo
50
- },
51
- bar: {
52
- type: 'object',
53
- properties: {
54
- baz: {
55
- type: 'number',
56
- default: concise.bar.baz
57
- }
58
- }
59
- }
60
- }
61
- })
62
- })
63
-
64
- it('should expand partially concise', () => {
65
- const concise = {
66
- foo: generate(),
67
- bar: {
68
- baz: random()
69
- },
70
- qux: {
71
- type: 'string',
72
- default: null
73
- }
74
- }
75
-
76
- const declaration = manifest(concise)
77
-
78
- expect(declaration).toMatchObject({
79
- type: 'object',
80
- properties: {
81
- foo: {
82
- type: 'string',
83
- default: concise.foo
84
- },
85
- bar: {
86
- type: 'object',
87
- properties: {
88
- baz: {
89
- type: 'number',
90
- default: concise.bar.baz
91
- }
92
- }
93
- },
94
- qux: {
95
- type: 'string',
96
- default: null
97
- }
98
- }
99
- })
100
- })
101
-
102
- it('should expand arrays', () => {
103
- const concise = { foo: [1, 2, 3] }
104
-
105
- const declaration = manifest(concise)
106
-
107
- expect(declaration).toMatchObject({
108
- type: 'object',
109
- properties: {
110
- foo: {
111
- type: 'array',
112
- default: [1, 2, 3]
113
- }
114
- }
115
- })
116
- })
117
-
118
- it('should throw on empty array', () => {
119
- const concise = { foo: [] }
120
-
121
- expect(() => manifest(concise)).toThrow(/array items type because it's empty/)
122
- })
123
- })
@@ -1,121 +0,0 @@
1
- 'use strict'
2
-
3
- const clone = require('clone-deep')
4
- const { decode, encode, empty, overwrite } = require('@toa.io/generic')
5
-
6
- const { Connector } = require('@toa.io/core')
7
-
8
- const { secrets } = require('./secrets')
9
- const { env, form } = require('./.provider')
10
-
11
- /**
12
- * @implements {toa.extensions.configuration.Provider}
13
- */
14
- class Provider extends Connector {
15
- /** @type {toa.schema.Schema} */
16
- #schema
17
-
18
- /** @type {Object} */
19
- #form
20
- /** @type {Object} */
21
- #value
22
-
23
- source
24
- object
25
- key
26
-
27
- /**
28
- * @param {toa.core.Locator} locator
29
- * @param {toa.schema.Schema} schema
30
- */
31
- constructor (locator, schema) {
32
- super()
33
-
34
- this.source = this.#source.bind(this)
35
-
36
- this.key = PREFIX + locator.uppercase
37
- this.#schema = schema
38
-
39
- // form is required to enable nested defaults
40
- this.#form = form(schema.schema)
41
- }
42
-
43
- async open () {
44
- this.#retrieve()
45
- }
46
-
47
- set (key, value) {
48
- const object = this.object === undefined ? {} : clone(this.object)
49
- const properties = key.split('.')
50
- const property = properties.pop()
51
-
52
- let cursor = object
53
-
54
- for (const name of properties) {
55
- if (cursor[name] === undefined) cursor[name] = {}
56
-
57
- cursor = cursor[name]
58
- }
59
-
60
- if (value === undefined) delete cursor[property]
61
- else cursor[property] = value
62
-
63
- this.#set(object)
64
- }
65
-
66
- unset (key) {
67
- this.set(key, undefined)
68
- }
69
-
70
- reset () {
71
- this.object = undefined
72
- }
73
-
74
- export () {
75
- return this.object === undefined ? undefined : encode(this.object)
76
- }
77
-
78
- #source () {
79
- return this.#value
80
- }
81
-
82
- #retrieve () {
83
- const string = process.env[this.key]
84
- const object = string === undefined ? {} : decode(string)
85
-
86
- this.#set(object)
87
- }
88
-
89
- #set (object) {
90
- object = this.#reveal(object)
91
- object = env(object)
92
-
93
- this.#merge(object)
94
-
95
- this.object = empty(object) ? undefined : object
96
- }
97
-
98
- #merge (object) {
99
- object = clone(object)
100
-
101
- const form = clone(this.#form)
102
- const value = overwrite(form, object)
103
-
104
- this.#schema.validate(value)
105
- this.#value = value
106
- }
107
-
108
- #reveal (object) {
109
- return secrets(object, (variable) => {
110
- if (!(variable in process.env)) throw new Error(`Configuration secret value ${variable} is not set`)
111
-
112
- const base64 = process.env[variable]
113
-
114
- return decode(base64)
115
- })
116
- }
117
- }
118
-
119
- const PREFIX = 'TOA_CONFIGURATION_'
120
-
121
- exports.Provider = Provider
@@ -1,130 +0,0 @@
1
- 'use strict'
2
-
3
- /* eslint-disable no-template-curly-in-string */
4
-
5
- const { generate } = require('randomstring')
6
- const { encode } = require('@toa.io/generic')
7
-
8
- const { Provider } = require('./provider')
9
-
10
- it('should be', async () => {
11
- expect(Provider).toBeInstanceOf(Function)
12
- })
13
-
14
- const locator = /** @type {toa.core.Locator} */ { uppercase: generate().toUpperCase() }
15
- const schema = /** @type {toa.schema.Schema} */ { validate: () => undefined }
16
-
17
- /** @type {Provider} */
18
- let provider
19
-
20
- beforeEach(() => {
21
- cleanEnv()
22
- provider = new Provider(locator, schema)
23
- })
24
-
25
- it('should replace secret values', async () => {
26
- const configuration = { foo: '$FOO_SECRET' }
27
- const secrets = { FOO_SECRET: generate() }
28
-
29
- setEnv(configuration, secrets)
30
-
31
- await provider.open()
32
- const value = provider.source()
33
-
34
- expect(value).toStrictEqual({ foo: secrets.FOO_SECRET })
35
- })
36
-
37
- it('should throw if secret value is not set', async () => {
38
- const configuration = { foo: '$FOO_SECRET' }
39
-
40
- setEnv(configuration)
41
-
42
- await expect(provider.open()).rejects.toThrow('FOO_SECRET is not set')
43
- })
44
-
45
- it('should replace nested secrets', async () => {
46
- const configuration = { foo: { bar: '$BAR' } }
47
- const secrets = { BAR: generate() }
48
-
49
- setEnv(configuration, secrets)
50
-
51
- await provider.open()
52
- const value = provider.source()
53
-
54
- expect(value).toStrictEqual({ foo: { bar: secrets.BAR } })
55
- })
56
-
57
- it('should replace placeholders', async () => {
58
- const name = 'FOO_VALUE'
59
- const configuration = { foo: { bar: 'foo_${' + name + '}' } }
60
- const value = generate()
61
-
62
- setEnv(configuration)
63
- setVal(name, value)
64
-
65
- await provider.open()
66
- const source = provider.source()
67
-
68
- expect(source).toStrictEqual({ foo: { bar: 'foo_' + value } })
69
- })
70
-
71
- it('should replace multiple placeholders', async () => {
72
- const configuration = { foo: '${FOO} ${BAR}' }
73
-
74
- setEnv(configuration)
75
- setVal('FOO', 'hello')
76
- setVal('BAR', 'world')
77
-
78
- await provider.open()
79
- const source = provider.source()
80
-
81
- expect(source).toStrictEqual({ foo: 'hello world' })
82
- })
83
-
84
- it('should not replace if variable not set', async () => {
85
- const configuration = { foo: '${FOO}' }
86
-
87
- setEnv(configuration)
88
-
89
- await provider.open()
90
- const value = provider.source()
91
-
92
- expect(value).toStrictEqual(configuration)
93
- })
94
-
95
- const usedVariables = []
96
-
97
- /**
98
- * @param {object} configuration
99
- * @param {Record<string, string>} [secrets]
100
- */
101
- function setEnv (configuration, secrets) {
102
- const variable = PREFIX + locator.uppercase
103
- const encoded = encode(configuration)
104
-
105
- setVal(variable, encoded)
106
-
107
- if (secrets !== undefined) {
108
- for (const [key, value] of Object.entries(secrets)) {
109
- const variable = PREFIX + '_' + key
110
-
111
- process.env[variable] = encode(value)
112
- usedVariables.push(variable)
113
- }
114
- }
115
- }
116
-
117
- function setVal (variable, value) {
118
- process.env[variable] = value
119
- usedVariables.push(variable)
120
- }
121
-
122
- function cleanEnv () {
123
- for (const variable of usedVariables) {
124
- delete process.env[variable]
125
- }
126
-
127
- usedVariables.length = 0
128
- }
129
-
130
- const PREFIX = 'TOA_CONFIGURATION_'
package/source/secrets.js DELETED
@@ -1,28 +0,0 @@
1
- 'use strict'
2
-
3
- const { map } = require('@toa.io/generic')
4
-
5
- /**
6
- * @param {object} configuration
7
- * @param {(variable: string, name?: string) => void} callback
8
- * @returns {object}
9
- */
10
- function secrets (configuration, callback) {
11
- return map(configuration, (value) => {
12
- if (typeof value !== 'string') return
13
-
14
- const match = value.match(SECRET_RX)
15
-
16
- if (match === null) return
17
-
18
- const name = match.groups.variable
19
- const variable = PREFIX + name
20
-
21
- return callback(variable, name)
22
- })
23
- }
24
-
25
- const PREFIX = 'TOA_CONFIGURATION__'
26
- const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
27
-
28
- exports.secrets = secrets
@@ -1,47 +0,0 @@
1
- 'use strict'
2
-
3
- const { secrets } = require('./secrets')
4
-
5
- it('should be', async () => {
6
- expect(secrets).toBeInstanceOf(Function)
7
- })
8
-
9
- it('should find secrets', async () => {
10
- const configuration = {
11
- foo: {
12
- bar: '$BAR_VALUE'
13
- },
14
- baz: '$BAZ_VALUE'
15
- }
16
-
17
- const variables = new Set()
18
- const names = new Set()
19
-
20
- secrets(configuration, (variable, name) => {
21
- variables.add(variable)
22
- names.add(name)
23
- })
24
-
25
- expect(variables.has('TOA_CONFIGURATION__BAR_VALUE')).toStrictEqual(true)
26
- expect(variables.has('TOA_CONFIGURATION__BAZ_VALUE')).toStrictEqual(true)
27
-
28
- expect(names.has('BAR_VALUE')).toStrictEqual(true)
29
- expect(names.has('BAZ_VALUE')).toStrictEqual(true)
30
- })
31
-
32
- it('should replace values', async () => {
33
- const configuration = { foo: '$FOO' }
34
-
35
- const output = secrets(configuration, (variable) => 'hello')
36
-
37
- expect(output).toStrictEqual({ foo: 'hello' })
38
- })
39
-
40
- it('should allow numbers in secret names', async () => {
41
- const configuration = { foo: '$HOST_0' }
42
- const found = new Set()
43
-
44
- secrets(configuration, (variable) => found.add(variable))
45
-
46
- expect(found.has('TOA_CONFIGURATION__HOST_0')).toStrictEqual(true)
47
- })
package/types/aspect.ts DELETED
@@ -1,11 +0,0 @@
1
- // noinspection ES6UnusedImports
2
-
3
- import * as core from '@toa.io/core/types/extensions'
4
-
5
- declare namespace toa.extensions.configuration {
6
-
7
- interface Aspect extends core.Aspect {
8
- invoke(path?: string[]): any
9
- }
10
-
11
- }
@@ -1,12 +0,0 @@
1
- import { Component } from '@toa.io/norm/types'
2
-
3
- import * as _extensions from '@toa.io/core/types/extensions'
4
- import * as _provider from './provider'
5
-
6
- declare namespace toa.extensions.configuration {
7
-
8
- interface Factory extends _extensions.Factory {
9
- provider(component: Component): _provider.Provider
10
- }
11
-
12
- }