@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
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.configuration",
3
- "version": "0.20.0-dev.9",
3
+ "version": "0.20.1-alpha.0",
4
4
  "description": "Toa Configuration",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
7
- "main": "source/index.js",
7
+ "main": "transpiled/index.js",
8
+ "types": "transpiled/index.d.ts",
8
9
  "repository": {
9
10
  "type": "git",
10
11
  "url": "git+https://github.com/toa-io/toa.git"
@@ -15,12 +16,17 @@
15
16
  "publishConfig": {
16
17
  "access": "public"
17
18
  },
18
- "dependencies": {
19
- "@toa.io/core": "0.20.0-dev.9",
20
- "@toa.io/generic": "0.20.0-dev.9",
21
- "@toa.io/schema": "0.20.0-dev.9",
22
- "@toa.io/yaml": "0.20.0-dev.9",
23
- "clone-deep": "4.0.1"
19
+ "scripts": {
20
+ "transpile": "tsc"
21
+ },
22
+ "jest": {
23
+ "preset": "ts-jest",
24
+ "testEnvironment": "node"
24
25
  },
25
- "gitHead": "4e503ffe4fe6dfab1165e795832d3d4fba711582"
26
+ "gitHead": "ed28dc0d2823022fbb1188bb28b994bc827a4432",
27
+ "dependencies": {
28
+ "@toa.io/core": "0.21.0-alpha.0",
29
+ "@toa.io/generic": "0.20.1-alpha.0",
30
+ "@toa.io/schemas": "0.20.1-alpha.0"
31
+ }
26
32
  }
package/readme.md CHANGED
@@ -1,24 +1,28 @@
1
- # Toa Configuration Extension
1
+ # Toa Configuration
2
2
 
3
3
  ## TL;DR
4
4
 
5
5
  ### Define
6
6
 
7
7
  ```yaml
8
- # component.toa.yaml
8
+ # manifest.toa.yaml
9
9
  name: dummy
10
10
  namespace: dummies
11
11
 
12
12
  configuration:
13
- foo: bar
14
- baz: 1
13
+ schema:
14
+ foo: string
15
+ bar: number
16
+ defaults:
17
+ foo: bar
18
+ bar: 1
15
19
  ```
16
20
 
17
21
  ### Use
18
22
 
19
23
  ```javascript
20
24
  function transition (input, entity, context) {
21
- const { foo, baz } = context.configuration
25
+ const { foo, bar } = context.configuration
22
26
 
23
27
  // ...
24
28
  }
@@ -30,144 +34,111 @@ function transition (input, entity, context) {
30
34
  # context.toa.yaml
31
35
  configuration:
32
36
  dummies.dummy:
33
- foo: qux # override default value defined by dummies.dummy
37
+ foo: qux # override default value
34
38
  foo@staging: quux # deployment environment discriminator
35
- baz: $BAZ_VALUE # secret
39
+ bar: $BAZ_VALUE # secret
36
40
  ```
37
41
 
38
42
  ### Deploy secrets
39
43
 
40
44
  ```shell
41
- $ toa conceal configuration BAZ_VALUE '$ecr3t'
45
+ $ toa conceal configuration BAZ_VALUE=$ecr3t
42
46
  ```
43
47
 
44
48
  ---
45
49
 
46
50
  ## Problem Definition
47
51
 
48
- - Components must be reusable in different contexts and deployment environments that are in different configurations.
49
- - Some algorithm parameters must be deployed secretly.
50
-
51
- ## Definitions
52
-
53
- ### Configuration (Distributed System Configuration)
54
-
55
- Set of static[^1] parameters for all algorithms within a given system.
56
-
57
- ### Configuration Schema
58
-
59
- Schema is defining component's algorithm parameters (optionally with default values).
60
-
61
- ### Configuration Object
52
+ - Components should be runnable in different deployment environments.
53
+ - Some algorithm's parameters should be deployed secretly.
54
+ - Components should be reusable in different contexts.
62
55
 
63
- Value valid against Configuration Schema.
56
+ ## Manifest
64
57
 
65
- ### Configuration Value
58
+ Component's configuration is declared using `configuration` manifest,
59
+ containing `schema` and optionnaly `defaults` properties.
66
60
 
67
- The merge result of Configuration Schema's defaults and Configuration Object.
61
+ ### Schema
68
62
 
69
- ### Context Configuration
63
+ Configuration schema is declared with [COS](/libraries/concise).
70
64
 
71
- Map of Configuration Objects for components added to a given context.
72
-
73
- ## Responsibility Segregation
65
+ ```yaml
66
+ # manifest.toa.yaml
67
+ name: dummy
68
+ namespace: dummies
74
69
 
75
- Configuration Schema is a *form* of configuration defined by component. Specific *values* for
76
- specific contexts and deployment environments are defined by Context Configuration according to the
77
- Schema.
70
+ configuration:
71
+ schema:
72
+ foo: string
73
+ bar: number
74
+ ```
78
75
 
79
- See [Reusable Components](#).
76
+ > Introducing non-backward compatible changes to a configuration schema will result in a loss of
77
+ > compatibility with existing contexts and deployment environments.
78
+ > Therefore, configuration schema changes are subject to component versioning.
80
79
 
81
- ## Configuration Schema
80
+ If `configuration` object doesn't contain property `schema`, then it is considered to be schema.
82
81
 
83
- Configuration Schema is declared as a component extension
84
- using [JSON Schema](https://json-schema.org) `object` type.
82
+ ```yaml
83
+ # manifest.toa.yaml
84
+ name: dummy
85
+ namespace: dummies
85
86
 
86
- > ![Warning](https://img.shields.io/badge/Warning-yellow)<br/>
87
- > By introducing non-backward compatible changes to a Configuration Schema, the compatibility
88
- > with existent contexts and deployment environments will be broken. That is, Configuration
89
- > Schema changes are subjects of component versioning.
87
+ configuration:
88
+ foo: string
89
+ bar: number
90
+ ```
90
91
 
91
- > ![Recommendation](https://img.shields.io/badge/Recommendation-green)<br/>
92
- > Having default values for all required parameters will allow components to be runnable
93
- > without configuration (i.e. on local environment).
92
+ ### Defaults
94
93
 
95
- ### Example
94
+ The default configuration value can be provided using the `defaults` property, which should conform
95
+ to the configuration schema.
96
96
 
97
97
  ```yaml
98
- # component.toa.yaml
98
+ # manifest.toa.yaml
99
99
  name: dummy
100
100
  namespace: dummies
101
101
 
102
- extensions:
103
- "@toa.io/extensions.configuration":
104
- properties:
105
- foo:
106
- type: string
107
- default: 'baz'
108
- bar:
109
- type: number
110
- required: [foo]
102
+ configuration:
103
+ schema:
104
+ foo: string
105
+ bar: number
106
+ defaults:
107
+ foo: hello
108
+ bar: 0
111
109
  ```
112
110
 
113
- ### Concise Declaration
111
+ #### Schema defaults hint
114
112
 
115
- As it is known that Configuration Schema is declared with a JSON Schema `object` type, any
116
- configuration declaration without defined `properties` considered as concise. Properties of concise
117
- declaration are treated as required Configuration Schema properties with the same type as its value
118
- type and no additional properties allowed.
119
-
120
- Also note that a well-known shortcut `configuration` is available.
121
-
122
- The next two declarations are equivalent.
113
+ The configuration schema itself can contain default primitive values using the COS syntax.
123
114
 
124
115
  ```yaml
125
- # component.toa.yaml
126
- configuration:
127
- foo: baz
128
- bar: 1
129
- ```
116
+ # manifest.toa.yaml
117
+ name: dummy
118
+ namespace: dummies
130
119
 
131
- ```yaml
132
- # component.toa.yaml
133
- extensions:
134
- "@toa.io/extensions.configuration":
135
- properties:
136
- foo:
137
- type: string
138
- default: baz
139
- bar:
140
- type: number
141
- default: 1
142
- additionalProperties: false
143
- required: [foo, bar]
120
+ configuration:
121
+ schema:
122
+ foo: hello
123
+ bar: 0
144
124
  ```
145
125
 
146
- ## Context Configuration
147
-
148
- Context Configuration is declared as a context annotation. Its keys must be
149
- component identifiers, and its values must be Configuration Objects for those
150
- components.
151
-
152
- Context Configuration keys and Configuration Object keys may be defined
153
- with [deployment environment discriminators](#).
126
+ ## Annotation
154
127
 
155
- ### Example
128
+ A component's configuration can be overridden using the configuration context annotation.
156
129
 
157
130
  ```yaml
158
131
  # context.toa.yaml
159
132
  configuration:
160
133
  dummies.dummy:
161
- foo: quu
134
+ foo: bye
162
135
  bar: 1
163
136
  bar@staging: 2
164
137
  ```
165
138
 
166
- ## Configuration Secrets
139
+ ## Secrets
167
140
 
168
- Context Configuration values which are uppercase strings prefixed with `$` considered as Secrets.
169
-
170
- ### Example
141
+ Configuration annotation top-level values which are uppercase strings prefixed with `$` considered as secrets.
171
142
 
172
143
  ```yaml
173
144
  # context.toa.yaml
@@ -176,81 +147,25 @@ configuration:
176
147
  api-key: $STRIPE_API_KEY
177
148
  ```
178
149
 
179
- Configuration values that are assigned with a reference to the Secret must be of type `string`.
180
-
181
- ### Secrets Deployment
182
-
183
150
  Secrets are not being deployed with context
184
- deployment ([`toa deploy`](../../runtime/cli/readme.md#deploy)),
185
- thus must be deployed separately at least once for each deployment environment
186
- manually ([`toa conceal`](../../runtime/cli/readme.md#conceal)).
151
+ deployment ([`toa deploy`](/runtime/cli/readme.md#deploy)), thus must be deployed separately at
152
+ least once for each deployment environment
153
+ manually ([`toa conceal`](/runtime/cli/readme.md#conceal)).
187
154
 
188
155
  Deployed kubernetes secret's name is predefined as `configuration`.
189
156
 
190
157
  ```shell
191
- $ toa conceal configuration STRIPE_API_KEY xxxxxxxx
158
+ $ toa conceal configuration STRIPE_API_KEY=xxxxxxxx
192
159
  ```
193
160
 
194
161
  ## Aspect
195
162
 
196
- Configuration Value is available as a well-known operation Aspect `configuration`.
163
+ Component's configuration values are available as a well-known Aspect `configuration`.
197
164
 
198
165
  ```javascript
199
- // Node.js bridge
200
-
201
166
  function transition (input, entity, context) {
202
167
  const foo = context.configiuration.foo
203
168
 
204
169
  // ...
205
170
  }
206
171
  ```
207
-
208
- > ![Warning](https://img.shields.io/badge/Warning-yellow)<br/>
209
- > It is strongly **not** recommended to store a copy of value type configuration
210
- > values outside operation scope, thus it prevents operation to benefit
211
- > from [hot updates](#).
212
- >
213
- > ```javascript
214
- > // NOT RECOMMENDED
215
- > let foo
216
- >
217
- > function transition (input, entity, context) {
218
- > // NOT RECOMMENDED
219
- > if (foo === undefined) foo = context.configuration.foo
220
- >
221
- > // ...
222
- > }
223
- > ```
224
- > See [Genuine operations](/documentation/design.md#genuine-operations).
225
-
226
- ## Development Configuration
227
-
228
- Configuration can be exported by [`toa env`](/runtime/cli/readme.md#env).
229
-
230
- ### Local Environment Placeholders
231
-
232
- Context Configuration values may contain placeholders that reference environment variables.
233
- Placeholders are replaced with values if the corresponding environment variables are set.
234
-
235
- > Placeholders can only be used with local environment (exported by `toa env`), as these values are not
236
- > deployed.
237
-
238
- ```yaml
239
- # context.toa.yaml
240
- configuration:
241
- dummies.dummy:
242
- url@local: https://stage${STAGE}.intranet/
243
- ```
244
-
245
- ```dotenv
246
- # .env
247
- STAGE=82
248
- ```
249
-
250
- ## Appendix
251
-
252
- - [Discussion](./docs/discussion.md)
253
- - [Configuration consistency](./docs/consistency.md)
254
-
255
- [^1]: Cannot be changed without a deployment as new values are considered to be a subject of
256
- testing. [#146](https://github.com/toa-io/toa/issues/146)
@@ -0,0 +1 @@
1
+ <object>
@@ -0,0 +1,2 @@
1
+ schema: object
2
+ defaults?: object
@@ -0,0 +1,15 @@
1
+ import { Aspect } from './Aspect'
2
+
3
+ it('should return value', async () => {
4
+ const configuration = {
5
+ foo: 'bar',
6
+ bar: {
7
+ baz: 'quux'
8
+ }
9
+ }
10
+
11
+ const aspect = new Aspect(configuration)
12
+
13
+ expect(aspect.invoke(['foo'])).toStrictEqual(configuration.foo)
14
+ expect(aspect.invoke(['bar', 'baz'])).toStrictEqual(configuration.bar.baz)
15
+ })
@@ -0,0 +1,23 @@
1
+ import { Connector, type extensions } from '@toa.io/core'
2
+
3
+ export class Aspect extends Connector implements extensions.Aspect {
4
+ public readonly name = 'configuration'
5
+
6
+ private readonly value: object
7
+
8
+ public constructor (value: object) {
9
+ super()
10
+
11
+ this.value = value
12
+ }
13
+
14
+ public invoke (path: string[]): any {
15
+ let cursor: any = this.value
16
+
17
+ if (path !== undefined)
18
+ for (const segment of path)
19
+ cursor = cursor[segment]
20
+
21
+ return cursor
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import { type Locator, type extensions } from '@toa.io/core'
2
+ import { Aspect } from './Aspect'
3
+ import { type Manifest } from './manifest'
4
+ import { get } from './configuration'
5
+
6
+ export class Factory implements extensions.Factory {
7
+ public aspect (locator: Locator, manifest: Manifest): extensions.Aspect {
8
+ const value = get(locator, manifest)
9
+
10
+ return new Aspect(value)
11
+ }
12
+ }
@@ -0,0 +1,89 @@
1
+ import { encode } from '@toa.io/generic'
2
+ import { Locator } from '@toa.io/core'
3
+ import { generate } from 'randomstring'
4
+ import { get } from './configuration'
5
+ import { type Manifest } from './manifest'
6
+
7
+ let locator: Locator
8
+ let manifest: Manifest
9
+
10
+ beforeEach(() => {
11
+ locator = new Locator(generate(), generate())
12
+ manifest = { schema: { foo: 'string' } }
13
+ })
14
+
15
+ afterEach(() => {
16
+ for (const name of used)
17
+ process.env[name] = undefined
18
+
19
+ used = []
20
+ })
21
+
22
+ it('should read value', async () => {
23
+ manifest.schema = { foo: 'string' }
24
+ const value: object = { foo: generate() }
25
+
26
+ set(value)
27
+
28
+ const result = get(locator, manifest)
29
+
30
+ expect(result).toStrictEqual(value)
31
+ })
32
+
33
+ it('should return empty object if no value set', async () => {
34
+ expect(get(locator, manifest)).toStrictEqual({})
35
+ })
36
+
37
+ it('should substitute secrets', async () => {
38
+ const value: object = { foo: '$BAR' }
39
+
40
+ set(value)
41
+ set('bar', '_BAR')
42
+
43
+ const result = get(locator, manifest)
44
+
45
+ expect(result).toStrictEqual({ foo: 'bar' })
46
+ })
47
+
48
+ it('should use defaults', async () => {
49
+ manifest.schema = { foo: 'string', bar: ['number'], 'baz?': 'string' }
50
+ manifest.defaults = { foo: 'bar', bar: [1] }
51
+
52
+ const values = { bar: [2], baz: 'foo' }
53
+
54
+ set(values)
55
+
56
+ const result = get(locator, manifest)
57
+
58
+ expect(result).toStrictEqual({
59
+ foo: 'bar',
60
+ bar: [2],
61
+ baz: 'foo'
62
+ })
63
+ })
64
+
65
+ it('should validate', async () => {
66
+ manifest.schema = { foo: 'hello', bar: 'number' }
67
+
68
+ const values = { bar: 5 }
69
+
70
+ set(values)
71
+
72
+ const result = get(locator, manifest)
73
+
74
+ expect(result).toStrictEqual({
75
+ foo: 'hello',
76
+ bar: 5
77
+ })
78
+ })
79
+
80
+ function set (value: object | string, key = locator.uppercase): void {
81
+ const string = typeof value === 'string' ? value : encode(value)
82
+ const name = 'TOA_CONFIGURATION_' + key
83
+
84
+ process.env[name] = string
85
+
86
+ used.push(name)
87
+ }
88
+
89
+ let used: string[] = []
@@ -0,0 +1,53 @@
1
+ import { type Locator } from '@toa.io/core'
2
+ import { decode, add } from '@toa.io/generic'
3
+ import * as schemas from '@toa.io/schemas'
4
+ import { PREFIX, SECRET_RX } from './deployment'
5
+ import { type Manifest } from './manifest'
6
+
7
+ export function get (locator: Locator, manifest: Manifest): Configuration {
8
+ const values = getConfiguration(locator.uppercase)
9
+
10
+ substituteSecrets(values)
11
+
12
+ if (manifest.defaults !== undefined)
13
+ add(values, manifest.defaults)
14
+
15
+ const schema = schemas.schema(manifest.schema)
16
+
17
+ schema.validate(values)
18
+
19
+ return values
20
+ }
21
+
22
+ function getConfiguration (suffix: string): Configuration {
23
+ const variable = PREFIX + suffix
24
+ const string = process.env[variable]
25
+
26
+ if (string === undefined) return {}
27
+ else return decode(string)
28
+ }
29
+
30
+ function substituteSecrets (configuration: Configuration): void {
31
+ for (const [key, value] of Object.entries(configuration)) {
32
+ if (typeof value !== 'string') continue
33
+
34
+ const match = value.match(SECRET_RX)
35
+
36
+ if (match === null) continue
37
+
38
+ const name = match.groups?.variable as string
39
+
40
+ configuration[key] = getSecret(name)
41
+ }
42
+ }
43
+
44
+ function getSecret (name: string): string {
45
+ const variable = PREFIX + '_' + name
46
+ const value = process.env[variable]
47
+
48
+ if (value === undefined) throw new Error(`${variable} is not set.`)
49
+
50
+ return value
51
+ }
52
+
53
+ export type Configuration = Record<string, any>
@@ -0,0 +1,21 @@
1
+ import { type Annotation, deployment, type Instance } from './deployment'
2
+ import { type Manifest } from './manifest'
3
+
4
+ it('should validate annotation', async () => {
5
+ const wrongType = 'not ok' as unknown as Annotation
6
+
7
+ expect(() => deployment([], wrongType)).toThrow('object')
8
+ })
9
+
10
+ it('should validate values', async () => {
11
+ const manifest: Manifest = {
12
+ schema: { foo: 'string', bar: 'number' },
13
+ defaults: { foo: 'ok', bar: 0 }
14
+ }
15
+
16
+ const locator = { id: 'component' }
17
+ const instances = [{ manifest, locator }] as unknown as Instance[]
18
+ const annotation: Annotation = { [locator.id]: { bar: 'not a number' } }
19
+
20
+ expect(() => deployment(instances, annotation)).toThrow('number')
21
+ })
@@ -0,0 +1,69 @@
1
+ import { type Dependency, type Variable, type Variables } from '@toa.io/operations'
2
+ import * as schemas from '@toa.io/schemas'
3
+ import { encode, overwrite } from '@toa.io/generic'
4
+ import { type Manifest } from './manifest'
5
+ import * as validators from './schemas'
6
+ import type { context } from '@toa.io/norm'
7
+
8
+ export function deployment (instances: Instance[], annotation: Annotation = {}): Dependency {
9
+ validators.annotation.validate(annotation)
10
+
11
+ const variables: Variables = {}
12
+
13
+ for (const instance of instances) {
14
+ const values = annotation[instance.locator.id]
15
+
16
+ validate(instance, values)
17
+
18
+ if (values === undefined) continue
19
+
20
+ variables[instance.locator.label] = [{
21
+ name: PREFIX + instance.locator.uppercase,
22
+ value: encode(values)
23
+ }]
24
+
25
+ const secrets = createSecrets(values)
26
+
27
+ variables[instance.locator.label].push(...secrets)
28
+ }
29
+
30
+ return { variables }
31
+ }
32
+
33
+ function createSecrets (values: object): Variable[] {
34
+ const secrets: Variable[] = []
35
+
36
+ for (const value of Object.values(values)) {
37
+ if (typeof value !== 'string') continue
38
+
39
+ const match = value.match(SECRET_RX)
40
+
41
+ if (match === null) continue
42
+
43
+ const name = match.groups?.variable as string
44
+
45
+ secrets.push({
46
+ name: PREFIX + '_' + name,
47
+ secret: {
48
+ name: 'toa-configuration',
49
+ key: name
50
+ }
51
+ })
52
+ }
53
+
54
+ return secrets
55
+ }
56
+
57
+ function validate (instace: Instance, values: object = {}): void {
58
+ const defaults = instace.manifest.defaults ?? {}
59
+ const configuration = overwrite(defaults, values)
60
+ const schema = schemas.schema(instace.manifest.schema)
61
+
62
+ schema.validate(configuration)
63
+ }
64
+
65
+ export const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
66
+ export const PREFIX = 'TOA_CONFIGURATION_'
67
+
68
+ export type Annotation = Record<string, object>
69
+ export type Instance = context.Dependency<Manifest>
@@ -0,0 +1,3 @@
1
+ export { manifest } from './manifest'
2
+ export { deployment } from './deployment'
3
+ export { Factory } from './Factory'
@@ -0,0 +1,15 @@
1
+ import { type Manifest, manifest } from './manifest'
2
+
3
+ it('should validate', async () => {
4
+ const additional = { schema: {}, foo: 'bar' } as unknown as Manifest
5
+
6
+ expect(() => {
7
+ manifest(additional)
8
+ }).toThrow('not expected')
9
+
10
+ const wrongType = { schema: 'not ok' } as unknown as Manifest
11
+
12
+ expect(() => {
13
+ manifest(wrongType)
14
+ }).toThrow('object')
15
+ })