@toa.io/extensions.exposition 0.20.0-dev.8 → 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 (191) hide show
  1. package/components/context.toa.yaml +15 -0
  2. package/components/identity.bans/manifest.toa.yaml +18 -0
  3. package/components/identity.basic/events/principal.js +9 -0
  4. package/components/identity.basic/manifest.toa.yaml +50 -0
  5. package/components/identity.basic/source/authenticate.ts +29 -0
  6. package/components/identity.basic/source/create.ts +19 -0
  7. package/components/identity.basic/source/transit.ts +64 -0
  8. package/components/identity.basic/source/types.ts +42 -0
  9. package/components/identity.basic/tsconfig.json +9 -0
  10. package/components/identity.roles/manifest.toa.yaml +31 -0
  11. package/components/identity.roles/source/list.ts +7 -0
  12. package/components/identity.roles/source/principal.ts +20 -0
  13. package/components/identity.roles/tsconfig.json +9 -0
  14. package/components/identity.tokens/manifest.toa.yaml +39 -0
  15. package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
  16. package/components/identity.tokens/source/authenticate.test.ts +56 -0
  17. package/components/identity.tokens/source/authenticate.ts +38 -0
  18. package/components/identity.tokens/source/decrypt.test.ts +59 -0
  19. package/components/identity.tokens/source/decrypt.ts +25 -0
  20. package/components/identity.tokens/source/encrypt.test.ts +35 -0
  21. package/components/identity.tokens/source/encrypt.ts +25 -0
  22. package/components/identity.tokens/source/revoke.ts +5 -0
  23. package/components/identity.tokens/source/types.ts +48 -0
  24. package/components/identity.tokens/tsconfig.json +9 -0
  25. package/cucumber.js +9 -0
  26. package/documentation/.assets/ia3-dark.jpg +0 -0
  27. package/documentation/.assets/ia3-light.jpg +0 -0
  28. package/documentation/.assets/overview-dark.jpg +0 -0
  29. package/documentation/.assets/overview-light.jpg +0 -0
  30. package/documentation/.assets/role-scopes-dark.jpg +0 -0
  31. package/documentation/.assets/role-scopes-light.jpg +0 -0
  32. package/documentation/.assets/rtd-dark.jpg +0 -0
  33. package/documentation/.assets/rtd-light.jpg +0 -0
  34. package/documentation/access.md +256 -0
  35. package/documentation/components.md +276 -0
  36. package/documentation/identity.md +156 -0
  37. package/documentation/notes/sse.md +71 -0
  38. package/documentation/protocol.md +18 -0
  39. package/documentation/query.md +226 -0
  40. package/documentation/tree.md +169 -0
  41. package/features/access.feature +448 -0
  42. package/features/annotation.feature +30 -0
  43. package/features/body.feature +45 -0
  44. package/features/directives.feature +56 -0
  45. package/features/dynamic.feature +99 -0
  46. package/features/errors.feature +193 -0
  47. package/features/identity.basic.feature +276 -0
  48. package/features/identity.feature +61 -0
  49. package/features/identity.roles.feature +51 -0
  50. package/features/identity.tokens.feature +119 -0
  51. package/features/queries.feature +214 -0
  52. package/features/routes.feature +49 -0
  53. package/features/steps/Common.ts +10 -0
  54. package/features/steps/Components.ts +43 -0
  55. package/features/steps/Database.ts +58 -0
  56. package/features/steps/Gateway.ts +113 -0
  57. package/features/steps/HTTP.ts +71 -0
  58. package/features/steps/Parameters.ts +12 -0
  59. package/features/steps/Workspace.ts +40 -0
  60. package/features/steps/components/echo/manifest.toa.yaml +9 -0
  61. package/features/steps/components/echo/operations/affect.js +7 -0
  62. package/features/steps/components/echo/operations/compute.js +7 -0
  63. package/features/steps/components/greeter/manifest.toa.yaml +5 -0
  64. package/features/steps/components/greeter/operations/greet.js +7 -0
  65. package/features/steps/components/pots/manifest.toa.yaml +20 -0
  66. package/features/steps/components/sequences/manifest.toa.yaml +10 -0
  67. package/features/steps/components/sequences/operations/numbers.js +7 -0
  68. package/features/steps/components/sequences/operations/tokens.js +16 -0
  69. package/features/steps/components/users/manifest.toa.yaml +11 -0
  70. package/features/steps/tsconfig.json +9 -0
  71. package/features/streams.feature +26 -0
  72. package/package.json +32 -17
  73. package/readme.md +183 -0
  74. package/schemas/annotation.cos.yaml +5 -0
  75. package/schemas/directive.cos.yaml +3 -0
  76. package/schemas/method.cos.yaml +8 -0
  77. package/schemas/node.cos.yaml +5 -0
  78. package/schemas/query.cos.yaml +17 -0
  79. package/schemas/querystring.cos.yaml +5 -0
  80. package/schemas/range.cos.yaml +2 -0
  81. package/schemas/route.cos.yaml +2 -0
  82. package/source/Annotation.ts +7 -0
  83. package/source/Branch.ts +8 -0
  84. package/source/Composition.ts +57 -0
  85. package/source/Context.ts +6 -0
  86. package/source/Directive.test.ts +91 -0
  87. package/source/Directive.ts +120 -0
  88. package/source/Endpoint.ts +59 -0
  89. package/source/Factory.ts +51 -0
  90. package/source/Gateway.ts +93 -0
  91. package/source/HTTP/Server.fixtures.ts +45 -0
  92. package/source/HTTP/Server.test.ts +221 -0
  93. package/source/HTTP/Server.ts +135 -0
  94. package/source/HTTP/exceptions.ts +77 -0
  95. package/source/HTTP/formats/index.ts +19 -0
  96. package/source/HTTP/formats/json.ts +13 -0
  97. package/source/HTTP/formats/msgpack.ts +10 -0
  98. package/source/HTTP/formats/text.ts +9 -0
  99. package/source/HTTP/formats/yaml.ts +14 -0
  100. package/source/HTTP/index.ts +3 -0
  101. package/source/HTTP/messages.test.ts +116 -0
  102. package/source/HTTP/messages.ts +89 -0
  103. package/source/Mapping.ts +51 -0
  104. package/source/Query.test.ts +37 -0
  105. package/source/Query.ts +105 -0
  106. package/source/RTD/Context.ts +16 -0
  107. package/source/RTD/Directives.ts +9 -0
  108. package/source/RTD/Endpoint.ts +11 -0
  109. package/source/RTD/Match.ts +16 -0
  110. package/source/RTD/Method.ts +24 -0
  111. package/source/RTD/Node.ts +85 -0
  112. package/source/RTD/Route.ts +59 -0
  113. package/source/RTD/Tree.ts +54 -0
  114. package/source/RTD/factory.ts +47 -0
  115. package/source/RTD/index.ts +8 -0
  116. package/source/RTD/segment.test.ts +32 -0
  117. package/source/RTD/segment.ts +25 -0
  118. package/source/RTD/syntax/index.ts +2 -0
  119. package/source/RTD/syntax/parse.test.ts +188 -0
  120. package/source/RTD/syntax/parse.ts +153 -0
  121. package/source/RTD/syntax/types.ts +48 -0
  122. package/source/Remotes.test.ts +42 -0
  123. package/source/Remotes.ts +22 -0
  124. package/source/Tenant.ts +38 -0
  125. package/source/deployment.ts +49 -0
  126. package/source/directives/auth/Anonymous.ts +14 -0
  127. package/source/directives/auth/Echo.ts +12 -0
  128. package/source/directives/auth/Family.ts +145 -0
  129. package/source/directives/auth/Id.ts +19 -0
  130. package/source/directives/auth/Incept.ts +42 -0
  131. package/source/directives/auth/Role.test.ts +62 -0
  132. package/source/directives/auth/Role.ts +56 -0
  133. package/source/directives/auth/Rule.ts +28 -0
  134. package/source/directives/auth/Scheme.ts +26 -0
  135. package/source/directives/auth/index.ts +3 -0
  136. package/source/directives/auth/schemes.ts +8 -0
  137. package/source/directives/auth/split.ts +15 -0
  138. package/source/directives/auth/types.ts +37 -0
  139. package/source/directives/dev/Family.ts +36 -0
  140. package/source/directives/dev/Stub.ts +14 -0
  141. package/source/directives/dev/Throw.ts +14 -0
  142. package/source/directives/dev/index.ts +3 -0
  143. package/source/directives/dev/types.ts +5 -0
  144. package/source/directives/index.ts +5 -0
  145. package/source/discovery.ts +1 -0
  146. package/source/exceptions.ts +17 -0
  147. package/source/index.test.ts +9 -0
  148. package/source/index.ts +6 -0
  149. package/source/manifest.test.ts +59 -0
  150. package/source/manifest.ts +48 -0
  151. package/source/root.ts +38 -0
  152. package/source/schemas.ts +9 -0
  153. package/tsconfig.json +12 -0
  154. package/src/.manifest/index.js +0 -7
  155. package/src/.manifest/normalize.js +0 -58
  156. package/src/.manifest/schema.yaml +0 -71
  157. package/src/.manifest/validate.js +0 -17
  158. package/src/constants.js +0 -3
  159. package/src/deployment.js +0 -23
  160. package/src/exposition.js +0 -68
  161. package/src/factory.js +0 -76
  162. package/src/index.js +0 -9
  163. package/src/manifest.js +0 -12
  164. package/src/query/criteria.js +0 -55
  165. package/src/query/enum.js +0 -35
  166. package/src/query/index.js +0 -17
  167. package/src/query/query.js +0 -60
  168. package/src/query/range.js +0 -28
  169. package/src/query/sort.js +0 -19
  170. package/src/remote.js +0 -88
  171. package/src/server.js +0 -83
  172. package/src/tenant.js +0 -29
  173. package/src/translate/etag.js +0 -14
  174. package/src/translate/index.js +0 -7
  175. package/src/translate/request.js +0 -68
  176. package/src/translate/response.js +0 -62
  177. package/src/tree.js +0 -109
  178. package/test/manifest.normalize.fixtures.js +0 -37
  179. package/test/manifest.normalize.test.js +0 -37
  180. package/test/manifest.validate.test.js +0 -40
  181. package/test/query.range.test.js +0 -18
  182. package/test/tree.fixtures.js +0 -21
  183. package/test/tree.test.js +0 -44
  184. package/types/annotations.d.ts +0 -10
  185. package/types/declarations.d.ts +0 -31
  186. package/types/exposition.d.ts +0 -13
  187. package/types/http.d.ts +0 -13
  188. package/types/query.d.ts +0 -16
  189. package/types/remote.d.ts +0 -19
  190. package/types/server.d.ts +0 -13
  191. 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,8 @@
1
+ export * from './Tree'
2
+ export * from './Node'
3
+ export * from './Match'
4
+ export * from './Method'
5
+ export * from './Context'
6
+ export * from './Endpoint'
7
+ export * from './Directives'
8
+ export * as syntax from './syntax'
@@ -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,2 @@
1
+ export * from './parse'
2
+ export * from './types'
@@ -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
+ }
@@ -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>