@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.
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,214 @@
1
+ Feature: Queries
2
+
3
+ Background:
4
+ Given the `pots` database contains:
5
+ | _id | title | volume | temperature |
6
+ | 4c4759e6f9c74da989d64511df42d6f4 | First pot | 100 | 80 |
7
+ | 99988d785d7d445cad45dbf8531f560b | Second pot | 200 | 30 |
8
+ | a7edded6b2ab47a0aca9508cc4da4138 | Third pot | 300 | 50 |
9
+ | bc6913d317334d76acd07d9f25f73535 | Fourth pot | 400 | 90 |
10
+
11
+ Scenario: Request with `id` query parameter
12
+ Given the `pots` is running with the following manifest:
13
+ """yaml
14
+ exposition:
15
+ /pot:
16
+ GET: observe
17
+ """
18
+ When the following request is received:
19
+ """
20
+ GET /pots/pot/?id=99988d785d7d445cad45dbf8531f560b HTTP/1.1
21
+ accept: application/yaml
22
+ """
23
+ Then the following reply is sent:
24
+ """
25
+ 200 OK
26
+ content-type: application/yaml
27
+
28
+ id: 99988d785d7d445cad45dbf8531f560b
29
+ title: Second pot
30
+ volume: 200
31
+ """
32
+
33
+ Scenario: Request with query criteria
34
+ Given the `pots` is running with the following manifest:
35
+ """yaml
36
+ exposition:
37
+ /:
38
+ GET: enumerate
39
+ """
40
+ When the following request is received:
41
+ """
42
+ GET /pots/?criteria=volume<300&limit=10 HTTP/1.1
43
+ accept: application/yaml
44
+ """
45
+ Then the following reply is sent:
46
+ """
47
+ 200 OK
48
+ content-type: application/yaml
49
+
50
+ - id: 4c4759e6f9c74da989d64511df42d6f4
51
+ title: First pot
52
+ volume: 100
53
+ - id: 99988d785d7d445cad45dbf8531f560b
54
+ title: Second pot
55
+ volume: 200
56
+ """
57
+
58
+ Scenario: Request with `omit` and `limit`
59
+ Given the `pots` is running with the following manifest:
60
+ """yaml
61
+ exposition:
62
+ /:
63
+ GET: enumerate
64
+ """
65
+ When the following request is received:
66
+ """
67
+ GET /pots/?omit=1&limit=2 HTTP/1.1
68
+ accept: application/yaml
69
+ """
70
+ Then the following reply is sent:
71
+ """
72
+ 200 OK
73
+ content-type: application/yaml
74
+
75
+ - id: 99988d785d7d445cad45dbf8531f560b
76
+ title: Second pot
77
+ volume: 200
78
+ - id: a7edded6b2ab47a0aca9508cc4da4138
79
+ title: Third pot
80
+ volume: 300
81
+ """
82
+
83
+ Scenario: Request with sorting
84
+ Given the `pots` is running with the following manifest:
85
+ """yaml
86
+ exposition:
87
+ /:
88
+ GET: enumerate
89
+ """
90
+ When the following request is received:
91
+ """
92
+ GET /pots/?sort=volume:desc&limit=2 HTTP/1.1
93
+ accept: application/yaml
94
+ """
95
+ Then the following reply is sent:
96
+ """
97
+ 200 OK
98
+ content-type: application/yaml
99
+
100
+ - id: bc6913d317334d76acd07d9f25f73535
101
+ title: Fourth pot
102
+ volume: 400
103
+ - id: a7edded6b2ab47a0aca9508cc4da4138
104
+ title: Third pot
105
+ volume: 300
106
+ """
107
+
108
+ Scenario: Request to a route with a path variable
109
+ Given the `pots` is running with the following manifest:
110
+ """yaml
111
+ exposition:
112
+ /:id:
113
+ GET: observe
114
+ """
115
+ When the following request is received:
116
+ """
117
+ GET /pots/99988d785d7d445cad45dbf8531f560b/ HTTP/1.1
118
+ accept: application/yaml
119
+ """
120
+ Then the following reply is sent:
121
+ """
122
+ 200 OK
123
+ content-type: application/yaml
124
+
125
+ id: 99988d785d7d445cad45dbf8531f560b
126
+ title: Second pot
127
+ volume: 200
128
+ """
129
+
130
+ Scenario: Request to a route with predefined criretia
131
+ Given the `pots` is running with the following manifest:
132
+ """yaml
133
+ exposition:
134
+ /big:
135
+ GET:
136
+ endpoint: enumerate
137
+ query:
138
+ criteria: volume>200
139
+ """
140
+ When the following request is received:
141
+ """
142
+ GET /pots/big/ HTTP/1.1
143
+ accept: application/yaml
144
+ """
145
+ Then the following reply is sent:
146
+ """
147
+ 200 OK
148
+ content-type: application/yaml
149
+
150
+ - id: a7edded6b2ab47a0aca9508cc4da4138
151
+ title: Third pot
152
+ volume: 300
153
+ - id: bc6913d317334d76acd07d9f25f73535
154
+ title: Fourth pot
155
+ volume: 400
156
+ """
157
+
158
+ Scenario: Request to a route with combined criteria
159
+ Given the `pots` is running with the following manifest:
160
+ """yaml
161
+ exposition:
162
+ /big:
163
+ GET:
164
+ endpoint: enumerate
165
+ query:
166
+ criteria: volume>200; # open criteria
167
+ """
168
+ When the following request is received:
169
+ """
170
+ GET /pots/big/?criteria=temperature>50 HTTP/1.1
171
+ accept: application/yaml
172
+ """
173
+ Then the following reply is sent:
174
+ """
175
+ 200 OK
176
+ content-type: application/yaml
177
+
178
+ - id: bc6913d317334d76acd07d9f25f73535
179
+ title: Fourth pot
180
+ volume: 400
181
+ temperature: 90
182
+ """
183
+
184
+ Scenario: Request to a route with predefined query
185
+ Given the `pots` is running with the following manifest:
186
+ """yaml
187
+ exposition:
188
+ /hottest2:
189
+ GET:
190
+ endpoint: enumerate
191
+ query:
192
+ criteria: temperature>60
193
+ sort: temperature:desc
194
+ limit: 2
195
+ """
196
+ When the following request is received:
197
+ """
198
+ GET /pots/hottest2/ HTTP/1.1
199
+ accept: application/yaml
200
+ """
201
+ Then the following reply is sent:
202
+ """
203
+ 200 OK
204
+ content-type: application/yaml
205
+
206
+ - id: bc6913d317334d76acd07d9f25f73535
207
+ title: Fourth pot
208
+ volume: 400
209
+ temperature: 90
210
+ - id: 4c4759e6f9c74da989d64511df42d6f4
211
+ title: First pot
212
+ volume: 100
213
+ temperature: 80
214
+ """
@@ -0,0 +1,49 @@
1
+ Feature: Routes
2
+
3
+ Scenario Outline: Basic routes
4
+ Given the `greeter` is running with the following manifest:
5
+ """yaml
6
+ namespace: basic
7
+
8
+ exposition:
9
+ /strict:
10
+ GET:
11
+ endpoint: greet
12
+ /shortcuts:
13
+ /operation:
14
+ GET: greet
15
+ """
16
+ When the following request is received:
17
+ """
18
+ GET /basic/greeter<route> HTTP/1.1
19
+ accept: text/plain
20
+ """
21
+ Then the following reply is sent:
22
+ """
23
+ 200 OK
24
+
25
+ Hello
26
+ """
27
+ Examples:
28
+ | route |
29
+ | /strict/ |
30
+ | /shortcuts/operation/ |
31
+
32
+ Scenario: Basic routes within default namespace
33
+ Given the `greeter` is running with the following manifest:
34
+ """yaml
35
+ exposition:
36
+ /:
37
+ GET: greet
38
+ """
39
+ When the following request is received:
40
+ """
41
+ GET /greeter/ HTTP/1.1
42
+ accept: text/plain
43
+ """
44
+ Then the following reply is sent:
45
+ """
46
+ 200 OK
47
+
48
+ Hello
49
+ """
@@ -0,0 +1,10 @@
1
+ import { binding, given } from 'cucumber-tsflow'
2
+ import { timeout } from '@toa.io/generic'
3
+
4
+ @binding()
5
+ export class Common {
6
+ @given('after {float} second(s)')
7
+ public async timeout (interval: number): Promise<void> {
8
+ await timeout(interval * 1000)
9
+ }
10
+ }
@@ -0,0 +1,43 @@
1
+ import { after, binding, given } from 'cucumber-tsflow'
2
+ import * as boot from '@toa.io/boot'
3
+ import { timeout } from '@toa.io/generic'
4
+ import { type Connector } from '@toa.io/core'
5
+ import { parse } from '@toa.io/yaml'
6
+ import { Workspace } from './Workspace'
7
+
8
+ @binding([Workspace])
9
+ export class Components {
10
+ private readonly workspace: Workspace
11
+ private composition: Connector | null = null
12
+
13
+ public constructor (workspace: Workspace) {
14
+ this.workspace = workspace
15
+ }
16
+
17
+ @given('the `{word}` is running')
18
+ public async run (name: string): Promise<void> {
19
+ await this.runComponent(name)
20
+ }
21
+
22
+ @given('the `{word}` is running with the following manifest:')
23
+ public async patchAndRun (name: string, yaml: string): Promise<void> {
24
+ const manifest = parse(yaml)
25
+
26
+ await this.runComponent(name, manifest)
27
+ }
28
+
29
+ @after()
30
+ @given('the `{word}` is stopped')
31
+ public async stop (_?: string): Promise<void> {
32
+ await this.composition?.disconnect()
33
+ }
34
+
35
+ private async runComponent (name: string, manifest?: object): Promise<void> {
36
+ const path = await this.workspace.addComponent(name, manifest)
37
+
38
+ this.composition = await boot.composition([path])
39
+
40
+ await this.composition.connect()
41
+ await timeout(50) // discovery
42
+ }
43
+ }
@@ -0,0 +1,58 @@
1
+ import { afterAll, beforeAll, binding, given } from 'cucumber-tsflow'
2
+ import { type DataTable } from '@cucumber/cucumber'
3
+ import { MongoClient } from 'mongodb'
4
+
5
+ @binding()
6
+ export class Database {
7
+ private static client: MongoClient
8
+
9
+ @given('the `{word}` database contains:')
10
+ public async upsert (id: string, table: DataTable): Promise<void> {
11
+ const [name, namespace = 'default'] = id.split('.').reverse()
12
+ const collection = Database.client.db(namespace).collection(name)
13
+
14
+ const columns = table.raw()[0]
15
+ const rows = table.rows()
16
+ const documents: Document[] = []
17
+
18
+ for (let r = 0; r < rows.length; r++) {
19
+ const document: Document = {}
20
+
21
+ for (let c = 0; c < columns.length; c++) {
22
+ const str = rows[r][c]
23
+ const int = parseInt(str)
24
+
25
+ document[columns[c]] = int.toString() === str ? int : str
26
+ }
27
+
28
+ documents.push(document)
29
+ }
30
+
31
+ await collection.deleteMany({})
32
+
33
+ if (documents.length > 0)
34
+ await collection.insertMany(documents)
35
+ }
36
+
37
+ @given('the `{word}` database is empty')
38
+ public async truncate (id: string): Promise<void> {
39
+ const [name, namespace = 'default'] = id.split('.').reverse()
40
+ const collection = Database.client.db(namespace).collection(name)
41
+
42
+ await collection.deleteMany({})
43
+ }
44
+
45
+ @beforeAll()
46
+ public static async connect (): Promise<void> {
47
+ this.client = new MongoClient('mongodb://developer:secret@localhost:27017')
48
+
49
+ await this.client.connect()
50
+ }
51
+
52
+ @afterAll()
53
+ public static async disconnect (): Promise<void> {
54
+ await this.client.close()
55
+ }
56
+ }
57
+
58
+ type Document = Record<string, string | number>
@@ -0,0 +1,113 @@
1
+ import { after, afterAll, binding, given } from 'cucumber-tsflow'
2
+ import * as boot from '@toa.io/boot'
3
+ import { type Connector } from '@toa.io/core'
4
+ import { parse } from '@toa.io/yaml'
5
+ import { encode, timeout } from '@toa.io/generic'
6
+ import { Factory } from '../../source'
7
+ import * as syntax from '../../source/RTD/syntax'
8
+ import { shortcuts } from '../../source/Directive'
9
+
10
+ let instance: Connector | null = null
11
+
12
+ @binding()
13
+ export class Gateway {
14
+ private default: boolean = true
15
+
16
+ @given('the annotation:')
17
+ public async annotate (yaml: string): Promise<void> {
18
+ const annotation = parse(yaml)
19
+
20
+ if ('/' in annotation) {
21
+ const node = { '/': annotation['/'] }
22
+ const tree = syntax.parse(node, shortcuts)
23
+
24
+ process.env.TOA_EXPOSITION = encode(tree)
25
+ }
26
+
27
+ if (annotation.debug === true)
28
+ process.env.TOA_EXPOSITION_DEBUG = '1'
29
+
30
+ await Gateway.stop()
31
+
32
+ this.default = false
33
+ }
34
+
35
+ @given('the `{word}` configuration:')
36
+ public async configure (id: string, yaml: string): Promise<void> {
37
+ const [name, namespace = 'default'] = id.split('.').reverse()
38
+ const key = `TOA_CONFIGURATION_${namespace.toUpperCase()}_${name.toUpperCase()}`
39
+ const def = DEFAULT_CONFIGURATION[id] ?? {}
40
+ const patch: object = parse(yaml)
41
+ const configuration = Object.assign({}, def, patch)
42
+
43
+ process.env[key] = encode(configuration)
44
+
45
+ await Gateway.stop()
46
+
47
+ this.default = false
48
+ }
49
+
50
+ @given('the Gateway is running')
51
+ public async start (): Promise<void> {
52
+ if (instance !== null)
53
+ return
54
+
55
+ process.env.TOA_EXPOSITION ??= DEFAULT_ANNOTATION
56
+
57
+ this.writeConfiguration()
58
+
59
+ const factory = new Factory(boot)
60
+ const service = factory.service()
61
+
62
+ if (service === null)
63
+ throw new Error('?')
64
+
65
+ instance = service
66
+
67
+ await service.connect()
68
+ await timeout(50) // resource discovery
69
+ }
70
+
71
+ @after()
72
+ public async cleanup (): Promise<void> {
73
+ if (this.default)
74
+ return
75
+
76
+ delete process.env.TOA_EXPOSITION
77
+
78
+ await Gateway.stop()
79
+ }
80
+
81
+ @afterAll()
82
+ public static async stop (): Promise<void> {
83
+ await instance?.disconnect()
84
+ instance = null
85
+ }
86
+
87
+ private writeConfiguration (): void {
88
+ for (const [id, configuration] of Object.entries(DEFAULT_CONFIGURATION)) {
89
+ const [name, namespace = 'default'] = id.split('.').reverse()
90
+ const key = `TOA_CONFIGURATION_${namespace.toUpperCase()}_${name.toUpperCase()}`
91
+
92
+ process.env[key] ??= encode(configuration)
93
+ }
94
+ }
95
+ }
96
+
97
+ const DEFAULT_ANNOTATION = encode({
98
+ routes: [],
99
+ methods: [],
100
+ directives: [
101
+ {
102
+ family: 'auth',
103
+ name: 'anonymous',
104
+ value: true
105
+ }
106
+ ]
107
+ } satisfies syntax.Node)
108
+
109
+ const DEFAULT_CONFIGURATION: Record<string, object> = {
110
+ 'identity.tokens': {
111
+ key0: 'k3.local.pIZT8-9Fa6U_QtfQHOSStfGtmyzPINyKQq2Xk-hd7vA'
112
+ }
113
+ }
@@ -0,0 +1,71 @@
1
+ import * as assert from 'assert'
2
+ import { AssertionError } from 'assert'
3
+ import { binding, when, then } from 'cucumber-tsflow'
4
+ import * as http from '@toa.io/http'
5
+ import { trim } from '@toa.io/generic'
6
+ import { Parameters } from './parameters'
7
+ import { Gateway } from './Gateway'
8
+
9
+ @binding([Gateway, Parameters])
10
+ export class HTTP {
11
+ private readonly gateway: Gateway
12
+ private readonly origin: string
13
+ private response: string = ''
14
+ private readonly variables: Record<string, string> = {}
15
+
16
+ public constructor (gateway: Gateway, parameters: Parameters) {
17
+ this.gateway = gateway
18
+ this.origin = parameters.origin
19
+ }
20
+
21
+ @when('the following request is received:')
22
+ public async request (input: string): Promise<any> {
23
+ let [headers, body] = trim(input).split('\n\n')
24
+
25
+ if (body !== undefined)
26
+ headers += '\ncontent-length: ' + body?.length
27
+
28
+ const text = headers + '\n\n' + (body ?? '')
29
+ const request = text.replaceAll(SUBSTITUTE, (_, name) => this.variables[name])
30
+
31
+ await this.gateway.start()
32
+
33
+ this.response = await http.request(request, this.origin)
34
+ }
35
+
36
+ @then('the following reply is sent:')
37
+ public responseIncludes (expected: string): void {
38
+ const lines = trim(expected).split('\n')
39
+
40
+ for (const line of lines) {
41
+ const escaped = line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
42
+
43
+ const expression = escaped.replace(CAPTURE,
44
+ (_, name) => `(?<${name}>\\S{1,2048})`)
45
+
46
+ const rx = new RegExp(expression, 'i')
47
+ const match = this.response.match(rx)
48
+
49
+ if (match === null)
50
+ throw new AssertionError({ message: `Response is missing '${line}'\n${this.response}` })
51
+
52
+ Object.assign(this.variables, match.groups)
53
+ }
54
+ }
55
+
56
+ @then('the reply does not contain:')
57
+ public responseExcludes (expected: string): void {
58
+ const lines = trim(expected).split('\n')
59
+
60
+ for (const line of lines) {
61
+ line.replace(SUBSTITUTE, (_, name) => this.variables[name])
62
+
63
+ const includes = this.response.includes(line)
64
+
65
+ assert.equal(includes, false, `Response contains '${line}'\n${this.response}`)
66
+ }
67
+ }
68
+ }
69
+
70
+ const CAPTURE = /\\\$\\{\\{ (?<name>[A-Za-z_]{0,32}) \\}\\}/g
71
+ const SUBSTITUTE = /\${{ (?<name>[A-Za-z_]{0,32}) }}/g
@@ -0,0 +1,12 @@
1
+ import { setDefaultTimeout } from '@cucumber/cucumber'
2
+
3
+ export class Parameters {
4
+ public readonly origin: string
5
+
6
+ public constructor () {
7
+ this.origin = 'http://localhost:8000'
8
+ }
9
+ }
10
+
11
+ setDefaultTimeout(10 * 1000)
12
+ process.env.TOA_DEV = '1'
@@ -0,0 +1,40 @@
1
+ import { join } from 'node:path'
2
+ import { directory } from '@toa.io/filesystem'
3
+ import * as yaml from '@toa.io/yaml'
4
+
5
+ export class Workspace {
6
+ private root: string = devnull
7
+
8
+ public static exists
9
+ (target: Workspace, key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
10
+ const method = descriptor.value
11
+
12
+ descriptor.value = async function (this: Workspace, ...args: any[]): Promise<any> {
13
+ if (this.root === devnull) this.root = await directory.temp()
14
+
15
+ return method.apply(this, args)
16
+ }
17
+
18
+ return descriptor
19
+ }
20
+
21
+ @Workspace.exists
22
+ public async addComponent (name: string, patch?: object): Promise<string> {
23
+ const source = join(__dirname, 'components', name)
24
+ const target = join(this.root, name)
25
+
26
+ await directory.copy(source, target)
27
+
28
+ if (patch !== undefined) await this.patchManifest(target, patch)
29
+
30
+ return target
31
+ }
32
+
33
+ private async patchManifest (target: string, patch: object): Promise<void> {
34
+ const path = join(target, 'manifest.toa.yaml')
35
+
36
+ await yaml.patch(path, patch)
37
+ }
38
+ }
39
+
40
+ const devnull = '/dev/null'
@@ -0,0 +1,9 @@
1
+ name: echo
2
+
3
+ operations:
4
+ compute:
5
+ input:
6
+ name*: string
7
+ affect:
8
+ input:
9
+ name*: string
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function effect (input) {
4
+ return `Hello ${input.name}`
5
+ }
6
+
7
+ exports.effect = effect
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function computation (input) {
4
+ return `Hello ${input.name}`
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,5 @@
1
+ name: greeter
2
+
3
+ exposition:
4
+ /:
5
+ GET: greet
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function computation () {
4
+ return 'Hello'
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,20 @@
1
+ name: pots
2
+
3
+ entity:
4
+ schema:
5
+ title: string
6
+ volume: number
7
+ temperature?: number
8
+
9
+ operations:
10
+ transit:
11
+ concurrency: none
12
+ input:
13
+ title*: ~
14
+ volume*: ~
15
+ observe:
16
+ output:
17
+ id: string
18
+ title: string
19
+ volume: number
20
+ temperature: number
@@ -0,0 +1,10 @@
1
+ name: sequences
2
+
3
+ operations:
4
+ numbers:
5
+ input: 5
6
+
7
+ exposition:
8
+ /tokens:
9
+ anonymous: true
10
+ GET: tokens
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function * effect (amount) {
4
+ for (let i = 0; i < amount; i++) yield i
5
+ }
6
+
7
+ exports.effect = effect