@toa.io/extensions.exposition 1.0.0-alpha.40 → 1.0.0-alpha.42

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.
@@ -20,7 +20,7 @@ Stores the content of the request body into a storage, under the request path wi
20
20
  specified `content-type`.
21
21
 
22
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),
23
+ the [validation](/extensions/storages/readme.md#async-putpath-string-stream-readable-options-options-maybeentry),
24
24
  the request is rejected with a `415 Unsupported Media Type` response.
25
25
 
26
26
  The value of the directive is `null` or an object with the following properties:
@@ -83,7 +83,8 @@ is [multipart](protocol.md#multipart-types).
83
83
  The first part represents the created Entry, which is sent immediately after the BLOB is stored,
84
84
  while subsequent parts are results from the workflow endpoints, sent as soon as they are available.
85
85
 
86
- In case a workflow endpoint returns an `Error`, the error part is sent, and the response is closed.
86
+ In case a workflow endpoint returns an `Error`, the error part is sent,
87
+ and the response is closed.
87
88
  Error's properties are added to the error part, among with the `step` identifier.
88
89
 
89
90
  ```
@@ -91,16 +92,29 @@ Error's properties are added to the error part, among with the `step` identifier
91
92
  content-type: multipart/yaml; boundary=cut
92
93
 
93
94
  --cut
95
+
94
96
  id: eecd837c
95
97
  type: image/jpeg
96
98
  created: 1698004822358
99
+
97
100
  --cut
98
- optimize: null
101
+
102
+ step: optimize
103
+ status: completed
104
+
99
105
  --cut
106
+
107
+ step: resize
100
108
  error:
101
- step: resize
102
109
  code: TOO_SMALL
103
110
  message: Image is too small
111
+ status: completed
112
+
113
+ --cut
114
+
115
+ step: analyze
116
+ status: exception
117
+
104
118
  --cut--
105
119
  ```
106
120
 
@@ -193,22 +207,6 @@ the entry is deleted.
193
207
 
194
208
  The error returned by the workflow prevents the deletion of the entry.
195
209
 
196
- ## `octets:permute`
197
-
198
- Performs
199
- a [permutation](/extensions/storages/readme.md#async-permutepath-string-ids-string-maybevoid) on the
200
- entries
201
- under the request path.
202
-
203
- ```yaml
204
- /images:
205
- octets:context: images
206
- PUT:
207
- octets:permute: ~
208
- ```
209
-
210
- The request body must be a list of entry identifiers.
211
-
212
210
  ## `octets:workflow`
213
211
 
214
212
  Execute a [workflow](#workflows) on the entry under the request path.
@@ -227,14 +225,16 @@ A workflow is a list of endpoints to be called.
227
225
  The following input will be passed to each endpoint:
228
226
 
229
227
  ```yaml
228
+ authority: string
230
229
  storage: string
231
230
  path: string
232
231
  entry: Entry
233
232
  parameters: Record<string, string> # route parameters
234
233
  ```
235
234
 
236
- See [Entry](/extensions/storages/readme.md#entry) and an
237
- example [workflow step processor](../features/steps/components/octets.tester).
235
+ - [Storages](/extensions/storages/readme.md)
236
+ - [Authorities](authorities.md)
237
+ - Example [workflow step processor](../features/steps/components/octets.tester)
238
238
 
239
239
  A _workflow unit_ is an object with keys referencing the workflow step identifier, and an endpoint
240
240
  as value.
@@ -258,4 +258,5 @@ octets:store:
258
258
  analyze: images.analyze # executed in parallel with `resize`
259
259
  ```
260
260
 
261
- If one of the workflow units returns an error, the execution of the workflow is interrupted.
261
+ If one of the workflow units returns or throws an error,
262
+ the execution of the workflow is interrupted.
@@ -123,7 +123,6 @@ Feature: Accessing entries
123
123
  """
124
124
  200 OK
125
125
  content-type: application/yaml
126
- content-length: 124
127
126
 
128
127
  id: 10cf16b458f759e0d617f2f3d83599ff
129
128
  type: application/octet-stream
@@ -24,7 +24,7 @@ Feature: Octets storage workflows
24
24
  """
25
25
  POST / HTTP/1.1
26
26
  host: nex.toa.io
27
- accept: application/yaml
27
+ accept: application/yaml, multipart/yaml
28
28
  content-type: application/octet-stream
29
29
  """
30
30
  Then the following reply is sent:
@@ -33,18 +33,29 @@ Feature: Octets storage workflows
33
33
  content-type: multipart/yaml; boundary=cut
34
34
 
35
35
  --cut
36
+
36
37
  id: 10cf16b458f759e0d617f2f3d83599ff
37
38
  type: application/octet-stream
38
39
  size: 8169
39
40
  --cut
40
- add-foo: null
41
+
42
+ step: add-foo
43
+ status: completed
41
44
  --cut
42
- add-bar:
45
+
46
+ step: add-bar
47
+ output:
43
48
  bar: baz
49
+ status: completed
44
50
  --cut
45
- add-baz: null
51
+
52
+ step: add-baz
53
+ status: completed
46
54
  --cut
47
- diversify: null
55
+
56
+ step: diversify
57
+ output: hello
58
+ status: completed
48
59
  --cut--
49
60
  """
50
61
  When the following request is received:
@@ -88,15 +99,15 @@ Feature: Octets storage workflows
88
99
  POST:
89
100
  octets:store:
90
101
  workflow:
91
- add-foo: octets.tester.foo
92
- add-bar: octets.tester.err
93
- add-baz: octets.tester.baz
102
+ - add-foo: octets.tester.foo
103
+ - add-bar: octets.tester.err
104
+ - add-baz: octets.tester.baz
94
105
  """
95
106
  When the stream of `lenna.ascii` is received with the following headers:
96
107
  """
97
108
  POST / HTTP/1.1
98
109
  host: nex.toa.io
99
- accept: application/yaml
110
+ accept: application/yaml, multipart/yaml
100
111
  content-type: application/octet-stream
101
112
  """
102
113
  Then the following reply is sent:
@@ -109,12 +120,16 @@ Feature: Octets storage workflows
109
120
  type: application/octet-stream
110
121
  size: 8169
111
122
  --cut
112
- add-foo: null
123
+
124
+ step: add-foo
125
+ status: completed
113
126
  --cut
127
+
128
+ step: add-bar
114
129
  error:
115
- step: add-bar
116
130
  code: ERROR
117
131
  message: Something went wrong
132
+ status: completed
118
133
  --cut--
119
134
  """
120
135
 
@@ -149,7 +164,7 @@ Feature: Octets storage workflows
149
164
  """
150
165
  DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
151
166
  host: nex.toa.io
152
- accept: application/yaml
167
+ accept: application/yaml, multipart/yaml
153
168
  """
154
169
  Then the following reply is sent:
155
170
  """
@@ -157,7 +172,9 @@ Feature: Octets storage workflows
157
172
  content-type: multipart/yaml; boundary=cut
158
173
 
159
174
  --cut
160
- echo: 10cf16b458f759e0d617f2f3d83599ff
175
+ step: echo
176
+ status: completed
177
+ output: 10cf16b458f759e0d617f2f3d83599ff
161
178
  --cut--
162
179
  """
163
180
  When the following request is received:
@@ -201,7 +218,7 @@ Feature: Octets storage workflows
201
218
  """
202
219
  DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
203
220
  host: nex.toa.io
204
- accept: application/yaml
221
+ accept: application/yaml, multipart/yaml
205
222
  """
206
223
  Then the following reply is sent:
207
224
  """
@@ -209,8 +226,10 @@ Feature: Octets storage workflows
209
226
  content-type: multipart/yaml; boundary=cut
210
227
 
211
228
  --cut
229
+
230
+ step: err
231
+ status: completed
212
232
  error:
213
- step: err
214
233
  code: ERROR
215
234
  message: Something went wrong
216
235
  --cut--
@@ -242,7 +261,7 @@ Feature: Octets storage workflows
242
261
  """
243
262
  POST /hello/world/ HTTP/1.1
244
263
  host: nex.toa.io
245
- accept: application/yaml
264
+ accept: application/yaml, multipart/yaml
246
265
  content-type: application/octet-stream
247
266
  """
248
267
  Then the following reply is sent:
@@ -251,11 +270,53 @@ Feature: Octets storage workflows
251
270
  content-type: multipart/yaml; boundary=cut
252
271
 
253
272
  --cut
273
+
254
274
  id: 10cf16b458f759e0d617f2f3d83599ff
255
275
  type: application/octet-stream
256
276
  size: 8169
257
277
  --cut
258
- concat: hello world
278
+
279
+ step: concat
280
+ status: completed
281
+ output: hello world
282
+ --cut--
283
+ """
284
+
285
+ Scenario: Passing authority to the workflow
286
+ Given the `octets.tester` is running
287
+ And the annotation:
288
+ """yaml
289
+ /:
290
+ /:a/:b:
291
+ auth:anonymous: true
292
+ octets:context: octets
293
+ POST:
294
+ octets:store:
295
+ workflow:
296
+ authority: octets.tester.authority
297
+ """
298
+ When the stream of `lenna.ascii` is received with the following headers:
299
+ """
300
+ POST /hello/world/ HTTP/1.1
301
+ host: nex.toa.io
302
+ accept: application/yaml, multipart/yaml
303
+ content-type: application/octet-stream
304
+ """
305
+ Then the following reply is sent:
306
+ """
307
+ 201 Created
308
+ content-type: multipart/yaml; boundary=cut
309
+
310
+ --cut
311
+
312
+ id: 10cf16b458f759e0d617f2f3d83599ff
313
+ type: application/octet-stream
314
+ size: 8169
315
+ --cut
316
+
317
+ step: authority
318
+ status: completed
319
+ output: nex
259
320
  --cut--
260
321
  """
261
322
 
@@ -287,7 +348,7 @@ Feature: Octets storage workflows
287
348
  """
288
349
  DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
289
350
  host: nex.toa.io
290
- accept: application/yaml
351
+ accept: application/yaml, multipart/yaml
291
352
  """
292
353
  Then the following reply is sent:
293
354
  """
@@ -295,6 +356,110 @@ Feature: Octets storage workflows
295
356
  content-type: multipart/yaml; boundary=cut
296
357
 
297
358
  --cut
298
- echo: 10cf16b458f759e0d617f2f3d83599ff
359
+
360
+ step: echo
361
+ status: completed
362
+ output: 10cf16b458f759e0d617f2f3d83599ff
363
+
364
+ --cut--
365
+ """
366
+
367
+ Scenario: Workflow with streaming response
368
+ Given the `octets.tester` is running
369
+ And the annotation:
370
+ """yaml
371
+ /:
372
+ auth:anonymous: true
373
+ octets:context: octets
374
+ POST:
375
+ octets:store:
376
+ workflow:
377
+ - foo: octets.tester.foo
378
+ - yield: octets.tester.yield
379
+ """
380
+ When the stream of `lenna.ascii` is received with the following headers:
381
+ """
382
+ POST / HTTP/1.1
383
+ host: nex.toa.io
384
+ accept: application/yaml, multipart/yaml
385
+ content-type: application/octet-stream
386
+ """
387
+ Then the following reply is sent:
388
+ """
389
+ 201 Created
390
+ content-type: multipart/yaml; boundary=cut
391
+
392
+ --cut
393
+
394
+ id: 10cf16b458f759e0d617f2f3d83599ff
395
+ type: application/octet-stream
396
+
397
+ --cut
398
+
399
+ step: foo
400
+ status: completed
401
+
402
+ --cut
403
+
404
+ step: yield
405
+ output: hello
406
+
407
+ --cut
408
+
409
+ step: yield
410
+ output: world
411
+
412
+ --cut
413
+
414
+ step: yield
415
+ status: completed
416
+
417
+ --cut--
418
+ """
419
+
420
+ Scenario: Workflow with streaming response and an exception
421
+ Given the `octets.tester` is running
422
+ And the annotation:
423
+ """yaml
424
+ /:
425
+ auth:anonymous: true
426
+ octets:context: octets
427
+ POST:
428
+ octets:store:
429
+ workflow:
430
+ yield: octets.tester.yex
431
+ """
432
+ When the stream of `lenna.ascii` is received with the following headers:
433
+ """
434
+ POST / HTTP/1.1
435
+ host: nex.toa.io
436
+ accept: application/yaml, multipart/yaml
437
+ content-type: application/octet-stream
438
+ """
439
+ Then the following reply is sent:
440
+ """
441
+ 201 Created
442
+ content-type: multipart/yaml; boundary=cut
443
+
444
+ --cut
445
+
446
+ id: 10cf16b458f759e0d617f2f3d83599ff
447
+ type: application/octet-stream
448
+
449
+ --cut
450
+
451
+ step: yield
452
+ output: hello
453
+
454
+ --cut
455
+
456
+ step: yield
457
+ output: world
458
+
459
+ --cut
460
+
461
+ step: yield
462
+ status: exception
463
+
299
464
  --cut--
300
465
  """
@@ -7,9 +7,10 @@ storages: octets
7
7
  operations:
8
8
  foo: &operation
9
9
  input:
10
+ authority*: string
10
11
  storage*: string
11
12
  path*: string
12
- entry: object
13
+ entry*: object
13
14
  parameters: object
14
15
  bar: *operation
15
16
  baz: *operation
@@ -17,3 +18,6 @@ operations:
17
18
  echo: *operation
18
19
  concat: *operation
19
20
  diversify: *operation
21
+ yield: *operation
22
+ yex: *operation
23
+ authority: *operation
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function computation (input) {
4
+ return input.authority
5
+ }
6
+
7
+ exports.computation = computation
@@ -4,8 +4,7 @@ import { setTimeout } from 'node:timers/promises'
4
4
 
5
5
  async function baz (input, context) {
6
6
  await setTimeout(30)
7
-
8
- return context.storages.octets.annotate(input.path, 'baz', 'qux')
7
+ await context.storages.octets.annotate(input.path, 'baz', 'qux')
9
8
  }
10
9
 
11
10
  exports.effect = baz
@@ -8,7 +8,9 @@ const lenna = join(__dirname, 'lenna.png')
8
8
  async function diversify (input, context) {
9
9
  const stream = createReadStream(lenna)
10
10
 
11
- return context.storages[input.storage].diversify(input.path, 'hello.png', stream)
11
+ await context.storages[input.storage].diversify(input.path, 'hello.png', stream)
12
+
13
+ return 'hello'
12
14
  }
13
15
 
14
16
  exports.effect = diversify
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- function foo (input, context) {
4
- return context.storages.octets.annotate(input.path, 'foo', 'bar')
3
+ async function foo (input, context) {
4
+ await context.storages.octets.annotate(input.path, 'foo', 'bar')
5
5
  }
6
6
 
7
7
  exports.effect = foo
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ import { setTimeout } from 'node:timers/promises'
4
+
5
+ async function * effect (_) {
6
+ await setTimeout(10)
7
+ yield 'hello'
8
+
9
+ await setTimeout(10)
10
+ yield 'world'
11
+
12
+ await setTimeout(10)
13
+ throw new Error('Oops!')
14
+ }
15
+
16
+ exports.effect = effect
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ import { setTimeout } from 'node:timers/promises'
4
+
5
+ async function * effect (_) {
6
+ await setTimeout(10)
7
+ yield 'hello'
8
+
9
+ await setTimeout(10)
10
+ yield 'world'
11
+ }
12
+
13
+ exports.effect = effect
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.40",
3
+ "version": "1.0.0-alpha.42",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,9 +17,9 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@toa.io/core": "1.0.0-alpha.40",
21
- "@toa.io/generic": "1.0.0-alpha.40",
22
- "@toa.io/schemas": "1.0.0-alpha.40",
20
+ "@toa.io/core": "1.0.0-alpha.42",
21
+ "@toa.io/generic": "1.0.0-alpha.42",
22
+ "@toa.io/schemas": "1.0.0-alpha.42",
23
23
  "bcryptjs": "2.4.3",
24
24
  "error-value": "0.3.0",
25
25
  "js-yaml": "4.1.0",
@@ -44,11 +44,11 @@
44
44
  "features:security": "cucumber-js --tags @security"
45
45
  },
46
46
  "devDependencies": {
47
- "@toa.io/agent": "1.0.0-alpha.40",
48
- "@toa.io/extensions.storages": "1.0.0-alpha.40",
47
+ "@toa.io/agent": "1.0.0-alpha.42",
48
+ "@toa.io/extensions.storages": "1.0.0-alpha.42",
49
49
  "@types/bcryptjs": "2.4.3",
50
50
  "@types/cors": "2.8.13",
51
51
  "@types/negotiator": "0.6.1"
52
52
  },
53
- "gitHead": "973a253abb982ddd6cdd7427ef98331d1157180d"
53
+ "gitHead": "6df7ba51d3f8f419b95253538e08dc7e9bc14072"
54
54
  }
@@ -1,9 +1,13 @@
1
- import { Readable } from 'node:stream'
1
+ import { PassThrough, Readable } from 'node:stream'
2
+ import * as streamConsumers from 'node:stream/consumers'
3
+ import { once } from 'node:events'
2
4
  import { generate } from 'randomstring'
3
5
  import * as msgpack from 'msgpackr'
4
- import { read } from './messages'
6
+ import { multipart, read, type OutgoingMessage } from './messages'
5
7
  import { BadRequest, UnsupportedMediaType } from './exceptions'
8
+ import { formats } from './formats'
6
9
  import { Timing } from './Timing'
10
+ import type * as http from 'node:http'
7
11
  import type { Context } from './Context'
8
12
 
9
13
  beforeEach(() => {
@@ -69,6 +73,39 @@ describe('read', () => {
69
73
 
70
74
  await expect(read(request)).rejects.toThrow(BadRequest)
71
75
  })
76
+
77
+ it('should output correct mulitpart format', async () => {
78
+ const response = new class extends PassThrough {
79
+ public readonly headers = new Headers()
80
+
81
+ public setHeader (key: string, value: string): this {
82
+ this.headers.set(key, value)
83
+
84
+ return this
85
+ }
86
+ }()
87
+
88
+ const context = { encoder: formats['text/plain'] } as unknown as Context
89
+ const message = { body: Readable.from(['Hello', 'New', 'World']) } as unknown as OutgoingMessage
90
+
91
+ multipart(message, context, response as unknown as http.ServerResponse)
92
+
93
+ await once(message.body, 'end')
94
+
95
+ const result = await streamConsumers.text(response)
96
+
97
+ expect(result).toBe([
98
+ '--cut',
99
+ '',
100
+ 'Hello',
101
+ '--cut',
102
+ '',
103
+ 'New',
104
+ '--cut',
105
+ '',
106
+ 'World',
107
+ '--cut--'].join('\r\n'))
108
+ })
72
109
  })
73
110
 
74
111
  export function createContext
@@ -72,7 +72,7 @@ function stream
72
72
  })
73
73
  }
74
74
 
75
- function multipart
75
+ export function multipart
76
76
  (message: OutgoingMessage, context: Context, response: http.ServerResponse): void {
77
77
  if (context.encoder === null)
78
78
  throw new NotAcceptable()
@@ -82,7 +82,11 @@ function multipart
82
82
  response.setHeader('content-type', `${encoder.multipart}; boundary=${BOUNDARY}`)
83
83
 
84
84
  message.body
85
- .map((part: unknown) => Buffer.concat([CUT, encoder.encode(part), CRLF]))
85
+ .map((part: unknown) => Buffer.concat([
86
+ CUT,
87
+ CRLF /* indicates no boundary headers */,
88
+ encoder.encode(part),
89
+ CRLF]))
86
90
  .on('end', () => response.end(FINALCUT))
87
91
  .pipe(response)
88
92
  }