@toa.io/extensions.configuration 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/docs/consistency.md +69 -0
- package/docs/discussion.md +109 -0
- package/package.json +25 -0
- package/readme.md +231 -0
- package/src/.manifest/.normalize/verbose.js +48 -0
- package/src/.manifest/index.js +7 -0
- package/src/.manifest/normalize.js +16 -0
- package/src/.manifest/schema.yaml +8 -0
- package/src/.manifest/validate.js +13 -0
- package/src/.provider/form.js +20 -0
- package/src/annotation.js +36 -0
- package/src/aspect.js +36 -0
- package/src/configuration.js +19 -0
- package/src/deployment.js +23 -0
- package/src/factory.js +39 -0
- package/src/index.js +13 -0
- package/src/manifest.js +13 -0
- package/src/provider.js +114 -0
- package/test/annotations.fixtures.js +38 -0
- package/test/annotations.test.js +43 -0
- package/test/aspect.fixtures.js +33 -0
- package/test/aspect.test.js +77 -0
- package/test/deployment.fixtures.js +38 -0
- package/test/deployment.test.js +45 -0
- package/test/factory.test.js +22 -0
- package/test/manifest.test.js +132 -0
- package/types/aspect.ts +11 -0
- package/types/factory.d.ts +12 -0
- package/types/provider.d.ts +22 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Configuration Consistency
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
## Problem Definition
|
|
6
|
+
|
|
7
|
+
At arbitrary moments when the configuration has changed, some distributed operations have started
|
|
8
|
+
but haven't yet
|
|
9
|
+
finished. This leads to operations that start with certain configuration values but will finish with
|
|
10
|
+
another, which may
|
|
11
|
+
result in logical problems.
|
|
12
|
+
|
|
13
|
+
Given that distributed operations may have arbitrary participants and last for an arbitrary period,
|
|
14
|
+
there is no such
|
|
15
|
+
single moment when configuration can be safely changed. Thus, it must be a process.
|
|
16
|
+
|
|
17
|
+
## ~~Not a~~ Solution
|
|
18
|
+
|
|
19
|
+
1. Configuration is a single versioned object.
|
|
20
|
+
2. Configuration version value is included in the Uniform Communication Interface.
|
|
21
|
+
3. The first distributed operation participant (which is the one who has received a UCI without the
|
|
22
|
+
configuration
|
|
23
|
+
version value) must include in the outgoing UCI the configuration version value that it considers
|
|
24
|
+
as current.
|
|
25
|
+
4. Non-first distributed operations participants (which is those who has received a UCI with the
|
|
26
|
+
configuration version
|
|
27
|
+
value) must use the configuration version specified in the UCI.
|
|
28
|
+
5. Non-first distributed operation participants must include in the outgoing UCI the same
|
|
29
|
+
configuration version value as
|
|
30
|
+
they received in the incoming.
|
|
31
|
+
|
|
32
|
+
## Restrictions
|
|
33
|
+
|
|
34
|
+
1. Configuration updates are always backward compatible.
|
|
35
|
+
2. Configuration updates are always being delivered before algorithm updates.
|
|
36
|
+
1. Including federated deployment, that is: first deliver configuration to all facilities (data
|
|
37
|
+
centers, zones,
|
|
38
|
+
whatever), then deliver algorithm updates.
|
|
39
|
+
3. Configuration storage and access solution must provide the transactional updates, that is if any
|
|
40
|
+
arbitrary
|
|
41
|
+
participant observed a certain configuration version, then any other participant is guaranteed to
|
|
42
|
+
be able to
|
|
43
|
+
subsequently observe that version.
|
|
44
|
+
1. Including federated deployment[^1].
|
|
45
|
+
|
|
46
|
+
## Solution
|
|
47
|
+
|
|
48
|
+
The configuration compatibility problem described above is a particular case of a common
|
|
49
|
+
compatibility problem for
|
|
50
|
+
distributed operation participants. It is not the only configuration that may change while a
|
|
51
|
+
distributed operation is
|
|
52
|
+
running, but participant algorithms themselves. This leads to the conclusion that this particular
|
|
53
|
+
solution must be
|
|
54
|
+
propagated to algorithm versions, that is: which system (as a set of algorithms and configuration)
|
|
55
|
+
version was used to
|
|
56
|
+
start the operation, and that system version must be used to finish the operation.
|
|
57
|
+
|
|
58
|
+
This kind of solution results in the need to run all versions of the system with appropriate message
|
|
59
|
+
routing. The
|
|
60
|
+
implementation complexity of this solution and its operations costs are considered unreasonable.
|
|
61
|
+
However, an attempt
|
|
62
|
+
will be made to implement this solution with a certain
|
|
63
|
+
constraints [#147](https://github.com/toa-io/toa/issues/147).
|
|
64
|
+
|
|
65
|
+
[^1]: Since it looks like there is no reasonable way to provide this kind of guarantee without
|
|
66
|
+
significant performance
|
|
67
|
+
and/or availability impact, it may be implemented as a mechanism with read retries and with a
|
|
68
|
+
timeout considered as
|
|
69
|
+
“enough for the most of fail-over scenarios”.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Discussion
|
|
2
|
+
|
|
3
|
+
## Change Requests
|
|
4
|
+
|
|
5
|
+
- [x] feat(configuration): add configuration extension
|
|
6
|
+
- manifest (schema) validation
|
|
7
|
+
- context extension
|
|
8
|
+
- [x] feat(formation): add well-known extension 'configuration'
|
|
9
|
+
- component
|
|
10
|
+
- context
|
|
11
|
+
- [x] feat(node): add well-known context extension 'configuration'
|
|
12
|
+
- [x] feat(configuration): add concise declarations
|
|
13
|
+
- [x] feat(configuration): add runtime configuration resolution
|
|
14
|
+
- [x] feat(cli): add `toa configure <key> <value> --reset`
|
|
15
|
+
- validate type
|
|
16
|
+
- [x] feat(operations): add configuration deployment
|
|
17
|
+
- annotations (values) validation
|
|
18
|
+
- [ ] feat(configuration): add secrets resolution
|
|
19
|
+
- [ ] feat(operations): add secrets deployment
|
|
20
|
+
- [ ] feat(cli): add `toa conceal`
|
|
21
|
+
- validate type
|
|
22
|
+
- [ ] feat(cli): add `toa configure`
|
|
23
|
+
- prompt required values
|
|
24
|
+
- use JSON Schema title
|
|
25
|
+
|
|
26
|
+
## Statements
|
|
27
|
+
|
|
28
|
+
### Common
|
|
29
|
+
|
|
30
|
+
- Secrets are being deployed separately by `toa conceal` command
|
|
31
|
+
|
|
32
|
+
### 1: Environment variables
|
|
33
|
+
|
|
34
|
+
- Configuration values and secrets are mapped as environment variables to composition deployments
|
|
35
|
+
- Extensions may expose *deployment mutators*, which are able to modify deployment declaration
|
|
36
|
+
- Configuration context extension reads environment variables to resolve configuration and secrets
|
|
37
|
+
|
|
38
|
+
### 2: Dedicated Components
|
|
39
|
+
|
|
40
|
+
- Hot updates
|
|
41
|
+
- [Configuration consistency](consistency.md)
|
|
42
|
+
|
|
43
|
+
## Questions
|
|
44
|
+
|
|
45
|
+
### Where are values comes from?
|
|
46
|
+
|
|
47
|
+
Environment variables.
|
|
48
|
+
|
|
49
|
+
### Is there a configuration service or configuration component?
|
|
50
|
+
|
|
51
|
+
No. It will be implemented later as a part of [consistent configuration](consistency.md).
|
|
52
|
+
|
|
53
|
+
### How are configuration values being stored?
|
|
54
|
+
|
|
55
|
+
As a kubernetes secrets mapped as environment variables.
|
|
56
|
+
|
|
57
|
+
### Where are secrets being stored and how do they resolve to value?
|
|
58
|
+
|
|
59
|
+
As a kubernetes secrets mapped as environment variables.
|
|
60
|
+
|
|
61
|
+
### Is configuration a single environment variable or a set (one per component)?
|
|
62
|
+
|
|
63
|
+
#### Context Configuration
|
|
64
|
+
|
|
65
|
+
In later versions, context extension will resolve configuration values by component locator. Given
|
|
66
|
+
that it is yet
|
|
67
|
+
unknown when this will happen, a certain context might have appeared which configuration is big
|
|
68
|
+
enough to not fit the
|
|
69
|
+
environment variable limitations.
|
|
70
|
+
|
|
71
|
+
That is, Context Configuration must be mapped as a set of environment variables (one per component).
|
|
72
|
+
Values are
|
|
73
|
+
serialized Configuration Objects.
|
|
74
|
+
|
|
75
|
+
> This will also allow to configure local environment per component.
|
|
76
|
+
|
|
77
|
+
#### Secrets
|
|
78
|
+
|
|
79
|
+
Secrets are mapped per secret as they are not bound to components.
|
|
80
|
+
|
|
81
|
+
### Is configuration a single kubernetes secret or a set (one per component)?
|
|
82
|
+
|
|
83
|
+
#### Configuration
|
|
84
|
+
|
|
85
|
+
Single secret with a set of values per component.
|
|
86
|
+
|
|
87
|
+
#### Secrets
|
|
88
|
+
|
|
89
|
+
Once kubernetes secret per configuration secret.
|
|
90
|
+
|
|
91
|
+
### Is there an option to configure local environment?
|
|
92
|
+
|
|
93
|
+
<dl>
|
|
94
|
+
<dt><code>toa configure <component></code></dt>
|
|
95
|
+
<dd>Create local environment configuration values</dd>
|
|
96
|
+
</dl>
|
|
97
|
+
|
|
98
|
+
### Whose responsibility is to call annotations?
|
|
99
|
+
|
|
100
|
+
- norm
|
|
101
|
+
- deployment
|
|
102
|
+
|
|
103
|
+
`toa export context` should throw errors if context has invalid annotations, and it's not a part of
|
|
104
|
+
the deployment.
|
|
105
|
+
|
|
106
|
+
## References
|
|
107
|
+
|
|
108
|
+
- [#125](https://github.com/toa-io/toa/issues/125)
|
|
109
|
+
- [#132](https://github.com/toa-io/toa/issues/132)
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toa.io/extensions.configuration",
|
|
3
|
+
"version": "0.2.1-dev.3",
|
|
4
|
+
"description": "Toa Configuration",
|
|
5
|
+
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
|
+
"homepage": "https://github.com/toa-io/toa#readme",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/toa-io/toa.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/toa-io/toa/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@toa.io/core": "*",
|
|
20
|
+
"@toa.io/generic": "*",
|
|
21
|
+
"@toa.io/schema": "*",
|
|
22
|
+
"@toa.io/yaml": "*",
|
|
23
|
+
"clone-deep": "4.0.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Toa Configuration Extension
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
### Define
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
# component.toa.yaml
|
|
9
|
+
name: dummy
|
|
10
|
+
namespace: dummies
|
|
11
|
+
|
|
12
|
+
configuration:
|
|
13
|
+
foo: bar
|
|
14
|
+
baz: 1
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Use
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
function transition (input, entity, context) {
|
|
21
|
+
const { foo, baz } = context.configuration
|
|
22
|
+
|
|
23
|
+
// ...
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Override
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
# context.toa.yaml
|
|
31
|
+
configuration:
|
|
32
|
+
dummies.dummy:
|
|
33
|
+
foo: qux
|
|
34
|
+
foo@staging: quux # use deployment environment discriminator
|
|
35
|
+
baz: $BAZ_VALUE # use secrets
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Deploy secrets
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
$ toa conceal
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Problem Definition
|
|
47
|
+
|
|
48
|
+
- Components must be reusable in different contexts and deployment environments,
|
|
49
|
+
that is in different configurations.
|
|
50
|
+
- Some algorithm parameters must be deployed secretly.
|
|
51
|
+
|
|
52
|
+
## Definitions
|
|
53
|
+
|
|
54
|
+
### Configuration (Distributed System Configuration)
|
|
55
|
+
|
|
56
|
+
Set of static[^1] parameters for all algorithms within a given system.
|
|
57
|
+
|
|
58
|
+
### Configuration Schema
|
|
59
|
+
|
|
60
|
+
Schema defining component's algorithms parameters (optionally with default
|
|
61
|
+
values).
|
|
62
|
+
|
|
63
|
+
### Configuration Object
|
|
64
|
+
|
|
65
|
+
Value valid against Configuration Schema.
|
|
66
|
+
|
|
67
|
+
### Configuration Value
|
|
68
|
+
|
|
69
|
+
Merge result of Configuration Schema's defaults and Configuration Object.
|
|
70
|
+
|
|
71
|
+
### Context Configuration
|
|
72
|
+
|
|
73
|
+
Map of Configuration Objects for components added to a given context.
|
|
74
|
+
|
|
75
|
+
## Responsibility Segregation
|
|
76
|
+
|
|
77
|
+
Configuration Schema is a *form* of configuration defined by component. Specific *values* for
|
|
78
|
+
specific contexts and deployment environments are defined by Context Configuration according to the
|
|
79
|
+
Schema.
|
|
80
|
+
|
|
81
|
+
See [Reusable Components](#).
|
|
82
|
+
|
|
83
|
+
## Configuration Schema
|
|
84
|
+
|
|
85
|
+
Configuration Schema is declared as a component extension
|
|
86
|
+
using [JSON Schema](https://json-schema.org) `object` type.
|
|
87
|
+
|
|
88
|
+
> <br/>
|
|
89
|
+
> By introducing non-backward compatible changes to a Configuration Schema the compatibility
|
|
90
|
+
> with existent contexts and deployment environments will be broken. That is, Configuration
|
|
91
|
+
> Schema changes are subjects of component versioning.
|
|
92
|
+
|
|
93
|
+
> <br/>
|
|
94
|
+
> Having default values for all required parameters will allow components to be runnable
|
|
95
|
+
> without configuration (i.e. on local environment).
|
|
96
|
+
|
|
97
|
+
### Example
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
# component.toa.yaml
|
|
101
|
+
name: dummy
|
|
102
|
+
namespace: dummies
|
|
103
|
+
|
|
104
|
+
extensions:
|
|
105
|
+
@toa.io/extensions.configuration:
|
|
106
|
+
properties:
|
|
107
|
+
foo:
|
|
108
|
+
type: string
|
|
109
|
+
default: 'baz'
|
|
110
|
+
bar:
|
|
111
|
+
type: number
|
|
112
|
+
required: [foo]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Concise Declaration
|
|
116
|
+
|
|
117
|
+
As it is known that Configuration Schema is declared with a JSON Schema `object` type, any
|
|
118
|
+
configuration declaration without defined `properties` considered as concise. Properties of concise
|
|
119
|
+
declaration are treated as required Configuration Schema properties with the same type as its value
|
|
120
|
+
type and no additional properties allowed.
|
|
121
|
+
|
|
122
|
+
Also note that a well-known shortcut `configuration` is available.
|
|
123
|
+
|
|
124
|
+
Next two declarations are equivalent.
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
# component.toa.yaml
|
|
128
|
+
configuration:
|
|
129
|
+
foo: baz
|
|
130
|
+
bar: 1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
# component.toa.yaml
|
|
135
|
+
extensions:
|
|
136
|
+
@toa.io/extensions.configuration:
|
|
137
|
+
properties:
|
|
138
|
+
foo:
|
|
139
|
+
type: string
|
|
140
|
+
default: baz
|
|
141
|
+
bar:
|
|
142
|
+
type: number
|
|
143
|
+
default: 1
|
|
144
|
+
additionalProperties: false
|
|
145
|
+
required: [foo, bar]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Context Configuration
|
|
149
|
+
|
|
150
|
+
Context Configuration is declared as a context annotaion. Its keys must be
|
|
151
|
+
component identifiers and its values must be Configuration Objects for those
|
|
152
|
+
components.
|
|
153
|
+
|
|
154
|
+
Context Configuration keys and Configuration Object keys may be defined
|
|
155
|
+
with [deployment environment discriminators](#).
|
|
156
|
+
|
|
157
|
+
### Example
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
# context.toa.yaml
|
|
161
|
+
configuration:
|
|
162
|
+
dummies.dummy:
|
|
163
|
+
foo: quu
|
|
164
|
+
bar: 1
|
|
165
|
+
bar@staging: 2
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Local environment
|
|
169
|
+
|
|
170
|
+
Configuration Objects for local environment may be created
|
|
171
|
+
by [`toa configure`](../../runtime/cli/readme.md#configure) command.
|
|
172
|
+
|
|
173
|
+
## Configuration Secrets
|
|
174
|
+
|
|
175
|
+
Context Configuration values which are uppercase strings prefixed with `$`
|
|
176
|
+
considered as Secrets.
|
|
177
|
+
|
|
178
|
+
### Example
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
# context.toa.yaml
|
|
182
|
+
configuration:
|
|
183
|
+
payments.gateway:
|
|
184
|
+
api-key: $STRIPE_API_KEY
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Secrets Deployment
|
|
188
|
+
|
|
189
|
+
Secrets are not being deployed with context
|
|
190
|
+
deployment ([`toa deploy`](../../runtime/cli/readme.md#deploy)),
|
|
191
|
+
thus must be deployed separately at least once for each deployment environment
|
|
192
|
+
manually ([`toa conceal`](../../runtime/cli/readme.md#conceal)).
|
|
193
|
+
|
|
194
|
+
## Operation Context
|
|
195
|
+
|
|
196
|
+
Configuration Value is available as a well-known operation context extension `configuration`.
|
|
197
|
+
|
|
198
|
+
### Usage: node
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
function transition (input, entity, context) {
|
|
202
|
+
const foo = context.configiuration.foo
|
|
203
|
+
|
|
204
|
+
// ...
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> <br/>
|
|
209
|
+
> It is strongly **not** recommended to store a copy of value type configuration
|
|
210
|
+
> values outside of operation scope, thus it prevents operation to benefit
|
|
211
|
+
> from [hot updates](#).
|
|
212
|
+
>
|
|
213
|
+
> ```javascript
|
|
214
|
+
> // THIS IS WEIRD, BAD AND NOT RECOMMENDED
|
|
215
|
+
> let foo
|
|
216
|
+
>
|
|
217
|
+
> function transition (input, entity, context) {
|
|
218
|
+
> if (foo === undefined) foo = context.configuration.foo
|
|
219
|
+
>
|
|
220
|
+
> // ...
|
|
221
|
+
> }
|
|
222
|
+
> ```
|
|
223
|
+
> See [Genuine operations](#).
|
|
224
|
+
|
|
225
|
+
## Appendix
|
|
226
|
+
|
|
227
|
+
- [Discussion](./docs/discussion.md)
|
|
228
|
+
- [Configuration consistency](./docs/consistency.md)
|
|
229
|
+
|
|
230
|
+
[^1]: Cannot be changed without a deployment. New values are considered to be a subject of
|
|
231
|
+
testing. [#146](https://github.com/toa-io/toa/issues/146)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { remap } = require('@toa.io/generic')
|
|
4
|
+
|
|
5
|
+
const verbose = (node) => {
|
|
6
|
+
const isObject = node.properties !== undefined
|
|
7
|
+
const isValue = node.type !== undefined && node.type !== 'object'
|
|
8
|
+
const isProperties = node[SYM] === 1
|
|
9
|
+
|
|
10
|
+
if (!isObject && !isValue && !isProperties) return convert(node)
|
|
11
|
+
|
|
12
|
+
return node
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const convert = (node) => {
|
|
16
|
+
const properties = remap(node, property)
|
|
17
|
+
|
|
18
|
+
properties[SYM] = 1
|
|
19
|
+
|
|
20
|
+
return { type: 'object', properties, additionalProperties: false }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function property (node) {
|
|
24
|
+
if (node === null) throw new Error('Configuration: cannot resolve type of null, use JSONSchema declaration.')
|
|
25
|
+
|
|
26
|
+
const type = Array.isArray(node) ? 'array' : typeof node
|
|
27
|
+
|
|
28
|
+
if (type === 'object') return node
|
|
29
|
+
if (type === 'array') return array(node)
|
|
30
|
+
|
|
31
|
+
return { type, default: node }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const array = (array) => {
|
|
35
|
+
if (array.length === 0) throw new Error('Configuration: cannot resolve concise array items type because it\'s empty')
|
|
36
|
+
|
|
37
|
+
const type = typeof array[0]
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: { type },
|
|
42
|
+
default: array
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SYM = Symbol()
|
|
47
|
+
|
|
48
|
+
exports.verbose = verbose
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { traverse } = require('@toa.io/generic')
|
|
4
|
+
const { verbose } = require('./.normalize/verbose')
|
|
5
|
+
|
|
6
|
+
const normalize = (manifest) => {
|
|
7
|
+
if (manifest.properties === undefined) manifest = expand(manifest)
|
|
8
|
+
|
|
9
|
+
return manifest
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const expand = (concise) => {
|
|
13
|
+
return traverse(concise, verbose)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
exports.normalize = normalize
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { traverse } = require('@toa.io/generic')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {toa.schema.JSON | Object} schema
|
|
7
|
+
* @return {Object}
|
|
8
|
+
*/
|
|
9
|
+
const form = (schema) => {
|
|
10
|
+
const defaults = (node) => {
|
|
11
|
+
if (node.properties !== undefined) return { ...node.properties }
|
|
12
|
+
if (node.default !== undefined) return node.default
|
|
13
|
+
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return traverse(schema, defaults)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exports.form = form
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Schema } = require('@toa.io/schema')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {Object} annotation
|
|
7
|
+
* @param {toa.norm.context.dependencies.Instance[]} instances
|
|
8
|
+
*/
|
|
9
|
+
const annotation = (annotation, instances) => {
|
|
10
|
+
const keys = Object.keys(annotation)
|
|
11
|
+
|
|
12
|
+
check(keys, instances)
|
|
13
|
+
|
|
14
|
+
for (const instance of instances) {
|
|
15
|
+
const object = annotation[instance.locator.id] ?? {}
|
|
16
|
+
const schema = new Schema(instance.manifest)
|
|
17
|
+
|
|
18
|
+
schema.validate(object)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return annotation
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string[]} keys
|
|
26
|
+
* @param {toa.norm.context.dependencies.Instance[]} instances
|
|
27
|
+
*/
|
|
28
|
+
const check = (keys, instances) => {
|
|
29
|
+
const ids = instances.map((instance) => instance.locator.id)
|
|
30
|
+
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
if (!ids.includes(key)) throw new Error(`Configuration Schema '${key}' is not defined`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exports.annotation = annotation
|
package/src/aspect.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Connector } = require('@toa.io/core')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @implements {toa.extensions.configuration.Aspect}
|
|
7
|
+
*/
|
|
8
|
+
class Aspect extends Connector {
|
|
9
|
+
/** @readonly */
|
|
10
|
+
name = 'configuration'
|
|
11
|
+
|
|
12
|
+
/** @type {toa.core.Reflection} */
|
|
13
|
+
#refection
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {toa.core.Reflection} reflection
|
|
17
|
+
*/
|
|
18
|
+
constructor (reflection) {
|
|
19
|
+
super()
|
|
20
|
+
|
|
21
|
+
this.#refection = reflection
|
|
22
|
+
|
|
23
|
+
this.depends(reflection)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
invoke (path) {
|
|
27
|
+
/** @type {any} */
|
|
28
|
+
let cursor = this.#refection.value
|
|
29
|
+
|
|
30
|
+
if (path !== undefined) for (const segment of path) cursor = cursor[segment]
|
|
31
|
+
|
|
32
|
+
return cursor
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exports.Aspect = Aspect
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Reflection } = require('@toa.io/core')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @implements {toa.core.Reflection}
|
|
7
|
+
*/
|
|
8
|
+
class Configuration extends Reflection {
|
|
9
|
+
/**
|
|
10
|
+
* @param {toa.extensions.configuration.Provider} provider
|
|
11
|
+
*/
|
|
12
|
+
constructor (provider) {
|
|
13
|
+
super(provider.source)
|
|
14
|
+
|
|
15
|
+
this.depends(provider)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.Configuration = Configuration
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { encode } = require('@toa.io/generic')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @type {toa.deployment.dependency.Constructor}
|
|
7
|
+
*/
|
|
8
|
+
const deployment = (components, annotations) => {
|
|
9
|
+
const variables = {}
|
|
10
|
+
|
|
11
|
+
for (const [id, annotation] of Object.entries(annotations)) {
|
|
12
|
+
const component = components.find((component) => component.locator.id === id)
|
|
13
|
+
|
|
14
|
+
variables[component.locator.label] = [{
|
|
15
|
+
name: 'TOA_CONFIGURATION_' + component.locator.uppercase,
|
|
16
|
+
value: encode(annotation)
|
|
17
|
+
}]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { variables }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
exports.deployment = deployment
|
package/src/factory.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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} declaration
|
|
15
|
+
* @return {toa.extensions.configuration.Aspect}
|
|
16
|
+
*/
|
|
17
|
+
aspect (locator, declaration) {
|
|
18
|
+
const schema = new Schema(declaration)
|
|
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
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
|
package/src/manifest.js
ADDED
package/src/provider.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
const { form } = require('./.provider/form')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @implements {toa.extensions.configuration.Provider}
|
|
11
|
+
*/
|
|
12
|
+
class Provider extends Connector {
|
|
13
|
+
/** @type {toa.schema.Schema} */
|
|
14
|
+
#schema
|
|
15
|
+
|
|
16
|
+
/** @type {Object} */
|
|
17
|
+
#form
|
|
18
|
+
/** @type {Object} */
|
|
19
|
+
#value
|
|
20
|
+
|
|
21
|
+
source
|
|
22
|
+
object
|
|
23
|
+
key
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {toa.core.Locator} locator
|
|
27
|
+
* @param {toa.schema.Schema} schema
|
|
28
|
+
*/
|
|
29
|
+
constructor (locator, schema) {
|
|
30
|
+
super()
|
|
31
|
+
|
|
32
|
+
this.source = this.#source.bind(this)
|
|
33
|
+
|
|
34
|
+
this.key = PREFIX + locator.uppercase
|
|
35
|
+
this.#schema = schema
|
|
36
|
+
|
|
37
|
+
// form is required to enable nested defaults
|
|
38
|
+
this.#form = form(schema.schema)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async connection () {
|
|
42
|
+
await this.#retrieve()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #source () {
|
|
46
|
+
return this.#value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set (key, value) {
|
|
50
|
+
const object = this.object === undefined ? {} : clone(this.object)
|
|
51
|
+
const properties = key.split('.')
|
|
52
|
+
const property = properties.pop()
|
|
53
|
+
|
|
54
|
+
let cursor = object
|
|
55
|
+
|
|
56
|
+
for (const name of properties) {
|
|
57
|
+
if (cursor[name] === undefined) cursor[name] = {}
|
|
58
|
+
|
|
59
|
+
cursor = cursor[name]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (value === undefined) delete cursor[property]
|
|
63
|
+
else cursor[property] = value
|
|
64
|
+
|
|
65
|
+
this.#set(object)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
unset (key) {
|
|
69
|
+
this.set(key, undefined)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reset () {
|
|
73
|
+
this.object = undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export () {
|
|
77
|
+
return this.object === undefined ? undefined : encode(this.object)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async #retrieve () {
|
|
81
|
+
const string = process.env[this.key]
|
|
82
|
+
const object = string === undefined ? {} : decode(string)
|
|
83
|
+
|
|
84
|
+
this.#set(object)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#set (object) {
|
|
88
|
+
this.#validate(object)
|
|
89
|
+
this.#merge(object)
|
|
90
|
+
|
|
91
|
+
this.object = empty(object) ? undefined : object
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#validate (object) {
|
|
95
|
+
const error = this.#schema.match(object)
|
|
96
|
+
|
|
97
|
+
if (error !== null) throw new TypeError(error.message)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#merge (object) {
|
|
101
|
+
object = clone(object)
|
|
102
|
+
|
|
103
|
+
const form = clone(this.#form)
|
|
104
|
+
const value = overwrite(form, object)
|
|
105
|
+
|
|
106
|
+
this.#schema.validate(value)
|
|
107
|
+
|
|
108
|
+
this.#value = value
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const PREFIX = 'TOA_CONFIGURATION_'
|
|
113
|
+
|
|
114
|
+
exports.Provider = Provider
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { Locator } = require('@toa.io/core')
|
|
5
|
+
const { random } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
const instance = () => {
|
|
8
|
+
const name = generate()
|
|
9
|
+
const namespace = generate()
|
|
10
|
+
const locator = new Locator(name, namespace)
|
|
11
|
+
|
|
12
|
+
const manifest = {
|
|
13
|
+
properties: {
|
|
14
|
+
foo: {
|
|
15
|
+
type: 'number',
|
|
16
|
+
default: 1
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { locator, manifest }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @type {toa.norm.context.dependencies.Instance[]} */
|
|
25
|
+
const instances = []
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < random(5) + 5; i++) instances.push(instance())
|
|
28
|
+
|
|
29
|
+
/** @type {Object} */
|
|
30
|
+
const annotation = {}
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < random(instances.length - 1) + 1; i++) {
|
|
33
|
+
const instance = instances[i]
|
|
34
|
+
annotation[instance.locator.id] = { foo: random() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
exports.instances = instances
|
|
38
|
+
exports.annotation = annotation
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const clone = require('clone-deep')
|
|
4
|
+
const { generate } = require('randomstring')
|
|
5
|
+
const { sample } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
const fixtures = require('./annotations.fixtures')
|
|
8
|
+
const { annotation } = require('../')
|
|
9
|
+
|
|
10
|
+
let input
|
|
11
|
+
/** @type {toa.norm.context.dependencies.Instance[]} */ let instances
|
|
12
|
+
|
|
13
|
+
const call = () => annotation(input, instances)
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
input = clone(fixtures.annotation)
|
|
17
|
+
instances = clone(fixtures.instances)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('sample must be valid', () => {
|
|
21
|
+
expect(call).not.toThrow()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should must be a function', () => {
|
|
25
|
+
expect(annotation).toBeDefined()
|
|
26
|
+
expect(annotation).toBeInstanceOf(Function)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should throw on non-existent component', () => {
|
|
30
|
+
input[generate()] = {}
|
|
31
|
+
|
|
32
|
+
expect(call).toThrow(/Configuration Schema/)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should throw if object doesn\'t match schema', () => {
|
|
36
|
+
const keys = Object.keys(input)
|
|
37
|
+
const key = sample(keys)
|
|
38
|
+
const object = input[key]
|
|
39
|
+
|
|
40
|
+
object.foo = generate()
|
|
41
|
+
|
|
42
|
+
expect(call).toThrow(/foo must be number/)
|
|
43
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
const schema = {
|
|
6
|
+
properties: {
|
|
7
|
+
foo: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
default: generate()
|
|
10
|
+
},
|
|
11
|
+
bar: {
|
|
12
|
+
properties: {
|
|
13
|
+
baz: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
default: 1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
quu: {
|
|
20
|
+
type: 'number'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const concise = {
|
|
26
|
+
foo: schema.properties.foo.default,
|
|
27
|
+
bar: {
|
|
28
|
+
baz: schema.properties.bar.properties.baz.default
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.schema = schema
|
|
33
|
+
exports.concise = concise
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Locator } = require('@toa.io/core')
|
|
4
|
+
const { encode } = require('@toa.io/generic')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('./aspect.fixtures')
|
|
7
|
+
const { Factory } = require('../')
|
|
8
|
+
const { generate } = require('randomstring')
|
|
9
|
+
|
|
10
|
+
const factory = new Factory()
|
|
11
|
+
|
|
12
|
+
/** @type {toa.extensions.configuration.Aspect} */
|
|
13
|
+
let aspect
|
|
14
|
+
|
|
15
|
+
/** @type {toa.core.Locator} */
|
|
16
|
+
let locator
|
|
17
|
+
|
|
18
|
+
describe('defaults', () => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const namespace = generate()
|
|
21
|
+
const name = generate()
|
|
22
|
+
|
|
23
|
+
locator = new Locator(name, namespace)
|
|
24
|
+
|
|
25
|
+
aspect = factory.aspect(locator, fixtures.schema)
|
|
26
|
+
|
|
27
|
+
await aspect.connect()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return schema defaults', () => {
|
|
31
|
+
const foo = aspect.invoke(['foo'])
|
|
32
|
+
|
|
33
|
+
expect(foo).toStrictEqual(fixtures.schema.properties.foo.default)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should return nested values', () => {
|
|
37
|
+
const baz = aspect.invoke(['bar', 'baz'])
|
|
38
|
+
|
|
39
|
+
expect(baz).toStrictEqual(fixtures.schema.properties.bar.properties.baz.default)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should expose configuration tree', () => {
|
|
43
|
+
const configuration = aspect.invoke()
|
|
44
|
+
|
|
45
|
+
expect(configuration).toStrictEqual({
|
|
46
|
+
foo: fixtures.schema.properties.foo.default,
|
|
47
|
+
bar: {
|
|
48
|
+
baz: fixtures.schema.properties.bar.properties.baz.default
|
|
49
|
+
},
|
|
50
|
+
quu: 0
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('resolution', () => {
|
|
56
|
+
let object
|
|
57
|
+
let varname
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
object = { foo: generate() }
|
|
61
|
+
|
|
62
|
+
varname = 'TOA_CONFIGURATION_' + locator.uppercase
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should resolve configuration object from environment variable', async () => {
|
|
66
|
+
process.env[varname] = encode(object)
|
|
67
|
+
|
|
68
|
+
aspect = factory.aspect(locator, fixtures.schema)
|
|
69
|
+
|
|
70
|
+
await aspect.connect()
|
|
71
|
+
|
|
72
|
+
const configuration = aspect.invoke()
|
|
73
|
+
|
|
74
|
+
expect(configuration.foo).toStrictEqual(object.foo)
|
|
75
|
+
expect(configuration.bar.baz).toStrictEqual(fixtures.schema.properties.bar.properties.baz.default)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { encode } = require('@toa.io/generic')
|
|
4
|
+
|
|
5
|
+
const fixtures = require('./deployment.fixtures')
|
|
6
|
+
const { deployment } = require('../')
|
|
7
|
+
|
|
8
|
+
/** @type {toa.deployment.dependency.Declaration} */
|
|
9
|
+
let declaration
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
declaration = deployment(fixtures.components, fixtures.annotations)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should exist', () => {
|
|
16
|
+
expect(deployment).toBeDefined()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should declare variables', () => {
|
|
20
|
+
expect(declaration.variables).toBeDefined()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should map configurations', () => {
|
|
24
|
+
const keys = Object.keys(fixtures.annotations)
|
|
25
|
+
|
|
26
|
+
expect(keys.length).toBeGreaterThan(0)
|
|
27
|
+
|
|
28
|
+
for (const [id, annotations] of Object.entries(fixtures.annotations)) {
|
|
29
|
+
const component = fixtures.find(id)
|
|
30
|
+
const variables = declaration.variables[component.locator.label]
|
|
31
|
+
const encoded = encode(annotations)
|
|
32
|
+
|
|
33
|
+
expect(component).toBeDefined()
|
|
34
|
+
expect(variables).toBeDefined()
|
|
35
|
+
expect(variables).toBeInstanceOf(Array)
|
|
36
|
+
expect(variables.length).toStrictEqual(1)
|
|
37
|
+
|
|
38
|
+
const env = variables[0]
|
|
39
|
+
|
|
40
|
+
expect(env.name).toBeDefined()
|
|
41
|
+
expect(env.name).toStrictEqual('TOA_CONFIGURATION_' + component.locator.uppercase)
|
|
42
|
+
expect(env.value).toBeDefined()
|
|
43
|
+
expect(env.value).toStrictEqual(encoded)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
items: {
|
|
113
|
+
type: 'number'
|
|
114
|
+
},
|
|
115
|
+
default: [1, 2, 3]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should throw on empty array', () => {
|
|
122
|
+
const concise = { foo: [] }
|
|
123
|
+
|
|
124
|
+
expect(() => manifest(concise)).toThrow(/array items type because it's empty/)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should throw on null', () => {
|
|
128
|
+
const concise = { foo: null }
|
|
129
|
+
|
|
130
|
+
expect(() => manifest(concise)).toThrow(/type of null/)
|
|
131
|
+
})
|
|
132
|
+
})
|
package/types/aspect.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Source } from '@toa.io/core/types/reflection'
|
|
2
|
+
import { Connector } from '@toa.io/core/types'
|
|
3
|
+
|
|
4
|
+
declare namespace toa.extensions.configuration {
|
|
5
|
+
|
|
6
|
+
interface Provider extends Connector {
|
|
7
|
+
source: Source
|
|
8
|
+
object: Object
|
|
9
|
+
key: string
|
|
10
|
+
|
|
11
|
+
set(key: string, value: any): void
|
|
12
|
+
|
|
13
|
+
unset(key: string): void
|
|
14
|
+
|
|
15
|
+
reset(): void
|
|
16
|
+
|
|
17
|
+
export(): string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Provider = toa.extensions.configuration.Provider
|