@toa.io/extensions.exposition 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.
- package/components/context.toa.yaml +15 -0
- package/components/identity.bans/manifest.toa.yaml +18 -0
- package/components/identity.basic/events/principal.js +9 -0
- package/components/identity.basic/manifest.toa.yaml +50 -0
- package/components/identity.basic/source/authenticate.ts +29 -0
- package/components/identity.basic/source/create.ts +19 -0
- package/components/identity.basic/source/transit.ts +64 -0
- package/components/identity.basic/source/types.ts +42 -0
- package/components/identity.basic/tsconfig.json +9 -0
- package/components/identity.roles/manifest.toa.yaml +31 -0
- package/components/identity.roles/source/list.ts +7 -0
- package/components/identity.roles/source/principal.ts +20 -0
- package/components/identity.roles/tsconfig.json +9 -0
- package/components/identity.tokens/manifest.toa.yaml +39 -0
- package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
- package/components/identity.tokens/source/authenticate.test.ts +56 -0
- package/components/identity.tokens/source/authenticate.ts +38 -0
- package/components/identity.tokens/source/decrypt.test.ts +59 -0
- package/components/identity.tokens/source/decrypt.ts +25 -0
- package/components/identity.tokens/source/encrypt.test.ts +35 -0
- package/components/identity.tokens/source/encrypt.ts +25 -0
- package/components/identity.tokens/source/revoke.ts +5 -0
- package/components/identity.tokens/source/types.ts +48 -0
- package/components/identity.tokens/tsconfig.json +9 -0
- package/cucumber.js +9 -0
- package/documentation/.assets/ia3-dark.jpg +0 -0
- package/documentation/.assets/ia3-light.jpg +0 -0
- package/documentation/.assets/overview-dark.jpg +0 -0
- package/documentation/.assets/overview-light.jpg +0 -0
- package/documentation/.assets/role-scopes-dark.jpg +0 -0
- package/documentation/.assets/role-scopes-light.jpg +0 -0
- package/documentation/.assets/rtd-dark.jpg +0 -0
- package/documentation/.assets/rtd-light.jpg +0 -0
- package/documentation/access.md +256 -0
- package/documentation/components.md +276 -0
- package/documentation/identity.md +156 -0
- package/documentation/notes/sse.md +71 -0
- package/documentation/protocol.md +18 -0
- package/documentation/query.md +226 -0
- package/documentation/tree.md +169 -0
- package/features/access.feature +448 -0
- package/features/annotation.feature +30 -0
- package/features/body.feature +45 -0
- package/features/directives.feature +56 -0
- package/features/dynamic.feature +99 -0
- package/features/errors.feature +193 -0
- package/features/identity.basic.feature +276 -0
- package/features/identity.feature +61 -0
- package/features/identity.roles.feature +51 -0
- package/features/identity.tokens.feature +119 -0
- package/features/queries.feature +214 -0
- package/features/routes.feature +49 -0
- package/features/steps/Common.ts +10 -0
- package/features/steps/Components.ts +43 -0
- package/features/steps/Database.ts +58 -0
- package/features/steps/Gateway.ts +113 -0
- package/features/steps/HTTP.ts +71 -0
- package/features/steps/Parameters.ts +12 -0
- package/features/steps/Workspace.ts +40 -0
- package/features/steps/components/echo/manifest.toa.yaml +9 -0
- package/features/steps/components/echo/operations/affect.js +7 -0
- package/features/steps/components/echo/operations/compute.js +7 -0
- package/features/steps/components/greeter/manifest.toa.yaml +5 -0
- package/features/steps/components/greeter/operations/greet.js +7 -0
- package/features/steps/components/pots/manifest.toa.yaml +20 -0
- package/features/steps/components/sequences/manifest.toa.yaml +10 -0
- package/features/steps/components/sequences/operations/numbers.js +7 -0
- package/features/steps/components/sequences/operations/tokens.js +16 -0
- package/features/steps/components/users/manifest.toa.yaml +11 -0
- package/features/steps/tsconfig.json +9 -0
- package/features/streams.feature +26 -0
- package/package.json +32 -17
- package/readme.md +183 -0
- package/schemas/annotation.cos.yaml +5 -0
- package/schemas/directive.cos.yaml +3 -0
- package/schemas/method.cos.yaml +8 -0
- package/schemas/node.cos.yaml +5 -0
- package/schemas/query.cos.yaml +17 -0
- package/schemas/querystring.cos.yaml +5 -0
- package/schemas/range.cos.yaml +2 -0
- package/schemas/route.cos.yaml +2 -0
- package/source/Annotation.ts +7 -0
- package/source/Branch.ts +8 -0
- package/source/Composition.ts +57 -0
- package/source/Context.ts +6 -0
- package/source/Directive.test.ts +91 -0
- package/source/Directive.ts +120 -0
- package/source/Endpoint.ts +59 -0
- package/source/Factory.ts +51 -0
- package/source/Gateway.ts +93 -0
- package/source/HTTP/Server.fixtures.ts +45 -0
- package/source/HTTP/Server.test.ts +221 -0
- package/source/HTTP/Server.ts +135 -0
- package/source/HTTP/exceptions.ts +77 -0
- package/source/HTTP/formats/index.ts +19 -0
- package/source/HTTP/formats/json.ts +13 -0
- package/source/HTTP/formats/msgpack.ts +10 -0
- package/source/HTTP/formats/text.ts +9 -0
- package/source/HTTP/formats/yaml.ts +14 -0
- package/source/HTTP/index.ts +3 -0
- package/source/HTTP/messages.test.ts +116 -0
- package/source/HTTP/messages.ts +89 -0
- package/source/Mapping.ts +51 -0
- package/source/Query.test.ts +37 -0
- package/source/Query.ts +105 -0
- package/source/RTD/Context.ts +16 -0
- package/source/RTD/Directives.ts +9 -0
- package/source/RTD/Endpoint.ts +11 -0
- package/source/RTD/Match.ts +16 -0
- package/source/RTD/Method.ts +24 -0
- package/source/RTD/Node.ts +85 -0
- package/source/RTD/Route.ts +59 -0
- package/source/RTD/Tree.ts +54 -0
- package/source/RTD/factory.ts +47 -0
- package/source/RTD/index.ts +8 -0
- package/source/RTD/segment.test.ts +32 -0
- package/source/RTD/segment.ts +25 -0
- package/source/RTD/syntax/index.ts +2 -0
- package/source/RTD/syntax/parse.test.ts +188 -0
- package/source/RTD/syntax/parse.ts +153 -0
- package/source/RTD/syntax/types.ts +48 -0
- package/source/Remotes.test.ts +42 -0
- package/source/Remotes.ts +22 -0
- package/source/Tenant.ts +38 -0
- package/source/deployment.ts +49 -0
- package/source/directives/auth/Anonymous.ts +14 -0
- package/source/directives/auth/Echo.ts +12 -0
- package/source/directives/auth/Family.ts +145 -0
- package/source/directives/auth/Id.ts +19 -0
- package/source/directives/auth/Incept.ts +42 -0
- package/source/directives/auth/Role.test.ts +62 -0
- package/source/directives/auth/Role.ts +56 -0
- package/source/directives/auth/Rule.ts +28 -0
- package/source/directives/auth/Scheme.ts +26 -0
- package/source/directives/auth/index.ts +3 -0
- package/source/directives/auth/schemes.ts +8 -0
- package/source/directives/auth/split.ts +15 -0
- package/source/directives/auth/types.ts +37 -0
- package/source/directives/dev/Family.ts +36 -0
- package/source/directives/dev/Stub.ts +14 -0
- package/source/directives/dev/Throw.ts +14 -0
- package/source/directives/dev/index.ts +3 -0
- package/source/directives/dev/types.ts +5 -0
- package/source/directives/index.ts +5 -0
- package/source/discovery.ts +1 -0
- package/source/exceptions.ts +17 -0
- package/source/index.test.ts +9 -0
- package/source/index.ts +6 -0
- package/source/manifest.test.ts +59 -0
- package/source/manifest.ts +48 -0
- package/source/root.ts +38 -0
- package/source/schemas.ts +9 -0
- package/tsconfig.json +12 -0
- package/src/.manifest/index.js +0 -7
- package/src/.manifest/normalize.js +0 -58
- package/src/.manifest/schema.yaml +0 -71
- package/src/.manifest/validate.js +0 -17
- package/src/constants.js +0 -3
- package/src/deployment.js +0 -23
- package/src/exposition.js +0 -68
- package/src/factory.js +0 -76
- package/src/index.js +0 -9
- package/src/manifest.js +0 -12
- package/src/query/criteria.js +0 -55
- package/src/query/enum.js +0 -35
- package/src/query/index.js +0 -17
- package/src/query/query.js +0 -60
- package/src/query/range.js +0 -28
- package/src/query/sort.js +0 -19
- package/src/remote.js +0 -88
- package/src/server.js +0 -83
- package/src/tenant.js +0 -29
- package/src/translate/etag.js +0 -14
- package/src/translate/index.js +0 -7
- package/src/translate/request.js +0 -68
- package/src/translate/response.js +0 -62
- package/src/tree.js +0 -109
- package/test/manifest.normalize.fixtures.js +0 -37
- package/test/manifest.normalize.test.js +0 -37
- package/test/manifest.validate.test.js +0 -40
- package/test/query.range.test.js +0 -18
- package/test/tree.fixtures.js +0 -21
- package/test/tree.test.js +0 -44
- package/types/annotations.d.ts +0 -10
- package/types/declarations.d.ts +0 -31
- package/types/exposition.d.ts +0 -13
- package/types/http.d.ts +0 -13
- package/types/query.d.ts +0 -16
- package/types/remote.d.ts +0 -19
- package/types/server.d.ts +0 -13
- package/types/tree.d.ts +0 -33
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Node } from './Node'
|
|
2
|
+
import { createNode } from './factory'
|
|
3
|
+
import { fragment } from './segment'
|
|
4
|
+
import { type Match } from './Match'
|
|
5
|
+
import { type Context } from './Context'
|
|
6
|
+
import { type Directives, type DirectivesFactory } from './Directives'
|
|
7
|
+
import { type Endpoint, type EndpointsFactory } from './Endpoint'
|
|
8
|
+
import type * as syntax from './syntax'
|
|
9
|
+
|
|
10
|
+
export class Tree<
|
|
11
|
+
TEndpoint extends Endpoint<TEndpoint> = any,
|
|
12
|
+
TDirectives extends Directives<TDirectives> = any
|
|
13
|
+
> {
|
|
14
|
+
private readonly root: syntax.Node
|
|
15
|
+
private readonly trunk: Node<TEndpoint, TDirectives>
|
|
16
|
+
private readonly endpoints: EndpointsFactory<TEndpoint>
|
|
17
|
+
private readonly directives: DirectivesFactory
|
|
18
|
+
|
|
19
|
+
public constructor
|
|
20
|
+
(node: syntax.Node, endpoints: EndpointsFactory, directives: DirectivesFactory) {
|
|
21
|
+
this.endpoints = endpoints
|
|
22
|
+
this.directives = directives
|
|
23
|
+
this.trunk = this.createNode(node, PROTECTED)
|
|
24
|
+
this.root = node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public match (path: string): Match<TEndpoint, TDirectives> | null {
|
|
28
|
+
const fragments = fragment(path)
|
|
29
|
+
|
|
30
|
+
return this.trunk.match(fragments)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public merge (node: syntax.Node, extension: any): void {
|
|
34
|
+
const branch = this.createNode(node, !PROTECTED, extension)
|
|
35
|
+
|
|
36
|
+
this.trunk.merge(branch)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private createNode (node: syntax.Node, protect: boolean, extension?: any): Node {
|
|
40
|
+
const context: Context = {
|
|
41
|
+
protected: protect,
|
|
42
|
+
endpoints: this.endpoints,
|
|
43
|
+
directives: {
|
|
44
|
+
factory: this.directives,
|
|
45
|
+
stack: this.root?.directives ?? []
|
|
46
|
+
},
|
|
47
|
+
extension
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return createNode(node, context)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const PROTECTED = true
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Node, type Properties } from './Node'
|
|
2
|
+
import { Route } from './Route'
|
|
3
|
+
import { type Context } from './Context'
|
|
4
|
+
import { segment } from './segment'
|
|
5
|
+
import { Method, type Methods } from './Method'
|
|
6
|
+
import { type Endpoint } from './Endpoint'
|
|
7
|
+
import { type Directives } from './Directives'
|
|
8
|
+
import type * as syntax from './syntax'
|
|
9
|
+
|
|
10
|
+
export function createNode<TEndpoint extends Endpoint, TDirectives extends Directives>
|
|
11
|
+
(node: syntax.Node, context: Context): Node<TEndpoint, TDirectives> {
|
|
12
|
+
if (node.isolated === true)
|
|
13
|
+
context.directives.stack = node.directives
|
|
14
|
+
else
|
|
15
|
+
context.directives.stack = node.directives.concat(context.directives.stack)
|
|
16
|
+
|
|
17
|
+
const routes: Route[] = node.routes.map((route) => createRoute(route, context))
|
|
18
|
+
const methods: Methods = {}
|
|
19
|
+
|
|
20
|
+
for (const method of node.methods)
|
|
21
|
+
methods[method.verb] = createMethod(method, context)
|
|
22
|
+
|
|
23
|
+
const properties: Properties = { protected: node.protected ?? context.protected }
|
|
24
|
+
|
|
25
|
+
return new Node(routes, methods, properties)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createRoute (route: syntax.Route, context: Context): Route {
|
|
29
|
+
const stack = context.directives.stack.slice()
|
|
30
|
+
const segments = segment(route.path)
|
|
31
|
+
const node = createNode(route.node, context)
|
|
32
|
+
|
|
33
|
+
context.directives.stack = stack // restore
|
|
34
|
+
|
|
35
|
+
return new Route(segments, node)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createMethod (method: syntax.Method, context: Context): Method {
|
|
39
|
+
const stack = context.directives.stack.concat(method.directives.reverse())
|
|
40
|
+
const directives = context.directives.factory.create(stack)
|
|
41
|
+
|
|
42
|
+
const endpoint = method.mapping?.endpoint === undefined
|
|
43
|
+
? null
|
|
44
|
+
: context.endpoints.create(method, context)
|
|
45
|
+
|
|
46
|
+
return new Method(endpoint, directives)
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { segment, fragment } from './segment'
|
|
2
|
+
|
|
3
|
+
it('should return segments', async () => {
|
|
4
|
+
const segments = segment('/foo/bar/')
|
|
5
|
+
|
|
6
|
+
expect(segments).toHaveLength(2)
|
|
7
|
+
expect(segments[0].fragment).toBe('foo')
|
|
8
|
+
expect(segments[1].fragment).toBe('bar')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should parse placeholders', async () => {
|
|
12
|
+
const segments = segment('/foo/:id/')
|
|
13
|
+
|
|
14
|
+
expect(segments).toHaveLength(2)
|
|
15
|
+
expect(segments[0].fragment).toBe('foo')
|
|
16
|
+
expect(segments[1].fragment).toBeNull()
|
|
17
|
+
|
|
18
|
+
// helping typescript
|
|
19
|
+
if (segments[1].fragment !== null) throw new Error('?')
|
|
20
|
+
|
|
21
|
+
expect(segments[1].placeholder).toBe('id')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should handle root path', async () => {
|
|
25
|
+
expect(segment('/')).toStrictEqual([])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should split', async () => {
|
|
29
|
+
const parts = fragment('/foo/bar/')
|
|
30
|
+
|
|
31
|
+
expect(parts).toStrictEqual(['foo', 'bar'])
|
|
32
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function segment (path: string): Segment[] {
|
|
2
|
+
return fragment(path).map(parse)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function fragment (path: string): string[] {
|
|
6
|
+
const parts = path.split('/')
|
|
7
|
+
|
|
8
|
+
// trailing slash
|
|
9
|
+
if (parts[parts.length - 1] === '') parts.length--
|
|
10
|
+
|
|
11
|
+
// leading slash
|
|
12
|
+
return parts.splice(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parse (segment: string): Segment {
|
|
16
|
+
if (segment[0] === ':') return { fragment: null, placeholder: segment.substring(1) }
|
|
17
|
+
else return { fragment: segment }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Segment = {
|
|
21
|
+
fragment: string
|
|
22
|
+
} | {
|
|
23
|
+
fragment: null
|
|
24
|
+
placeholder: string
|
|
25
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { parse } from './parse'
|
|
2
|
+
|
|
3
|
+
describe('routes', () => {
|
|
4
|
+
it('should parse route', async () => {
|
|
5
|
+
const declaration = {
|
|
6
|
+
'/': {},
|
|
7
|
+
'/foo': {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const node = parse(declaration)
|
|
11
|
+
|
|
12
|
+
expect(node.routes).toHaveLength(2)
|
|
13
|
+
expect(node.routes[0].path).toBe('/')
|
|
14
|
+
expect(node.routes[1].path).toBe('/foo')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should parse nested routes', async () => {
|
|
18
|
+
const declaration = {
|
|
19
|
+
'/': {
|
|
20
|
+
'/foo': {},
|
|
21
|
+
'/bar': {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const node = parse(declaration)
|
|
26
|
+
const root = node.routes[0].node
|
|
27
|
+
|
|
28
|
+
expect(root.routes).toHaveLength(2)
|
|
29
|
+
expect(root.routes[0].path).toBe('/foo')
|
|
30
|
+
expect(root.routes[1].path).toBe('/bar')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('methods', () => {
|
|
35
|
+
it('should parse methods', () => {
|
|
36
|
+
const declaration = {
|
|
37
|
+
'/': {
|
|
38
|
+
GET: {
|
|
39
|
+
endpoint: 'observe'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const node = parse(declaration)
|
|
45
|
+
const root = node.routes[0].node
|
|
46
|
+
|
|
47
|
+
expect(root.methods).toHaveLength(1)
|
|
48
|
+
expect(root.methods[0].verb).toBe('GET')
|
|
49
|
+
expect(root.methods[0].mapping).toMatchObject({ endpoint: 'observe' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should parse endpoint shortcut', async () => {
|
|
53
|
+
const declaration = {
|
|
54
|
+
'/': {
|
|
55
|
+
GET: 'observe'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const node = parse(declaration)
|
|
60
|
+
const root = node.routes[0].node
|
|
61
|
+
|
|
62
|
+
expect(root.methods).toHaveLength(1)
|
|
63
|
+
expect(root.methods[0].verb).toBe('GET')
|
|
64
|
+
expect(root.methods[0].mapping).toMatchObject({ endpoint: 'observe' })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should parse fq endpoint', async () => {
|
|
68
|
+
const declaration = {
|
|
69
|
+
'/': {
|
|
70
|
+
GET: 'dummies.dummy.observe'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const node = parse(declaration)
|
|
75
|
+
const root = node.routes[0].node
|
|
76
|
+
|
|
77
|
+
expect(root.methods).toHaveLength(1)
|
|
78
|
+
expect(root.methods[0].verb).toBe('GET')
|
|
79
|
+
|
|
80
|
+
expect(root.methods[0].mapping).toMatchObject({
|
|
81
|
+
namespace: 'dummies',
|
|
82
|
+
component: 'dummy',
|
|
83
|
+
endpoint: 'observe'
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should parse fq endpoint within default namespace', async () => {
|
|
88
|
+
const declaration = {
|
|
89
|
+
'/': {
|
|
90
|
+
GET: 'dummy.observe'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const node = parse(declaration)
|
|
95
|
+
const root = node.routes[0].node
|
|
96
|
+
|
|
97
|
+
expect(root.methods).toHaveLength(1)
|
|
98
|
+
expect(root.methods[0].verb).toBe('GET')
|
|
99
|
+
|
|
100
|
+
expect(root.methods[0].mapping).toMatchObject({
|
|
101
|
+
namespace: 'default',
|
|
102
|
+
component: 'dummy',
|
|
103
|
+
endpoint: 'observe'
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should parse directives', async () => {
|
|
108
|
+
const declaration = {
|
|
109
|
+
'/': {
|
|
110
|
+
GET: {
|
|
111
|
+
'auth:incept': 'id',
|
|
112
|
+
endpoint: 'observe'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const node = parse(declaration)
|
|
118
|
+
const root = node.routes[0].node
|
|
119
|
+
|
|
120
|
+
expect(root.methods[0].directives)
|
|
121
|
+
.toStrictEqual([{ family: 'auth', name: 'incept', value: 'id' }])
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('directives', () => {
|
|
126
|
+
it('should parse shortcuts', async () => {
|
|
127
|
+
const declaration = {
|
|
128
|
+
'/': {
|
|
129
|
+
foo: 'baz'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const shortcuts = new Map<string, string>([
|
|
134
|
+
['foo', 'dev:foo']
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
const node = parse(declaration, shortcuts)
|
|
138
|
+
const root = node.routes[0].node
|
|
139
|
+
|
|
140
|
+
expect(root.directives).toHaveLength(1)
|
|
141
|
+
expect(root.directives[0].family).toBe('dev')
|
|
142
|
+
expect(root.directives[0].name).toBe('foo')
|
|
143
|
+
expect(root.directives[0].value).toBe('baz')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('validation', () => {
|
|
148
|
+
it('should throw on unknown key', async () => {
|
|
149
|
+
const declaration = { hello: 'world' }
|
|
150
|
+
|
|
151
|
+
expect(() => parse(declaration)).toThrow('RTD parse error: unknown key \'hello\'.')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should throw on invalid mapping', async () => {
|
|
155
|
+
const declaration = {
|
|
156
|
+
'/': {
|
|
157
|
+
GET: {
|
|
158
|
+
endpoint: 'observe',
|
|
159
|
+
hello: 'world'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
expect(() => parse(declaration)).toThrow('/methods/0/mapping')
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should expand ranges', async () => {
|
|
169
|
+
const declaration = {
|
|
170
|
+
'/': {
|
|
171
|
+
GET: {
|
|
172
|
+
endpoint: 'enumerate',
|
|
173
|
+
query: {
|
|
174
|
+
omit: 3,
|
|
175
|
+
limit: 2
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const node = parse(declaration)
|
|
182
|
+
const query = node.routes[0].node.methods[0].mapping?.query
|
|
183
|
+
|
|
184
|
+
expect(query).toMatchObject({
|
|
185
|
+
omit: { value: 3, range: [3, 3] },
|
|
186
|
+
limit: { value: 2, range: [2, 2] }
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as schemas from '../../schemas'
|
|
2
|
+
import {
|
|
3
|
+
verbs,
|
|
4
|
+
type Node,
|
|
5
|
+
type Route,
|
|
6
|
+
type Method,
|
|
7
|
+
type Mapping,
|
|
8
|
+
type Directive, type Range
|
|
9
|
+
} from './types'
|
|
10
|
+
|
|
11
|
+
export function parse (input: object, shortcuts?: Shortcuts): Node {
|
|
12
|
+
const node = parseNode(input, shortcuts)
|
|
13
|
+
|
|
14
|
+
schemas.node.validate(node)
|
|
15
|
+
|
|
16
|
+
return node
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseNode (input: object, shortcuts?: Shortcuts): Node {
|
|
20
|
+
const node = createNode()
|
|
21
|
+
|
|
22
|
+
for (const [key, value] of Object.entries(input)) {
|
|
23
|
+
if (PROPERTIES.includes(key as keyof Node)) {
|
|
24
|
+
node[key as keyof Node] = value
|
|
25
|
+
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (key[0] === '/') {
|
|
30
|
+
const route = parseRoute(key, value, shortcuts)
|
|
31
|
+
|
|
32
|
+
node.routes.push(route)
|
|
33
|
+
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (verbs.has(key)) {
|
|
38
|
+
const method = parseMethod(key, value, shortcuts)
|
|
39
|
+
|
|
40
|
+
node.methods.push(method)
|
|
41
|
+
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const directive = parseDirective(key, value, shortcuts)
|
|
46
|
+
|
|
47
|
+
if (directive !== null) {
|
|
48
|
+
node.directives.push(directive)
|
|
49
|
+
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error(`RTD parse error: unknown key '${key}'.`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return node
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createNode (): Node {
|
|
60
|
+
return {
|
|
61
|
+
routes: [],
|
|
62
|
+
methods: [],
|
|
63
|
+
directives: []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseRoute (path: string, value: object, shortcuts?: Shortcuts): Route {
|
|
68
|
+
const node = parse(value, shortcuts)
|
|
69
|
+
|
|
70
|
+
return createRoute(path, node)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createRoute (path: string, node: Node): Route {
|
|
74
|
+
return { path, node }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseMethod (verb: string, value: Mapping | string, shortcuts?: Shortcuts): Method {
|
|
78
|
+
const mapping = typeof value === 'string' ? { endpoint: value } : value
|
|
79
|
+
|
|
80
|
+
parseEndpoint(mapping)
|
|
81
|
+
parseQuery(mapping)
|
|
82
|
+
|
|
83
|
+
const directives = parseDirectives(mapping, shortcuts)
|
|
84
|
+
|
|
85
|
+
return { verb, mapping, directives }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseEndpoint (mapping: Mapping): void {
|
|
89
|
+
if (mapping.endpoint === undefined)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
const [endpoiont, component, namespace] = mapping.endpoint.split('.').reverse()
|
|
93
|
+
|
|
94
|
+
if (component !== undefined) {
|
|
95
|
+
mapping.component = component
|
|
96
|
+
mapping.namespace = namespace ?? mapping.namespace ?? 'default'
|
|
97
|
+
mapping.endpoint = endpoiont
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseQuery (mapping: any): void {
|
|
102
|
+
const query = mapping.query
|
|
103
|
+
|
|
104
|
+
if (query === undefined || query === null)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
if (typeof query.limit === 'number')
|
|
108
|
+
query.limit = expandRange(query.limit)
|
|
109
|
+
|
|
110
|
+
if (typeof query.omit === 'number')
|
|
111
|
+
query.omit = expandRange(query.omit)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseDirectives (mapping: Record<string, any>, shortcuts?: Shortcuts): Directive[] {
|
|
115
|
+
const directives: Directive[] = []
|
|
116
|
+
|
|
117
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
118
|
+
const directive = parseDirective(key, value, shortcuts)
|
|
119
|
+
|
|
120
|
+
if (directive === null)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
directives.push(directive)
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
126
|
+
delete mapping[key]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return directives
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseDirective (key: string, value: any, shortcuts?: Shortcuts): Directive | null {
|
|
133
|
+
if (shortcuts?.has(key) === true)
|
|
134
|
+
key = shortcuts.get(key) as string
|
|
135
|
+
|
|
136
|
+
const match = key.match(DIRECTIVE_RX)
|
|
137
|
+
|
|
138
|
+
if (match === null)
|
|
139
|
+
return null
|
|
140
|
+
|
|
141
|
+
const { family, name } = match.groups as { family: string, name: string }
|
|
142
|
+
|
|
143
|
+
return { family, name, value }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function expandRange (range: number): Range {
|
|
147
|
+
return { value: range, range: [range, range] }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const DIRECTIVE_RX = /^(?<family>\w{1,32}):(?<name>\w{1,32})$/
|
|
151
|
+
const PROPERTIES: Array<keyof Node> = ['protected', 'isolated']
|
|
152
|
+
|
|
153
|
+
export type Shortcuts = Map<string, string>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface Node {
|
|
2
|
+
protected?: boolean
|
|
3
|
+
isolated?: boolean
|
|
4
|
+
routes: Route[]
|
|
5
|
+
methods: Method[]
|
|
6
|
+
directives: Directive[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Route {
|
|
10
|
+
path: string
|
|
11
|
+
node: Node
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Method {
|
|
15
|
+
verb: string
|
|
16
|
+
mapping?: Mapping
|
|
17
|
+
directives: Directive[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Directive {
|
|
21
|
+
family: string
|
|
22
|
+
name: string
|
|
23
|
+
value: any
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Mapping {
|
|
27
|
+
namespace?: string
|
|
28
|
+
component?: string
|
|
29
|
+
endpoint: string
|
|
30
|
+
query?: Query
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Query {
|
|
34
|
+
id?: string
|
|
35
|
+
criteria?: string
|
|
36
|
+
sort?: string
|
|
37
|
+
omit: Range
|
|
38
|
+
limit: Range
|
|
39
|
+
selectors?: string[]
|
|
40
|
+
projection?: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Range {
|
|
44
|
+
value?: number
|
|
45
|
+
range: [number, number]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const verbs = new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { generate } from 'randomstring'
|
|
2
|
+
import { Connector } from '@toa.io/core'
|
|
3
|
+
import { Remotes } from './Remotes'
|
|
4
|
+
import type { Bootloader } from './Factory'
|
|
5
|
+
import type { Locator } from '@toa.io/core'
|
|
6
|
+
|
|
7
|
+
jest.mock('@toa.io/boot', () => ({
|
|
8
|
+
remote: async (locator: Locator) => await boot.remote(locator)
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
const boot = {
|
|
12
|
+
remote: jest.fn(async (..._) => ({
|
|
13
|
+
connect: jest.fn(() => undefined),
|
|
14
|
+
link: jest.fn(() => undefined)
|
|
15
|
+
}))
|
|
16
|
+
} as unknown as jest.MockedObjectDeep<Bootloader>
|
|
17
|
+
|
|
18
|
+
const namespace = generate()
|
|
19
|
+
const name = generate()
|
|
20
|
+
|
|
21
|
+
let remotes: Remotes
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
remotes = new Remotes(boot)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should create remote', async () => {
|
|
28
|
+
const remote = await remotes.discover(namespace, name)
|
|
29
|
+
|
|
30
|
+
expect(boot.remote).toHaveBeenCalledWith(expect.objectContaining({ namespace, name }))
|
|
31
|
+
expect(remote).toStrictEqual(await boot.remote.mock.results[0].value)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should be instance of Connector', async () => {
|
|
35
|
+
expect(remotes).toBeInstanceOf(Connector)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should depend on created remotes', async () => {
|
|
39
|
+
const remote = await remotes.discover(namespace, name)
|
|
40
|
+
|
|
41
|
+
expect(remote.link).toHaveBeenCalledWith(remotes)
|
|
42
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Locator, Connector, type Component } from '@toa.io/core'
|
|
2
|
+
import { type Bootloader } from './Factory'
|
|
3
|
+
|
|
4
|
+
export class Remotes extends Connector {
|
|
5
|
+
private readonly boot: Bootloader
|
|
6
|
+
|
|
7
|
+
public constructor (boot: Bootloader) {
|
|
8
|
+
super()
|
|
9
|
+
this.boot = boot
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async discover (namespace: string, name: string): Promise<Component> {
|
|
13
|
+
const locator = new Locator(name, namespace)
|
|
14
|
+
const remote = await this.boot.remote(locator)
|
|
15
|
+
|
|
16
|
+
this.depends(remote)
|
|
17
|
+
|
|
18
|
+
await remote.connect()
|
|
19
|
+
|
|
20
|
+
return remote
|
|
21
|
+
}
|
|
22
|
+
}
|
package/source/Tenant.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Connector, type Locator, type bindings } from '@toa.io/core'
|
|
2
|
+
import { type Label } from './discovery'
|
|
3
|
+
import { type Branch } from './Branch'
|
|
4
|
+
import type * as RTD from './RTD/syntax'
|
|
5
|
+
|
|
6
|
+
export class Tenant extends Connector {
|
|
7
|
+
private readonly broadcast: Broadcast
|
|
8
|
+
private readonly branch: Branch
|
|
9
|
+
|
|
10
|
+
public constructor (broadcast: Broadcast, locator: Locator, node: RTD.Node) {
|
|
11
|
+
super()
|
|
12
|
+
|
|
13
|
+
this.broadcast = broadcast
|
|
14
|
+
|
|
15
|
+
this.branch = {
|
|
16
|
+
namespace: locator.namespace,
|
|
17
|
+
component: locator.name,
|
|
18
|
+
isolated: locator.namespace === 'identity',
|
|
19
|
+
node
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.depends(broadcast)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public override async open (): Promise<void> {
|
|
26
|
+
await this.expose()
|
|
27
|
+
await this.broadcast.receive('ping', this.expose.bind(this))
|
|
28
|
+
|
|
29
|
+
console.info('Exposition Tenant for ' +
|
|
30
|
+
`'${this.branch.namespace}.${this.branch.component}' has started.`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async expose (): Promise<void> {
|
|
34
|
+
await this.broadcast.transmit('expose', this.branch)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Broadcast = bindings.Broadcast<Label>
|