@toa.io/extensions.exposition 0.22.1 → 0.23.0-dev.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 (66) hide show
  1. package/components/identity.basic/source/authenticate.ts +3 -2
  2. package/components/identity.basic/source/transit.ts +4 -3
  3. package/components/octets.storage/manifest.toa.yaml +26 -0
  4. package/components/octets.storage/operations/delete.js +7 -0
  5. package/components/octets.storage/operations/fetch.js +46 -0
  6. package/components/octets.storage/operations/get.js +7 -0
  7. package/components/octets.storage/operations/list.js +7 -0
  8. package/components/octets.storage/operations/permute.js +7 -0
  9. package/components/octets.storage/operations/store.js +11 -0
  10. package/cucumber.js +0 -1
  11. package/documentation/octets.md +196 -0
  12. package/documentation/protocol.md +49 -5
  13. package/features/access.feature +1 -0
  14. package/features/errors.feature +18 -0
  15. package/features/identity.basic.feature +2 -0
  16. package/features/octets.feature +295 -0
  17. package/features/octets.workflows.feature +114 -0
  18. package/features/routes.feature +40 -0
  19. package/features/steps/HTTP.ts +47 -5
  20. package/features/steps/Parameters.ts +5 -1
  21. package/features/steps/Workspace.ts +3 -2
  22. package/features/steps/components/octets.tester/manifest.toa.yaml +15 -0
  23. package/features/steps/components/octets.tester/operations/bar.js +12 -0
  24. package/features/steps/components/octets.tester/operations/baz.js +11 -0
  25. package/features/steps/components/octets.tester/operations/diversify.js +14 -0
  26. package/features/steps/components/octets.tester/operations/err.js +16 -0
  27. package/features/steps/components/octets.tester/operations/foo.js +7 -0
  28. package/features/steps/components/octets.tester/operations/lenna.png +0 -0
  29. package/features/steps/components/pots/manifest.toa.yaml +1 -1
  30. package/features/streams.feature +5 -1
  31. package/package.json +11 -7
  32. package/readme.md +7 -5
  33. package/schemas/octets/context.cos.yaml +1 -0
  34. package/schemas/octets/delete.cos.yaml +1 -0
  35. package/schemas/octets/fetch.cos.yaml +3 -0
  36. package/schemas/octets/list.cos.yaml +1 -0
  37. package/schemas/octets/permute.cos.yaml +1 -0
  38. package/schemas/octets/store.cos.yaml +3 -0
  39. package/source/Gateway.ts +9 -4
  40. package/source/HTTP/Server.fixtures.ts +2 -6
  41. package/source/HTTP/Server.test.ts +7 -31
  42. package/source/HTTP/Server.ts +31 -16
  43. package/source/HTTP/exceptions.ts +2 -12
  44. package/source/HTTP/formats/index.ts +7 -4
  45. package/source/HTTP/formats/json.ts +3 -0
  46. package/source/HTTP/formats/msgpack.ts +3 -0
  47. package/source/HTTP/formats/text.ts +3 -0
  48. package/source/HTTP/formats/yaml.ts +3 -0
  49. package/source/HTTP/messages.test.ts +3 -49
  50. package/source/HTTP/messages.ts +58 -33
  51. package/source/RTD/Route.ts +1 -1
  52. package/source/RTD/segment.ts +2 -1
  53. package/source/Remotes.ts +8 -0
  54. package/source/directives/auth/Family.ts +24 -20
  55. package/source/directives/auth/Rule.ts +1 -1
  56. package/source/directives/index.ts +2 -1
  57. package/source/directives/octets/Context.ts +18 -0
  58. package/source/directives/octets/Delete.ts +32 -0
  59. package/source/directives/octets/Family.ts +68 -0
  60. package/source/directives/octets/Fetch.ts +85 -0
  61. package/source/directives/octets/List.ts +32 -0
  62. package/source/directives/octets/Permute.ts +37 -0
  63. package/source/directives/octets/Store.ts +158 -0
  64. package/source/directives/octets/index.ts +3 -0
  65. package/source/directives/octets/schemas.ts +12 -0
  66. package/source/directives/octets/types.ts +13 -0
@@ -1,6 +1,7 @@
1
1
  import { atob } from 'buffer'
2
2
  import { compare } from 'bcryptjs'
3
3
  import { type Query, type Maybe } from '@toa.io/types'
4
+ import { Err } from 'error-value'
4
5
  import { type Context } from './types'
5
6
 
6
7
  export async function computation (input: string, context: Context): Promise<Maybe<Output>> {
@@ -21,8 +22,8 @@ export async function computation (input: string, context: Context): Promise<May
21
22
  else return ERR_PASSWORD_MISMATCH
22
23
  }
23
24
 
24
- const ERR_NOT_FOUND = new Error('NOT_FOUND')
25
- const ERR_PASSWORD_MISMATCH = new Error('PASSWORD_MISMATCH')
25
+ const ERR_NOT_FOUND = Err('NOT_FOUND')
26
+ const ERR_PASSWORD_MISMATCH = Err('PASSWORD_MISMATCH')
26
27
 
27
28
  interface Output {
28
29
  identity: {
@@ -1,5 +1,6 @@
1
1
  import { genSalt, hash } from 'bcryptjs'
2
2
  import { type Maybe, type Operation } from '@toa.io/types'
3
+ import { Err } from 'error-value'
3
4
  import { type Context, type Entity, type TransitInput, type TransitOutput } from './types'
4
5
 
5
6
  export class Transition implements Operation {
@@ -60,8 +61,8 @@ function invalid (value: string, expressions: RegExp[]): boolean {
60
61
  return expressions.some((expression) => !expression.test(value))
61
62
  }
62
63
 
63
- const ERR_PRINCIPAL_LOCKED = new Error('PRINCIPAL_LOCKED')
64
- const ERR_INVALID_USERNAME = new Error('INVALID_USERNAME')
65
- const ERR_INVALID_PASSWORD = new Error('INVALID_PASSWORD')
64
+ const ERR_PRINCIPAL_LOCKED = Err('PRINCIPAL_LOCKED', 'Principal username cannot be changed.')
65
+ const ERR_INVALID_USERNAME = Err('INVALID_USERNAME', 'Username is not meeting the requirements.')
66
+ const ERR_INVALID_PASSWORD = Err('INVALID_PASSWORD', 'Password is not meeting the requirements.')
66
67
 
67
68
  type Tokens = Context['remote']['identity']['tokens']
@@ -0,0 +1,26 @@
1
+ namespace: octets
2
+ name: storage
3
+
4
+ storages: ~
5
+
6
+ operations:
7
+ store:
8
+ bindings: ~
9
+ input:
10
+ storage*: string
11
+ request*: ~
12
+ accept: string
13
+ fetch: &simple
14
+ bindings: ~
15
+ input:
16
+ storage*: string
17
+ path*: string
18
+ get: *simple
19
+ list: *simple
20
+ delete: *simple
21
+ permute:
22
+ bindings: ~
23
+ input:
24
+ storage*: string
25
+ path*: string
26
+ list*: [string]
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function del (input, context) {
4
+ return context.storages[input.storage].delete(input.path)
5
+ }
6
+
7
+ exports.effect = del
@@ -0,0 +1,46 @@
1
+ 'use strict'
2
+
3
+ const { posix } = require('node:path')
4
+ const { Err } = require('error-value')
5
+
6
+ async function fetch (input, context) {
7
+ const storage = context.storages[input.storage]
8
+ const basename = posix.basename(input.path)
9
+ const path = posix.dirname(input.path)
10
+ const [id, suffix] = split(basename)
11
+ const entry = await storage.get(posix.join(path, id))
12
+
13
+ if (entry instanceof Error)
14
+ return entry
15
+
16
+ let variant
17
+
18
+ if (suffix !== undefined) {
19
+ variant = entry.variants.find((variant) => variant.name === suffix)
20
+
21
+ if (variant === undefined)
22
+ return NOT_FOUND
23
+ }
24
+
25
+ const stream = await storage.fetch(input.path)
26
+
27
+ if (stream instanceof Error)
28
+ return stream
29
+
30
+ const { type, size } = variant ?? entry
31
+
32
+ return { stream, checksum: entry.id, type, size }
33
+ }
34
+
35
+ function split (basename) {
36
+ const dot = basename.indexOf('.')
37
+
38
+ if (dot === -1)
39
+ return [basename, undefined]
40
+ else
41
+ return [basename.slice(0, dot), basename.slice(dot + 1)]
42
+ }
43
+
44
+ const NOT_FOUND = Err('NOT_FOUND')
45
+
46
+ exports.effect = fetch
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function get (input, context) {
4
+ return context.storages[input.storage].get(input.path)
5
+ }
6
+
7
+ exports.computation = get
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function list (input, context) {
4
+ return context.storages[input.storage].list(input.path)
5
+ }
6
+
7
+ exports.effect = list
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function permute (input, context) {
4
+ return context.storages[input.storage].permute(input.path, input.list)
5
+ }
6
+
7
+ exports.effect = permute
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ function store (input, context) {
4
+ const { storage, request } = input
5
+ const path = request.path
6
+ const claim = request.headers['content-type']
7
+
8
+ return context.storages[storage].put(path, request, { claim, accept: input.accept })
9
+ }
10
+
11
+ exports.effect = store
package/cucumber.js CHANGED
@@ -3,7 +3,6 @@ module.exports = {
3
3
  paths: ['features/**/*.feature'],
4
4
  requireModule: ['ts-node/register'],
5
5
  require: ['./features/**/*.ts'],
6
- publishQuiet: true,
7
6
  failFast: true
8
7
  }
9
8
  }
@@ -0,0 +1,196 @@
1
+ # BLOBs
2
+
3
+ The `octets` directive family implements operations with BLOBs, using
4
+ the [Storages extention](/extensions/storages).
5
+ The most common use case is to handle file uploads, downloads, and processing.
6
+
7
+ ## `octets:context`
8
+
9
+ Sets the [storage name](/extensions/storages/readme.md#annotation) to be used for the `octets`
10
+ directives under the current RTD Node.
11
+
12
+ ```yaml
13
+ /images:
14
+ octets:context: images
15
+ ```
16
+
17
+ ## `octets:store`
18
+
19
+ Stores the content of the request body into a storage, under the request path with
20
+ specified `content-type`.
21
+
22
+ If request's `content-type` is not acceptable, or if the request body does not pass
23
+ the [validation](/extensions/storages/readme.md#async-putpath-string-stream-readable-type-typecontrol-maybeentry),
24
+ the request is rejected with a `415 Unsupported Media Type` response.
25
+
26
+ The value of the directive is an object with the following properties:
27
+
28
+ - `accept`: a media type or an array of media types that are acceptable.
29
+ If the `accept` property is not specified, any media type is acceptable (which is the default).
30
+ - `workflow`: [workflow](#workflows) to be executed once the content is successfully stored.
31
+
32
+ ```yaml
33
+ /images:
34
+ octets:context: images
35
+ POST:
36
+ octets:store:
37
+ accept:
38
+ - image/jpeg
39
+ - image/png
40
+ - video/*
41
+ workflow:
42
+ resize: images.resize
43
+ analyze: images.analyze
44
+ ```
45
+
46
+ ### Workflows
47
+
48
+ A workflow is a list of endpoints to be called.
49
+ The following input will be passed to each endpoint:
50
+
51
+ ```yaml
52
+ storage: string
53
+ path: string
54
+ entry: Entry
55
+ ```
56
+
57
+ See [Entry](/extensions/storages/readme.md#entry) and an
58
+ example [workflow step processor](../features/steps/components/octets.tester).
59
+
60
+ A _workflow unit_ is an object with keys referencing the workflow step identifier, and an endpoint
61
+ as value.
62
+ Steps within a workflow unit are executed in parallel.
63
+
64
+ ```yaml
65
+ octets:store:
66
+ workflow:
67
+ resize: images.resize
68
+ analyze: images.analyze
69
+ ```
70
+
71
+ A workflow can be a single unit, or an array of units.
72
+ If it's an array, the workflow units are executed in sequence.
73
+
74
+ ```yaml
75
+ octets:store:
76
+ workflow:
77
+ - optimize: images.optimize # executed first
78
+ - resize: images.resize # executed second
79
+ analyze: images.analyze # executed in parallel with `resize`
80
+ ```
81
+
82
+ If one of the workflow units returns an error, the execution of the workflow is interrupted.
83
+
84
+ ### Response
85
+
86
+ The response of the `octets:store` directive is the created Entry.
87
+
88
+ ```
89
+ 201 Created
90
+ content-type: application/yaml
91
+
92
+ id: eecd837c
93
+ type: image/jpeg
94
+ created: 1698004822358
95
+ ```
96
+
97
+ If the `octets:store` directive contains a `workflow`, the response
98
+ is [multipart](protocol.md#multipart-types).
99
+ The first part represents the created Entry, which is sent immediately after the BLOB is stored,
100
+ while subsequent parts are results from the workflow endpoints, sent as soon as they are available.
101
+
102
+ In case a workflow endpoint returns an `Error`, the error part is sent, and the response is closed.
103
+ Error's properties are added to the error part, among with the `step` identifier.
104
+
105
+ ```
106
+ 201 Created
107
+ content-type: multipart/yaml; boundary=cut
108
+
109
+ --cut
110
+ id: eecd837c
111
+ type: image/jpeg
112
+ created: 1698004822358
113
+ --cut
114
+ optimize: null
115
+ --cut
116
+ error:
117
+ step: resize
118
+ code: TOO_SMALL
119
+ message: Image is too small
120
+ --cut--
121
+ ```
122
+
123
+ ## `octets:fetch`
124
+
125
+ Fetches the content of a stored BLOB corresponding to the request path, and returns it as the
126
+ response body with the corresponding `content-type`, `content-length`
127
+ and `etag` ([conditional GET](https://datatracker.ietf.org/doc/html/rfc2616#section-9.3) is
128
+ also supported).
129
+ The `accept` request header is disregarded.
130
+
131
+ The value of the directive is an object with the following properties:
132
+
133
+ - `meta`: `boolean` indicating whether an Entry is accessible.
134
+ Defaults to `false`.
135
+ - `blob`: `boolean` indicating whether the original BLOB is accessible,
136
+ [BLOB variant](/extensions/storages/readme.md#async-fetchpath-string-maybereadable) must be
137
+ specified in the path otherwise.
138
+ Defaults to `true`.
139
+
140
+ ```yaml
141
+ /images:
142
+ octets:context: images
143
+ /*:
144
+ GET:
145
+ octets:fetch:
146
+ blob: false # prevent access to the original BLOB
147
+ meta: true # allow access to an Entry
148
+ ```
149
+
150
+ To access an Entry, the request path must be suffixed with `:meta`:
151
+
152
+ ```http
153
+ GET /images/eecd837c:meta HTTP/1.1
154
+ ```
155
+
156
+ The `octets:fetch: ~` declaration is equivalent to defaults.
157
+
158
+ ## `octets:list`
159
+
160
+ Lists the entries stored under the request path.
161
+
162
+ ```yaml
163
+ /images:
164
+ octets:context: images
165
+ GET:
166
+ octets:list: ~
167
+ ```
168
+
169
+ Responds with a list of entry identifiers.
170
+
171
+ ## `octets:delete`
172
+
173
+ Delete the entry corresponding to the request path.
174
+
175
+ ```yaml
176
+ /images:
177
+ octets:context: images
178
+ DELETE:
179
+ octets:delete: ~
180
+ ```
181
+
182
+ ## `octets:permute`
183
+
184
+ Performs
185
+ a [permutation](/extensions/storages/readme.md#async-permutepath-string-ids-string-maybevoid) on the
186
+ entries
187
+ under the request path.
188
+
189
+ ```yaml
190
+ /images:
191
+ octets:context: images
192
+ PUT:
193
+ octets:permute: ~
194
+ ```
195
+
196
+ The request body must be a list of entry identifiers.
@@ -4,15 +4,59 @@
4
4
 
5
5
  The following media types are supported for both requests and responses:
6
6
 
7
- - `application/json`
8
- - `application/yaml` using [js-yaml](https://github.com/nodeca/js-yaml)
9
7
  - `application/msgpack` using [msgpackr](https://github.com/kriszyp/msgpackr)
8
+ - `application/yaml` using [js-yaml](https://github.com/nodeca/js-yaml)
9
+ - `application/json`
10
10
  - `text/plain`
11
11
 
12
12
  The response format is determined by content negotiation
13
13
  using [negotiator](https://github.com/jshttp/negotiator).
14
14
 
15
- ## Streams
15
+ ```http
16
+ GET / HTTP/1.1
17
+ accept: application/yaml
18
+ ```
19
+
20
+ ```
21
+ 200 OK
22
+ content-type: application/yaml
23
+
24
+ foo: bar
25
+ ```
26
+
27
+ ### Multipart types
28
+
29
+ Multipart responses are endoded using content negotiation,
30
+ and the `content-type` of the response is set to one of the custom `multipart/` subtypes, corresponding to the type of
31
+ the parts:
32
+
33
+ | Response type | Part type |
34
+ |---------------------|-----------------------|
35
+ | `multipart/msgpack` | `application/msgpack` |
36
+ | `multipart/yaml` | `application/yaml` |
37
+ | `multipart/json` | `application/json` |
38
+ | `multipart/text` | `text/plain` |
39
+
40
+ Example:
41
+
42
+ ```
43
+ GET /stream/ HTTP/1.1
44
+ accept: application/yaml
45
+ ```
46
+
47
+ ```
48
+ 200 OK
49
+ content-type: multipart/yaml; boundary=cut
50
+
51
+ --cut
52
+ foo: bar
53
+ --cut
54
+ baz: qux
55
+ --cut--
56
+ ```
57
+
58
+ See also:
16
59
 
17
- Reply streams are transmitted
18
- using [chunked transfer encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#directives).
60
+ - [Multipart Content-Type](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) at W3C
61
+ - [Content-Type: multipart](https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/aa493937(v=exchg.140))
62
+ at Microsoft
@@ -38,6 +38,7 @@ Feature: Access authorization
38
38
  When the following request is received:
39
39
  """
40
40
  GET / HTTP/1.1
41
+ accept: application/yaml
41
42
  """
42
43
  Then the following reply is sent:
43
44
  """
@@ -191,3 +191,21 @@ Feature: Errors
191
191
  | debug | response |
192
192
  | false | content-length: 0 |
193
193
  | true | Error: Broken! |
194
+
195
+ Scenario: Not acceptable request
196
+ Given the annotation:
197
+ """yaml
198
+ /:
199
+ GET:
200
+ anonymous: true
201
+ dev:stub: hello
202
+ """
203
+ When the following request is received:
204
+ """
205
+ GET / HTTP/1.1
206
+ accept: image/jpeg
207
+ """
208
+ Then the following reply is sent:
209
+ """
210
+ 406 Not Acceptable
211
+ """
@@ -181,6 +181,7 @@ Feature: Basic authentication
181
181
  When the following request is received:
182
182
  """
183
183
  POST /identity/basic/ HTTP/1.1
184
+ accept: application/yaml
184
185
  content-type: application/yaml
185
186
 
186
187
  username: root
@@ -224,6 +225,7 @@ Feature: Basic authentication
224
225
  """
225
226
  PATCH /identity/basic/${{ id }}/ HTTP/1.1
226
227
  authorization: Token ${{ token }}
228
+ accept: application/yaml
227
229
  content-type: application/yaml
228
230
 
229
231
  username: admin