@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.
- 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,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { randomBytes } = require('node:crypto')
|
|
4
|
+
|
|
5
|
+
async function * computation () {
|
|
6
|
+
while (true) {
|
|
7
|
+
await timeout(Math.floor(Math.random() * 100) + 10)
|
|
8
|
+
yield randomBytes(4).toString('hex')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function timeout (ms) {
|
|
13
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
exports.computation = computation
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Feature: Reply streams
|
|
2
|
+
|
|
3
|
+
Scenario: Getting a Reply stream
|
|
4
|
+
Given the `sequences` is running with the following manifest:
|
|
5
|
+
"""yaml
|
|
6
|
+
exposition:
|
|
7
|
+
/:
|
|
8
|
+
POST: numbers
|
|
9
|
+
"""
|
|
10
|
+
When the following request is received:
|
|
11
|
+
"""
|
|
12
|
+
POST /sequences/ HTTP/1.1
|
|
13
|
+
content-type: text/plain
|
|
14
|
+
accept: text/plain
|
|
15
|
+
|
|
16
|
+
3
|
|
17
|
+
"""
|
|
18
|
+
Then the following reply is sent:
|
|
19
|
+
"""
|
|
20
|
+
201 Created
|
|
21
|
+
transfer-encoding: chunked
|
|
22
|
+
|
|
23
|
+
0
|
|
24
|
+
1
|
|
25
|
+
2
|
|
26
|
+
"""
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "0.20.0
|
|
4
|
-
"description": "Toa
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
7
|
-
"main": "
|
|
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,22 +16,36 @@
|
|
|
15
16
|
"publishConfig": {
|
|
16
17
|
"access": "public"
|
|
17
18
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"@types/express": "4.17.13"
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"nopeable": "*"
|
|
23
21
|
},
|
|
24
22
|
"dependencies": {
|
|
25
|
-
"@toa.io/
|
|
26
|
-
"@toa.io/
|
|
27
|
-
"@toa.io/
|
|
28
|
-
"@toa.io/
|
|
29
|
-
"@toa.io/
|
|
30
|
-
"
|
|
23
|
+
"@toa.io/core": "0.20.0",
|
|
24
|
+
"@toa.io/generic": "0.20.0",
|
|
25
|
+
"@toa.io/http": "0.20.0",
|
|
26
|
+
"@toa.io/schemas": "0.20.0",
|
|
27
|
+
"@toa.io/streams": "0.1.0",
|
|
28
|
+
"bcryptjs": "2.4.3",
|
|
31
29
|
"cors": "2.8.5",
|
|
32
|
-
"express": "4.18.
|
|
33
|
-
"
|
|
30
|
+
"express": "4.18.2",
|
|
31
|
+
"js-yaml": "4.1.0",
|
|
32
|
+
"msgpackr": "1.9.5",
|
|
33
|
+
"negotiator": "0.6.3",
|
|
34
|
+
"paseto": "3.1.4"
|
|
35
|
+
},
|
|
36
|
+
"jest": {
|
|
37
|
+
"preset": "ts-jest",
|
|
38
|
+
"testEnvironment": "node"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"transpile": "npx tsc && npx tsc -p ./components/identity.basic && npx tsc -p ./components/identity.tokens",
|
|
42
|
+
"features": "npx cucumber-js"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bcryptjs": "2.4.3",
|
|
46
|
+
"@types/cors": "2.8.13",
|
|
47
|
+
"@types/express": "4.17.17",
|
|
48
|
+
"@types/negotiator": "0.6.1"
|
|
34
49
|
},
|
|
35
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "28fc4b45c224c3683acaaf0e4abd1eb04e07b408"
|
|
36
51
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Toa Exposition
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
Exposition is a converter from [ROA](https://en.wikipedia.org/wiki/Resource-oriented_architecture)
|
|
6
|
+
to [SOA](https://en.wikipedia.org/wiki/Service-oriented_architecture).
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
# manifest.toa.yaml
|
|
10
|
+
|
|
11
|
+
name: dummy
|
|
12
|
+
namespace: dummies
|
|
13
|
+
|
|
14
|
+
exposition:
|
|
15
|
+
/:
|
|
16
|
+
GET: observe
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
# context.toa.yaml
|
|
21
|
+
|
|
22
|
+
exposition:
|
|
23
|
+
host: api.example.com
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```http
|
|
27
|
+
GET /dummies/dummy/
|
|
28
|
+
Host: api.example.com
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
See [features](features) for more examples.
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
<a href="https://miro.com/app/board/uXjVOoy0ImU=/?moveToWidget=3458764555658883997&cot=14">
|
|
36
|
+
<picture>
|
|
37
|
+
<source media="(prefers-color-scheme: dark)" srcset="./documentation/.assets/overview-dark.jpg">
|
|
38
|
+
<img alt="Exposition" width="800" height="427" src="./documentation/.assets/overview-light.jpg">
|
|
39
|
+
</picture>
|
|
40
|
+
</a>
|
|
41
|
+
|
|
42
|
+
The Exposition extension includes a Service which is an HTTP server with ingress and a Tenant. The
|
|
43
|
+
Service communicates
|
|
44
|
+
with Tenants to discover their resource declarations and exposes them as HTTP resources. An instance
|
|
45
|
+
of the Tenant is
|
|
46
|
+
running within each Composition that has at least one Component with a resource declaration.
|
|
47
|
+
|
|
48
|
+
## Resource tree discovery
|
|
49
|
+
|
|
50
|
+
During the startup of the Tenant instance, it broadcasts an `expose` message
|
|
51
|
+
containing [Resource branches](#resource-branch)
|
|
52
|
+
of the Components within the Composition. Upon receiving the `expose` message, instances of the
|
|
53
|
+
Service (re-)configures
|
|
54
|
+
corresponding routes for its HTTP server.
|
|
55
|
+
|
|
56
|
+
During the startup of the Service instance, it broadcasts a `ping` message. Once an instance of the
|
|
57
|
+
Tenant receives
|
|
58
|
+
a `ping` message, it broadcasts an `expose` message.
|
|
59
|
+
|
|
60
|
+
## Resource branch
|
|
61
|
+
|
|
62
|
+
<a href="">
|
|
63
|
+
<picture>
|
|
64
|
+
<source media="(prefers-color-scheme: dark)" srcset="documentation/.assets/rtd-dark.jpg">
|
|
65
|
+
<img alt="IA3" width="600" height="293" src="documentation/.assets/rtd-light.jpg">
|
|
66
|
+
</picture>
|
|
67
|
+
</a>
|
|
68
|
+
|
|
69
|
+
A Component can specify how to expose its Operations as HTTP resources by declaring a Resource
|
|
70
|
+
branch using the manifest extension.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# manifest.toa.yaml
|
|
74
|
+
|
|
75
|
+
extensions:
|
|
76
|
+
'@toa.io/extensions.exposition': ...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Alternatively, a shortcut `exposition` is available:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# manifest.toa.yaml
|
|
83
|
+
|
|
84
|
+
exposition: ...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Resource branches are attached to a Tree with a prefix `/{namespace}/{name}` or `/{name}` for
|
|
88
|
+
components within the default namespace.
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
# manifest.toa.yaml
|
|
92
|
+
|
|
93
|
+
name: rooms
|
|
94
|
+
namespace: messaging
|
|
95
|
+
|
|
96
|
+
exposition:
|
|
97
|
+
/: ...
|
|
98
|
+
/:user-id: ...
|
|
99
|
+
/:user-id/:room-id: ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The declaration above will result in exposing the following resources:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
/messaging/rooms/
|
|
106
|
+
/messaging/rooms/:user-id/
|
|
107
|
+
/messaging/rooms/:user-id/:room-id/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> Trailing slash is required.
|
|
111
|
+
|
|
112
|
+
## Context annotation
|
|
113
|
+
|
|
114
|
+
The Exposition annotation declares options for its deployment.
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
annotations:
|
|
118
|
+
'@toa.io/extensions.exposition':
|
|
119
|
+
host: the.exmaple.com
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A shortcut is also available.
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
exposition:
|
|
126
|
+
host: the.example.com
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Option | Type | Description |
|
|
130
|
+
|---------------|-----------|------------------------------------------------------------------|
|
|
131
|
+
| `host` | `string` | Domain name to be used for the corresponding Kubernetes Ingress. |
|
|
132
|
+
| `class` | `string` | Ingress class |
|
|
133
|
+
| `annotations` | `object` | Ingress annotations |
|
|
134
|
+
| `debug` | `boolean` | Output server errors. Default `false`. |
|
|
135
|
+
|
|
136
|
+
### Context resources
|
|
137
|
+
|
|
138
|
+
Exposition annotaion can contain [resource declaration](documentation/tree.md) under the `/` key.
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
# context.toa.yaml
|
|
142
|
+
|
|
143
|
+
exposition:
|
|
144
|
+
host: the.example.com
|
|
145
|
+
/:
|
|
146
|
+
/code:
|
|
147
|
+
GET:
|
|
148
|
+
endpoint: development.code.checkout
|
|
149
|
+
type: observation
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In the example above, a request `GET /code` will be mapped to the `development.code.checkout`
|
|
153
|
+
operation call.
|
|
154
|
+
Unlike a component resource branch declaration, properties `namespace`, `component`, and `type` are
|
|
155
|
+
required.
|
|
156
|
+
|
|
157
|
+
If component resource branch conflicts with an annotation, the annotation takes precedence.
|
|
158
|
+
|
|
159
|
+
### Example
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
exposition:
|
|
163
|
+
host: the.example.com
|
|
164
|
+
host@staging: the.example.dev
|
|
165
|
+
class: alb
|
|
166
|
+
debug@staging: true
|
|
167
|
+
annotations:
|
|
168
|
+
alb.ingress.kubernetes.io/scheme: internet-facing
|
|
169
|
+
alb.ingress.kubernetes.io/target-type: ip
|
|
170
|
+
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
|
|
171
|
+
/:
|
|
172
|
+
/foo:
|
|
173
|
+
GET: foo.bar.observe
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## See Also
|
|
177
|
+
|
|
178
|
+
- [Protocol support](documentation/protocol.md)
|
|
179
|
+
- [Resource Tree Definition](documentation/tree.md)
|
|
180
|
+
- [Identity authentication](documentation/identity.md)
|
|
181
|
+
- [Access authorization](documentation/access.md)
|
|
182
|
+
- [Components and resources](documentation/components.md)
|
|
183
|
+
- [Features](features)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
_: true
|
|
2
|
+
id?: string
|
|
3
|
+
criteria?: string
|
|
4
|
+
sort?: string
|
|
5
|
+
omit:
|
|
6
|
+
$ref: range
|
|
7
|
+
default:
|
|
8
|
+
value: 0
|
|
9
|
+
range: [0, 1000]
|
|
10
|
+
limit:
|
|
11
|
+
$ref: range
|
|
12
|
+
default:
|
|
13
|
+
value: 10
|
|
14
|
+
range: [1, 1000]
|
|
15
|
+
required: [value]
|
|
16
|
+
selectors?: [string]
|
|
17
|
+
projection?: [string]
|
package/source/Branch.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdirSync, type Dirent } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { Connector } from '@toa.io/core'
|
|
4
|
+
import { type Bootloader } from './Factory'
|
|
5
|
+
|
|
6
|
+
export class Composition extends Connector {
|
|
7
|
+
private readonly boot: Bootloader
|
|
8
|
+
|
|
9
|
+
public constructor (boot: Bootloader) {
|
|
10
|
+
super()
|
|
11
|
+
this.boot = boot
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected override async open (): Promise<void> {
|
|
15
|
+
const paths = find()
|
|
16
|
+
const composition = await this.boot.composition(paths)
|
|
17
|
+
|
|
18
|
+
await composition.connect()
|
|
19
|
+
|
|
20
|
+
this.depends(composition)
|
|
21
|
+
|
|
22
|
+
console.info('Composition complete.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected override dispose (): void {
|
|
26
|
+
console.info('Composition shutdown complete.')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function find (): string[] {
|
|
31
|
+
return entries().map((entry) => resolve(ROOT, entry.name))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function entries (): Dirent[] {
|
|
35
|
+
const entries = readdirSync(ROOT, { withFileTypes: true })
|
|
36
|
+
|
|
37
|
+
return entries.filter((entry) => entry.isDirectory())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function components (): Components {
|
|
41
|
+
const labels: string[] = []
|
|
42
|
+
const paths: string[] = []
|
|
43
|
+
|
|
44
|
+
for (const entry of entries()) {
|
|
45
|
+
labels.push(entry.name.replace('.', '-'))
|
|
46
|
+
paths.push(resolve(ROOT, entry.name))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { labels, paths }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Components {
|
|
53
|
+
labels: string[]
|
|
54
|
+
paths: string[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ROOT = resolve(__dirname, '../components/')
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { generate } from 'randomstring'
|
|
2
|
+
import { DirectivesFactory, type Family } from './Directive'
|
|
3
|
+
import { type syntax } from './RTD'
|
|
4
|
+
import { type IncomingMessage } from './HTTP'
|
|
5
|
+
import { type Remotes } from './Remotes'
|
|
6
|
+
|
|
7
|
+
const families: Array<jest.MockedObject<Family>> = [
|
|
8
|
+
{
|
|
9
|
+
name: 'foo',
|
|
10
|
+
mandatory: true,
|
|
11
|
+
create: jest.fn((_0: any, _1: any, _2: any) => generate() as any),
|
|
12
|
+
preflight: jest.fn(),
|
|
13
|
+
settle: jest.fn()
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'bar',
|
|
17
|
+
mandatory: false,
|
|
18
|
+
create: jest.fn((_0: string, _1: any, _2: any) => generate() as any),
|
|
19
|
+
preflight: jest.fn(),
|
|
20
|
+
settle: jest.fn()
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
let factory: DirectivesFactory
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks()
|
|
28
|
+
|
|
29
|
+
families[0].preflight.mockImplementation(() => null)
|
|
30
|
+
families[1].preflight.mockImplementation(() => null)
|
|
31
|
+
factory = new DirectivesFactory(families, {} as unknown as Remotes)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should create directive', async () => {
|
|
35
|
+
const declarations: syntax.Directive[] = [
|
|
36
|
+
{
|
|
37
|
+
family: 'foo',
|
|
38
|
+
name: generate(),
|
|
39
|
+
value: generate()
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
family: 'bar',
|
|
43
|
+
name: generate(),
|
|
44
|
+
value: generate()
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
factory.create(declarations)
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < declarations.length; i++) {
|
|
51
|
+
expect(families[i].create.mock.calls[0][0]).toBe(declarations[i].name)
|
|
52
|
+
expect(families[i].create.mock.calls[0][1]).toBe(declarations[i].value)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should throw error if directive family is not found', async () => {
|
|
57
|
+
const declaration: syntax.Directive = {
|
|
58
|
+
family: generate(),
|
|
59
|
+
name: generate(),
|
|
60
|
+
value: generate()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
expect(() => factory.create([declaration]))
|
|
64
|
+
.toThrowError(`Directive family '${declaration.family}' not found.`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should apply directive', async () => {
|
|
68
|
+
const declaration: syntax.Directive = {
|
|
69
|
+
family: 'foo',
|
|
70
|
+
name: generate(),
|
|
71
|
+
value: generate()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const directives = factory.create([declaration])
|
|
75
|
+
const request = generate() as unknown as IncomingMessage
|
|
76
|
+
const directive = families[0].create.mock.results[0].value
|
|
77
|
+
|
|
78
|
+
await directives.preflight(request, [])
|
|
79
|
+
|
|
80
|
+
expect(families[0].preflight.mock.calls[0][0]).toStrictEqual([directive])
|
|
81
|
+
expect(families[0].preflight.mock.calls[0][1]).toEqual(request)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should apply mandatory families', async () => {
|
|
85
|
+
const directives = factory.create([])
|
|
86
|
+
const request = generate() as unknown as IncomingMessage
|
|
87
|
+
|
|
88
|
+
await directives.preflight(request, [])
|
|
89
|
+
|
|
90
|
+
expect(families[0].preflight).toHaveBeenCalled()
|
|
91
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type IncomingMessage, type OutgoingMessage } from './HTTP'
|
|
2
|
+
import { type Remotes } from './Remotes'
|
|
3
|
+
import type * as RTD from './RTD'
|
|
4
|
+
|
|
5
|
+
export class Directives implements RTD.Directives<Directives> {
|
|
6
|
+
private readonly directives: DirectiveSet[]
|
|
7
|
+
|
|
8
|
+
public constructor (directives: DirectiveSet[]) {
|
|
9
|
+
this.directives = directives
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async preflight (request: IncomingMessage, parameters: RTD.Parameter[]): Promise<Output> {
|
|
13
|
+
for (const directive of this.directives) {
|
|
14
|
+
const output = await directive.family.preflight(directive.directives, request, parameters)
|
|
15
|
+
|
|
16
|
+
if (output !== null) {
|
|
17
|
+
await this.settle(request, output)
|
|
18
|
+
|
|
19
|
+
return output
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async settle (request: IncomingMessage, response: OutgoingMessage): Promise<void> {
|
|
27
|
+
for (const directive of this.directives)
|
|
28
|
+
if (directive.family.settle !== undefined)
|
|
29
|
+
await directive.family.settle(directive.directives, request, response)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public merge (directives: Directives): void {
|
|
33
|
+
this.directives.push(...directives.directives)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
|
|
38
|
+
private readonly remtoes: Remotes
|
|
39
|
+
private readonly families: Record<string, Family> = {}
|
|
40
|
+
private readonly mandatory: string[] = []
|
|
41
|
+
|
|
42
|
+
public constructor (families: Family[], remotes: Remotes) {
|
|
43
|
+
for (const family of families) {
|
|
44
|
+
this.families[family.name] = family
|
|
45
|
+
|
|
46
|
+
if (family.mandatory)
|
|
47
|
+
this.mandatory.push(family.name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.remtoes = remotes
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public create (declarations: RTD.syntax.Directive[]): Directives {
|
|
54
|
+
const groups: Record<string, any> = {}
|
|
55
|
+
const mandatory = new Set(this.mandatory)
|
|
56
|
+
|
|
57
|
+
declarations.sort((a, b) =>
|
|
58
|
+
(mandatory.has(b.family) ? 1 : 0) - (mandatory.has(a.family) ? 1 : 0))
|
|
59
|
+
|
|
60
|
+
for (const declaration of declarations) {
|
|
61
|
+
const family = this.families[declaration.family]
|
|
62
|
+
|
|
63
|
+
if (family === undefined)
|
|
64
|
+
throw new Error(`Directive family '${declaration.family}' not found.`)
|
|
65
|
+
|
|
66
|
+
const directive = family.create(declaration.name, declaration.value, this.remtoes)
|
|
67
|
+
|
|
68
|
+
groups[family.name] ??= []
|
|
69
|
+
groups[family.name].push(directive)
|
|
70
|
+
mandatory.delete(family.name)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sets: DirectiveSet[] = []
|
|
74
|
+
|
|
75
|
+
for (const family of mandatory)
|
|
76
|
+
sets.push({
|
|
77
|
+
family: this.families[family],
|
|
78
|
+
directives: []
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
for (const [family, directives] of Object.entries(groups))
|
|
82
|
+
sets.push({
|
|
83
|
+
family: this.families[family],
|
|
84
|
+
directives
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return new Directives(sets)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const shortcuts: RTD.syntax.Shortcuts = new Map([
|
|
92
|
+
['anonymous', 'auth:anonymous'],
|
|
93
|
+
['id', 'auth:id'],
|
|
94
|
+
['role', 'auth:role'],
|
|
95
|
+
['rule', 'auth:rule'],
|
|
96
|
+
['incept', 'auth:incept']
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
export interface Family<TDirective = any, TExtension = any> {
|
|
100
|
+
readonly name: string
|
|
101
|
+
readonly mandatory: boolean
|
|
102
|
+
|
|
103
|
+
create: (name: string, value: any, remotes: Remotes) => TDirective
|
|
104
|
+
|
|
105
|
+
preflight: (directives: TDirective[],
|
|
106
|
+
request: IncomingMessage & TExtension,
|
|
107
|
+
parameters: RTD.Parameter[]) => Output | Promise<Output>
|
|
108
|
+
|
|
109
|
+
settle?: (directives: TDirective[],
|
|
110
|
+
request: IncomingMessage & TExtension,
|
|
111
|
+
response: OutgoingMessage) => void | Promise<void>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface DirectiveSet {
|
|
115
|
+
family: Family
|
|
116
|
+
directives: any[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type Input = IncomingMessage
|
|
120
|
+
export type Output = OutgoingMessage | null
|