@toa.io/extensions.origins 0.20.0-dev.9 → 0.20.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 (43) hide show
  1. package/package.json +17 -11
  2. package/readme.md +70 -67
  3. package/schemas/annotation.cos.yaml +3 -0
  4. package/schemas/manifest.cos.yaml +4 -0
  5. package/source/Factory.ts +88 -0
  6. package/source/annotation.test.ts +150 -0
  7. package/source/annotation.ts +83 -0
  8. package/source/extension.test.ts +161 -0
  9. package/source/extension.ts +60 -0
  10. package/source/index.ts +2 -0
  11. package/source/manifest.test.ts +30 -0
  12. package/source/manifest.ts +11 -0
  13. package/source/protocols/amqp/.test/aspect.fixtures.js +1 -1
  14. package/source/protocols/amqp/.test/mock.comq.js +2 -2
  15. package/source/protocols/amqp/aspect.js +17 -24
  16. package/source/protocols/amqp/deployment.js +8 -2
  17. package/source/protocols/http/.aspect/permissions.js +13 -10
  18. package/source/protocols/http/aspect.js +16 -37
  19. package/source/protocols/index.ts +16 -0
  20. package/tsconfig.json +12 -0
  21. package/source/.credentials.js +0 -14
  22. package/source/.deployment/index.js +0 -5
  23. package/source/.deployment/uris.js +0 -37
  24. package/source/.test/constants.js +0 -3
  25. package/source/.test/deployment.fixtures.js +0 -20
  26. package/source/.test/factory.fixtures.js +0 -13
  27. package/source/deployment.js +0 -41
  28. package/source/deployment.test.js +0 -185
  29. package/source/factory.js +0 -44
  30. package/source/factory.test.js +0 -140
  31. package/source/index.js +0 -9
  32. package/source/manifest.js +0 -41
  33. package/source/manifest.test.js +0 -82
  34. package/source/protocols/amqp/aspect.test.js +0 -119
  35. package/source/protocols/http/.aspect/permissions.test.js +0 -23
  36. package/source/protocols/http/aspect.test.js +0 -220
  37. package/source/protocols/index.js +0 -6
  38. package/source/schemas/annotations.cos.yaml +0 -1
  39. package/source/schemas/index.js +0 -8
  40. package/source/schemas/manifest.cos.yaml +0 -2
  41. package/types/amqp.d.ts +0 -9
  42. package/types/deployment.d.ts +0 -7
  43. package/types/http.d.ts +0 -28
@@ -0,0 +1,161 @@
1
+ import { generate } from 'randomstring'
2
+ import { encode } from 'msgpackr'
3
+ import { Locator } from '@toa.io/core'
4
+ import { deployment, type Instance } from './extension'
5
+ import type { Annotation, Properties } from './annotation'
6
+ import type { Manifest } from './manifest'
7
+ import type { Dependency } from '@toa.io/operations'
8
+
9
+ const locator = new Locator(generate(), generate())
10
+ const NAMESPACE = locator.namespace.toUpperCase()
11
+ const NAME = locator.name.toUpperCase()
12
+
13
+ it('should deploy pointer variables', async () => {
14
+ const manifest: Manifest = { queue: null }
15
+ const instance = { locator, manifest } as unknown as Instance
16
+ const url = 'amqp://host-' + generate()
17
+
18
+ const annotation: Annotation = {
19
+ [locator.id]: {
20
+ queue: url
21
+ }
22
+ }
23
+
24
+ const deploy = deployment([instance], annotation)
25
+
26
+ const expected: Dependency = {
27
+ variables: {
28
+ [locator.label]: expect.arrayContaining([
29
+ {
30
+ name: `TOA_ORIGINS_${NAMESPACE}_${NAME}_QUEUE`,
31
+ value: url
32
+ }
33
+ ])
34
+ }
35
+ }
36
+
37
+ expect(deploy)
38
+ .toMatchObject(expected)
39
+ })
40
+
41
+ it('should deploy default origin', async () => {
42
+ const example = 'http://api.example.com'
43
+ const manifest: Manifest = { example }
44
+ const instance = { locator, manifest } as unknown as Instance
45
+ const annotation: Annotation = {}
46
+ const deploy = deployment([instance], annotation)
47
+
48
+ const expected: Dependency = {
49
+ variables: {
50
+ [locator.label]: expect.arrayContaining([
51
+ {
52
+ name: `TOA_ORIGINS_${NAMESPACE}_${NAME}_EXAMPLE`,
53
+ value: example
54
+ }
55
+ ])
56
+ }
57
+ }
58
+
59
+ expect(deploy)
60
+ .toMatchObject(expected)
61
+ })
62
+
63
+ it('should deploy properties', async () => {
64
+ const manifest: Manifest = {}
65
+ const instance = { locator, manifest } as unknown as Instance
66
+ const properties: Properties = {
67
+ '.http': {
68
+ '/^http:\\/\\/\\w+api.example.com/': true
69
+ }
70
+ }
71
+
72
+ const annotation: Annotation = {
73
+ [locator.id]: properties
74
+ }
75
+
76
+ const deploy = deployment([instance], annotation)
77
+ const value = encode(properties).toString('base64')
78
+
79
+ const expected: Dependency = {
80
+ variables: {
81
+ [locator.label]: expect.arrayContaining([
82
+ {
83
+ name: `TOA_ORIGINS_${NAMESPACE}_${NAME}__PROPERTIES`,
84
+ value
85
+ }
86
+ ])
87
+ }
88
+ }
89
+
90
+ expect(deploy)
91
+ .toMatchObject(expected)
92
+ })
93
+
94
+ it('should deploy properties with null manifest', async () => {
95
+ const manifest: Manifest = null
96
+ const instance = { locator, manifest } as unknown as Instance
97
+ const properties: Properties = {
98
+ '.http': {
99
+ '/^http:\\/\\/\\w+api.example.com/': true
100
+ }
101
+ }
102
+
103
+ const annotation: Annotation = {
104
+ [locator.id]: properties
105
+ }
106
+
107
+ const deploy = deployment([instance], annotation)
108
+ const value = encode(properties).toString('base64')
109
+
110
+ const expected: Dependency = {
111
+ variables: {
112
+ [locator.label]: expect.arrayContaining([
113
+ {
114
+ name: `TOA_ORIGINS_${NAMESPACE}_${NAME}__PROPERTIES`,
115
+ value
116
+ }
117
+ ])
118
+ }
119
+ }
120
+
121
+ expect(deploy)
122
+ .toMatchObject(expected)
123
+ })
124
+
125
+ it('should deploy credentials for amqp', async () => {
126
+ const manifest: Manifest = { queue: null }
127
+ const instance = { locator, manifest } as unknown as Instance
128
+ const url = 'amqp://host-' + generate()
129
+
130
+ const annotation: Annotation = {
131
+ [locator.id]: {
132
+ queue: url
133
+ }
134
+ }
135
+
136
+ const deploy = deployment([instance], annotation)
137
+
138
+ const expected: Dependency = {
139
+ variables: {
140
+ [locator.label]: expect.arrayContaining([
141
+ {
142
+ name: `TOA_ORIGINS_${NAMESPACE}_${NAME}_QUEUE_USERNAME`,
143
+ secret: {
144
+ name: `toa-origins-${locator.label}-queue`,
145
+ key: 'username'
146
+ }
147
+ },
148
+ {
149
+ name: `TOA_ORIGINS_${NAMESPACE}_${NAME}_QUEUE_PASSWORD`,
150
+ secret: {
151
+ name: `toa-origins-${locator.label}-queue`,
152
+ key: 'password'
153
+ }
154
+ }
155
+ ])
156
+ }
157
+ }
158
+
159
+ expect(deploy)
160
+ .toMatchObject(expected)
161
+ })
@@ -0,0 +1,60 @@
1
+ import { encode } from 'msgpackr'
2
+ import { createVariables, type Request } from '@toa.io/pointer'
3
+ import { merge } from '@toa.io/generic'
4
+ import { normalize, split, type Annotation, type Properties, type Origins } from './annotation'
5
+ import { type Manifest, validate } from './manifest'
6
+ import type { Locator } from '@toa.io/core'
7
+ import type { Dependency, Variables } from '@toa.io/operations'
8
+ import type { context } from '@toa.io/norm'
9
+
10
+ export function deployment (instances: Instance[], annotation: Annotation = {}): Dependency {
11
+ normalize(instances, annotation)
12
+
13
+ const variables: Variables = {}
14
+
15
+ for (const instance of instances) {
16
+ const component = annotation[instance.locator.id]
17
+ const { origins, properties } = split(component)
18
+ const instanceVariables = createInstanceVariables(instance, origins)
19
+ const propertiesVariable = createPropertiesVariable(instance.locator, properties)
20
+
21
+ merge(variables, instanceVariables)
22
+ merge(variables, propertiesVariable)
23
+ }
24
+
25
+ return { variables }
26
+ }
27
+
28
+ export function manifest (manifest: Manifest): Manifest {
29
+ validate(manifest)
30
+
31
+ return manifest
32
+ }
33
+
34
+ function createInstanceVariables (instance: Instance, origins: Origins): Variables {
35
+ if (instance.manifest === null) return {}
36
+
37
+ const label: string = instance.locator.label
38
+ const id = ID_PREFIX + label
39
+ const selectors = Object.keys(instance.manifest)
40
+ const request: Request = { group: label, selectors }
41
+
42
+ return createVariables(id, origins, [request])
43
+ }
44
+
45
+ function createPropertiesVariable (locator: Locator, properties: Properties): Variables {
46
+ const name = ENV_PREFIX + locator.uppercase + PROPERTIES_SUFFIX
47
+ const value = encode(properties).toString('base64')
48
+
49
+ return {
50
+ [locator.label]: [
51
+ { name, value }
52
+ ]
53
+ }
54
+ }
55
+
56
+ export const ID_PREFIX = 'origins-'
57
+ export const ENV_PREFIX = 'TOA_ORIGINS_'
58
+ export const PROPERTIES_SUFFIX = '__PROPERTIES'
59
+
60
+ export type Instance = context.Dependency<Manifest>
@@ -0,0 +1,2 @@
1
+ export { deployment, manifest } from './extension'
2
+ export { Factory } from './Factory'
@@ -0,0 +1,30 @@
1
+ import { type Manifest, validate } from './manifest'
2
+
3
+ let manifest: Manifest
4
+
5
+ it('should not throw if valid', async () => {
6
+ manifest = {
7
+ one: 'http://localhost',
8
+ two: null
9
+ }
10
+
11
+ expect(run).not.toThrow()
12
+ })
13
+
14
+ it('should throw if not a uri', async () => {
15
+ manifest = {
16
+ one: 'not a URI'
17
+ }
18
+
19
+ expect(run).toThrow('must match format')
20
+ })
21
+
22
+ it('should not throw on null manifest', async () => {
23
+ manifest = null
24
+
25
+ expect(run).not.toThrow()
26
+ })
27
+
28
+ function run (): void {
29
+ validate(manifest)
30
+ }
@@ -0,0 +1,11 @@
1
+ import { resolve } from 'node:path'
2
+ import * as schemas from '@toa.io/schemas'
3
+
4
+ export function validate (manifest: Manifest): void {
5
+ if (manifest !== null) schema.validate(manifest)
6
+ }
7
+
8
+ const path = resolve(__dirname, '../schemas/manifest.cos.yaml')
9
+ const schema = schemas.schema(path)
10
+
11
+ export type Manifest = Record<string, string | null> | null
@@ -9,7 +9,7 @@ const originCount = random(5) + 2
9
9
  for (let j = 0; j < originCount; j++) {
10
10
  const origin = generate()
11
11
 
12
- manifest[origin] = 'amqp://' + generate()
12
+ manifest[origin] = ['amqp://' + generate()]
13
13
  }
14
14
 
15
15
  exports.manifest = manifest
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { generate } = require('randomstring')
4
4
 
5
- const connect = jest.fn(async () => ({
5
+ const assert = jest.fn(async () => ({
6
6
  emit: jest.fn(async () => undefined),
7
7
  request: jest.fn(async () => generate()),
8
8
  reply: jest.fn(async () => undefined),
@@ -10,4 +10,4 @@ const connect = jest.fn(async () => ({
10
10
  close: jest.fn(async () => undefined)
11
11
  }))
12
12
 
13
- exports.connect = connect
13
+ exports.assert = assert
@@ -1,33 +1,26 @@
1
1
  'use strict'
2
2
 
3
- const { connect } = require('comq')
3
+ const { assert } = require('comq')
4
4
  const { Connector } = require('@toa.io/core')
5
- const { shards } = require('@toa.io/generic')
5
+ const protocol = require('./index')
6
6
 
7
- const { id } = require('./id')
8
-
9
- /**
10
- * @implements {toa.origins.amqp.Aspect}
11
- */
12
7
  class Aspect extends Connector {
13
- name = id
14
- /** @type {toa.origins.Manifest} */
15
- #manifest
8
+ name = protocol.id
9
+
10
+ #resolve
16
11
 
17
12
  /** @type {Record<string, Partial<comq.IO>>} */
18
13
  #origins = {}
19
14
 
20
- /**
21
- * @param {toa.origins.Manifest} manifest
22
- */
23
- constructor (manifest) {
15
+ constructor (resolve) {
24
16
  super()
25
17
 
26
- this.#manifest = manifest
18
+ this.#resolve = resolve
27
19
  }
28
20
 
29
21
  async open () {
30
- const promises = Object.entries(this.#manifest).map(this.#open)
22
+ const cfg = await this.#resolve()
23
+ const promises = Object.entries(cfg.origins).map(this.#open)
31
24
 
32
25
  await Promise.all(promises)
33
26
  }
@@ -39,12 +32,15 @@ class Aspect extends Connector {
39
32
  }
40
33
 
41
34
  async invoke (origin, method, ...args) {
35
+ if (this.#origins[origin]?.[method] === undefined) {
36
+ throw new Error(`Origin "${origin}" or method "${method}" is undefined`)
37
+ }
38
+
42
39
  return this.#origins[origin][method](...args)
43
40
  }
44
41
 
45
- #open = async ([origin, reference]) => {
46
- const references = shards(reference)
47
- const io = await connect(...references)
42
+ #open = async ([origin, references]) => {
43
+ const io = await assert(...references)
48
44
 
49
45
  this.#origins[origin] = restrict(io)
50
46
  }
@@ -67,11 +63,8 @@ function restrict (io) {
67
63
  }
68
64
  }
69
65
 
70
- /**
71
- * @param {toa.origins.Manifest} manifest
72
- */
73
- function create (manifest) {
74
- return new Aspect(manifest)
66
+ function create (resolve) {
67
+ return new Aspect(resolve)
75
68
  }
76
69
 
77
70
  exports.create = create
@@ -15,9 +15,13 @@ function deployment (instances) {
15
15
  const secrets = []
16
16
 
17
17
  for (const [origin, reference] of Object.entries(manifest)) {
18
- const url = new URL(reference)
18
+ let protocol
19
19
 
20
- if (protocols.includes(url.protocol)) {
20
+ const match = reference.match(RX)
21
+
22
+ if (match !== null) protocol = match.groups.protocol
23
+
24
+ if (protocols.includes(protocol)) {
21
25
  const originSecrets = createSecrets(locator, origin)
22
26
 
23
27
  secrets.push(...originSecrets)
@@ -60,4 +64,6 @@ function createSecret (locator, origin, property) {
60
64
  }
61
65
  }
62
66
 
67
+ const RX = /^(?<protocol>\w{1,12}:)/
68
+
63
69
  exports.deployment = deployment
@@ -1,23 +1,26 @@
1
1
  'use strict'
2
2
 
3
3
  const { echo } = require('@toa.io/generic')
4
+ const { Connector } = require('@toa.io/core')
4
5
 
5
- /**
6
- * @implements {toa.origins.http.Permissions}
7
- */
8
- class Permissions {
9
- #default = process.env.TOA_DEV === '1'
10
-
6
+ class Permissions extends Connector {
11
7
  /** @type {RegExp[]} */
12
8
  #allowances = []
13
9
 
14
10
  /** @type {RegExp[]} */
15
11
  #denials = []
16
12
 
17
- /**
18
- * @param {toa.origins.http.Properties} properties
19
- */
20
- constructor (properties) {
13
+ #resolve
14
+
15
+ constructor (resolve) {
16
+ super()
17
+
18
+ this.#resolve = resolve
19
+ }
20
+
21
+ async open () {
22
+ const { properties } = await this.#resolve()
23
+
21
24
  if (properties !== undefined) this.#parse(properties)
22
25
  }
23
26
 
@@ -1,41 +1,35 @@
1
1
  'use strict'
2
2
 
3
- /**
4
- * @typedef {import('node-fetch').RequestInit} Request
5
- * @typedef {import('node-fetch').Response} Response
6
- */
7
-
8
3
  const fetch = require('node-fetch')
9
4
 
10
5
  const { Connector } = require('@toa.io/core')
11
6
  const { retry } = require('@toa.io/generic')
12
7
 
13
8
  const { Permissions } = require('./.aspect/permissions')
14
- const { id } = require('./id')
15
9
  const protocols = require('./protocols')
10
+ const protocol = require('./index')
16
11
 
17
- /**
18
- * @implements {toa.origins.http.Aspect}
19
- */
20
12
  class Aspect extends Connector {
21
13
  /** @readonly */
22
- name = id
14
+ name = protocol.id
23
15
 
24
- /** @type {toa.origins.Manifest} */
16
+ #resolve
25
17
  #origins
26
-
27
- /** @type {toa.origins.http.Permissions} */
28
18
  #permissions
29
19
 
30
- /**
31
- * @param {toa.origins.Manifest} manifest
32
- * @param {toa.origins.http.Permissions} permissions
33
- */
34
- constructor (manifest, permissions) {
20
+ constructor (resolve, permissions) {
35
21
  super()
36
22
 
37
- this.#origins = manifest
23
+ this.#resolve = resolve
38
24
  this.#permissions = permissions
25
+
26
+ this.depends(permissions)
27
+ }
28
+
29
+ async open () {
30
+ const { origins } = await this.#resolve()
31
+
32
+ this.#origins = origins
39
33
  }
40
34
 
41
35
  async invoke (name, path, request, options) {
@@ -56,23 +50,12 @@ class Aspect extends Connector {
56
50
  return this.#request(url.href, request, options?.retry)
57
51
  }
58
52
 
59
- /**
60
- * @param {string} url
61
- * @param {Request} request
62
- * @return {Promise<Response>}
63
- */
64
53
  async #invokeURL (url, request) {
65
54
  if (this.#permissions.test(url) === false) throw new Error(`URL '${url}' is not allowed`)
66
55
 
67
56
  return this.#request(url, request)
68
57
  }
69
58
 
70
- /**
71
- * @param {string} url
72
- * @param {Request} request
73
- * @param {toa.generic.retry.Options} [options]
74
- * @return {Promise<Response>}
75
- */
76
59
  async #request (url, request, options) {
77
60
  const call = () => fetch(url, request)
78
61
 
@@ -117,14 +100,10 @@ function isAbsoluteURL (path) {
117
100
 
118
101
  const PLACEHOLDER = /\*/g
119
102
 
120
- /**
121
- * @param {toa.origins.Manifest} manifest
122
- * @param {toa.origins.http.Properties} [properties]
123
- */
124
- function create (manifest, properties) {
125
- const permissions = new Permissions(properties)
103
+ function create (resolve) {
104
+ const permissions = new Permissions(resolve)
126
105
 
127
- return new Aspect(manifest, permissions)
106
+ return new Aspect(resolve, permissions)
128
107
  }
129
108
 
130
109
  exports.create = create
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ import { type Resolver } from '../Factory'
4
+ import amqp from './amqp'
5
+ import http from './http'
6
+ import type { extensions } from '@toa.io/core'
7
+
8
+ export const protocols: Protocol[] = [http, amqp]
9
+
10
+ export interface Protocol {
11
+ id: ProtocolID
12
+ protocols: string[]
13
+ create: (resolver: Resolver) => extensions.Aspect
14
+ }
15
+
16
+ export type ProtocolID = 'http' | 'amqp'
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./transpiled"
5
+ },
6
+ "include": [
7
+ "source"
8
+ ],
9
+ "exclude": [
10
+ "**/*.test.ts"
11
+ ]
12
+ }
@@ -1,14 +0,0 @@
1
- 'use strict'
2
-
3
- /**
4
- * @param {string} reference
5
- */
6
- function check (reference) {
7
- if (typeof reference !== 'string') return // aspect properties object
8
-
9
- const url = new URL(reference)
10
-
11
- if (url.username !== '' || url.password !== '') throw new Error('Origins must not contain credentials. Please use environment secrets instead.')
12
- }
13
-
14
- exports.check = check
@@ -1,5 +0,0 @@
1
- 'use strict'
2
-
3
- const { uris } = require('./uris')
4
-
5
- exports.uris = uris
@@ -1,37 +0,0 @@
1
- 'use strict'
2
-
3
- const { PREFIX } = require('../constants')
4
-
5
- /**
6
- * @param {toa.norm.context.dependencies.Instance[]} instances
7
- * @param {toa.origins.Annotations} annotations
8
- * @returns {toa.deployment.dependency.Variables}
9
- */
10
- function uris (instances, annotations) {
11
- const variables = {}
12
-
13
- for (const [id, annotation] of Object.entries(annotations)) {
14
- const component = instances.find((instance) => instance.locator.id === id)
15
-
16
- if (component === undefined) throw new Error(`Origins annotations error: component '${id}' is not found`)
17
-
18
- for (const origin of Object.keys(annotation)) {
19
- const properties = origin[0] === '.'
20
-
21
- if (!properties && !(origin in component.manifest)) {
22
- throw new Error(`Origins annotations error: component '${id}' doesn't have '${origin}' origin`)
23
- }
24
- }
25
-
26
- const name = PREFIX + component.locator.uppercase
27
- const json = JSON.stringify(annotation)
28
- const value = btoa(json)
29
- const variable = { name, value }
30
-
31
- variables[component.locator.label] = [variable]
32
- }
33
-
34
- return variables
35
- }
36
-
37
- exports.uris = uris
@@ -1,3 +0,0 @@
1
- 'use strict'
2
-
3
- exports.PROTOCOLS = ['http:', 'https:', 'amqp:', 'amqps:']
@@ -1,20 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { Locator } = require('@toa.io/core')
5
- const { random, sample } = require('@toa.io/generic')
6
-
7
- const { PROTOCOLS } = require('./constants')
8
-
9
- const component = () => ({
10
- locator: new Locator(generate(), generate()),
11
- manifest: { [generate()]: sample(PROTOCOLS) + '//' + generate() }
12
- })
13
-
14
- const components = () => {
15
- const length = random(20) + 10
16
-
17
- return Array.from({ length }, component)
18
- }
19
-
20
- exports.components = components
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
-
5
- /** @type {Record<string, string>} */
6
- const manifest = {
7
- [generate()]: 'https://toa.io',
8
- [generate()]: 'https://api.domain.com',
9
- [generate()]: 'amqp://localhost',
10
- [generate()]: 'amqps://localhost'
11
- }
12
-
13
- exports.manifest = manifest