@toa.io/extensions.exposition 0.24.0-alpha.21 → 0.24.0-alpha.23

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 (82) hide show
  1. package/components/context.toa.yaml +1 -1
  2. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  3. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  4. package/components/octets.storage/manifest.toa.yaml +1 -0
  5. package/components/octets.storage/operations/store.js +2 -2
  6. package/documentation/octets.md +89 -37
  7. package/features/cors.feature +33 -0
  8. package/features/octets.entries.feature +121 -0
  9. package/features/octets.feature +1 -27
  10. package/features/octets.meta.feature +65 -0
  11. package/features/octets.workflows.feature +105 -4
  12. package/features/routes.feature +37 -0
  13. package/features/steps/Captures.ts +3 -2
  14. package/features/steps/Components.ts +15 -8
  15. package/features/steps/HTTP.ts +1 -1
  16. package/features/steps/Parameters.ts +1 -1
  17. package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
  18. package/features/steps/components/octets.tester/operations/echo.js +7 -0
  19. package/features/steps/components/users/manifest.toa.yaml +3 -0
  20. package/features/steps/components/users.properties/manifest.toa.yaml +13 -0
  21. package/features/steps/tsconfig.json +1 -1
  22. package/features/vary.feature +1 -1
  23. package/package.json +8 -8
  24. package/schemas/octets/delete.cos.yaml +2 -1
  25. package/schemas/octets/list.cos.yaml +2 -1
  26. package/source/HTTP/Server.ts +32 -23
  27. package/source/HTTP/messages.ts +7 -1
  28. package/source/directives/cors/CORS.ts +25 -23
  29. package/source/directives/index.ts +1 -1
  30. package/source/directives/octets/Delete.ts +45 -6
  31. package/source/directives/octets/Fetch.ts +17 -18
  32. package/source/directives/octets/List.ts +36 -6
  33. package/source/directives/octets/Permute.ts +2 -2
  34. package/source/directives/octets/Store.ts +36 -94
  35. package/source/directives/octets/schemas.ts +11 -6
  36. package/source/directives/octets/workflow/Execution.ts +77 -0
  37. package/source/directives/octets/workflow/Workflow.ts +28 -0
  38. package/source/directives/octets/workflow/index.ts +1 -0
  39. package/source/manifest.test.ts +6 -14
  40. package/source/manifest.ts +9 -6
  41. package/source/schemas.ts +7 -3
  42. package/transpiled/HTTP/Server.d.ts +1 -1
  43. package/transpiled/HTTP/Server.js +22 -21
  44. package/transpiled/HTTP/Server.js.map +1 -1
  45. package/transpiled/HTTP/messages.d.ts +1 -0
  46. package/transpiled/HTTP/messages.js +4 -1
  47. package/transpiled/HTTP/messages.js.map +1 -1
  48. package/transpiled/directives/cors/CORS.d.ts +2 -5
  49. package/transpiled/directives/cors/CORS.js +18 -16
  50. package/transpiled/directives/cors/CORS.js.map +1 -1
  51. package/transpiled/directives/index.js +1 -1
  52. package/transpiled/directives/index.js.map +1 -1
  53. package/transpiled/directives/octets/Delete.d.ts +9 -1
  54. package/transpiled/directives/octets/Delete.js +30 -6
  55. package/transpiled/directives/octets/Delete.js.map +1 -1
  56. package/transpiled/directives/octets/Fetch.d.ts +4 -5
  57. package/transpiled/directives/octets/Fetch.js +11 -12
  58. package/transpiled/directives/octets/Fetch.js.map +1 -1
  59. package/transpiled/directives/octets/List.d.ts +6 -1
  60. package/transpiled/directives/octets/List.js +22 -4
  61. package/transpiled/directives/octets/List.js.map +1 -1
  62. package/transpiled/directives/octets/Permute.js +2 -2
  63. package/transpiled/directives/octets/Permute.js.map +1 -1
  64. package/transpiled/directives/octets/Store.d.ts +7 -19
  65. package/transpiled/directives/octets/Store.js +21 -66
  66. package/transpiled/directives/octets/Store.js.map +1 -1
  67. package/transpiled/directives/octets/schemas.d.ts +11 -6
  68. package/transpiled/directives/octets/schemas.js.map +1 -1
  69. package/transpiled/directives/octets/workflow/Execution.d.ts +24 -0
  70. package/transpiled/directives/octets/workflow/Execution.js +55 -0
  71. package/transpiled/directives/octets/workflow/Execution.js.map +1 -0
  72. package/transpiled/directives/octets/workflow/Workflow.d.ts +11 -0
  73. package/transpiled/directives/octets/workflow/Workflow.js +21 -0
  74. package/transpiled/directives/octets/workflow/Workflow.js.map +1 -0
  75. package/transpiled/directives/octets/workflow/index.d.ts +1 -0
  76. package/transpiled/directives/octets/workflow/index.js +6 -0
  77. package/transpiled/directives/octets/workflow/index.js.map +1 -0
  78. package/transpiled/manifest.js +10 -5
  79. package/transpiled/manifest.js.map +1 -1
  80. package/transpiled/schemas.d.ts +7 -3
  81. package/transpiled/schemas.js.map +1 -1
  82. package/transpiled/tsconfig.tsbuildinfo +1 -1
@@ -7,17 +7,21 @@ import Negotiator from 'negotiator'
7
7
  import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
8
8
  import { ClientError, Exception } from './exceptions'
9
9
  import { formats, types } from './formats'
10
- import type { Format } from './formats'
11
10
  import type * as http from 'node:http'
12
11
  import type { Express, Request, Response, NextFunction } from 'express'
13
12
 
14
13
  export class Server extends Connector {
15
14
  private server?: http.Server
15
+ private readonly app: Express
16
+ private readonly debug: boolean
17
+ private readonly requestedPort: number
16
18
 
17
- private constructor (private readonly app: Express,
18
- private readonly debug: boolean,
19
- private readonly requestedPort: number) {
19
+ private constructor (app: Express, debug: boolean, port: number) {
20
20
  super()
21
+
22
+ this.app = app
23
+ this.debug = debug
24
+ this.requestedPort = port
21
25
  }
22
26
 
23
27
  public get port (): number {
@@ -39,7 +43,6 @@ export class Server extends Connector {
39
43
  const app = express()
40
44
 
41
45
  app.disable('x-powered-by')
42
- // app.use(cors(CORS))
43
46
  app.use(supportedMethods(properties.methods))
44
47
 
45
48
  return new Server(app, properties.debug, properties.port)
@@ -80,7 +83,8 @@ export class Server extends Connector {
80
83
  private extend (request: Request): IncomingMessage {
81
84
  const message = request as IncomingMessage
82
85
 
83
- message.encoder = negotiate(request)
86
+ negotiate(request, message)
87
+
84
88
  message.pipelines = { body: [], response: [] }
85
89
 
86
90
  message.parse = async <T> (): Promise<T> => {
@@ -97,9 +101,6 @@ export class Server extends Connector {
97
101
 
98
102
  private success (request: IncomingMessage, response: Response) {
99
103
  return (message: OutgoingMessage) => {
100
- for (const transform of request.pipelines.response)
101
- transform(message)
102
-
103
104
  let status = message.status
104
105
 
105
106
  if (status === undefined)
@@ -109,12 +110,7 @@ export class Server extends Connector {
109
110
  else status = 200
110
111
 
111
112
  response.status(status)
112
- message.headers?.forEach((value, key) => response.set(key, value))
113
-
114
- if (message.body !== undefined && message.body !== null)
115
- write(request, response, message)
116
- else
117
- response.end()
113
+ write(request, response, message)
118
114
  }
119
115
  }
120
116
 
@@ -129,16 +125,15 @@ export class Server extends Connector {
129
125
 
130
126
  response.status(status)
131
127
 
132
- const outputAllowed = exception instanceof ClientError || this.debug
128
+ const message: OutgoingMessage = {}
129
+ const verbose = exception instanceof ClientError || this.debug
133
130
 
134
- if (outputAllowed) {
135
- const body = exception instanceof Exception
131
+ if (verbose)
132
+ message.body = exception instanceof Exception
136
133
  ? exception.body
137
134
  : (exception.stack ?? exception.message)
138
135
 
139
- write(request, response, { body })
140
- } else
141
- response.end()
136
+ write(request, response, message)
142
137
  }
143
138
  }
144
139
  }
@@ -150,11 +145,23 @@ function supportedMethods (methods: Set<string>) {
150
145
  }
151
146
  }
152
147
 
153
- function negotiate (request: Request): Format | null {
148
+ function negotiate (request: Request, message: IncomingMessage): void {
149
+ if (request.headers.accept !== undefined) {
150
+ const match = SUBTYPE.exec(request.headers.accept)
151
+
152
+ if (match !== null) {
153
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154
+ const { type, subtype, suffix } = match.groups!
155
+
156
+ request.headers.accept = `${type}/${suffix}`
157
+ message.subtype = subtype
158
+ }
159
+ }
160
+
154
161
  const negotiator = new Negotiator(request)
155
162
  const mediaType = negotiator.mediaType(types)
156
163
 
157
- return mediaType === undefined ? null : formats[mediaType]
164
+ message.encoder = mediaType === undefined ? null : formats[mediaType]
158
165
  }
159
166
 
160
167
  // https://github.com/whatwg/fetch/issues/1254
@@ -182,3 +189,5 @@ interface Properties {
182
189
  }
183
190
 
184
191
  export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
192
+
193
+ const SUBTYPE = /^(?<type>\w{1,32})\/(vnd\.toa\.(?<subtype>\S{1,32})\+)(?<suffix>\S{1,32})$/
@@ -9,6 +9,11 @@ import type { Format } from './formats'
9
9
 
10
10
  export function write
11
11
  (request: IncomingMessage, response: Response, message: OutgoingMessage): void {
12
+ for (const transform of request.pipelines.response)
13
+ transform(message)
14
+
15
+ message.headers?.forEach((value, key) => response.set(key, value))
16
+
12
17
  if (message.body instanceof Readable)
13
18
  stream(message, request, response)
14
19
  else
@@ -36,7 +41,7 @@ export async function read (request: Request): Promise<any> {
36
41
  }
37
42
 
38
43
  function send (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
39
- if (message.body === undefined) {
44
+ if (message.body === undefined || message.body === null) {
40
45
  response.end()
41
46
 
42
47
  return
@@ -99,6 +104,7 @@ export interface IncomingMessage extends Request {
99
104
  query: Query
100
105
  parse: <T> () => Promise<T>
101
106
  encoder: Format | null
107
+ subtype: string | null
102
108
  pipelines: {
103
109
  body: Array<(input: unknown) => unknown>
104
110
  response: Array<(output: OutgoingMessage) => void>
@@ -1,9 +1,7 @@
1
- import type { OutgoingMessage } from '../../HTTP'
2
1
  import type { Input, Output } from '../../io'
3
- import type { Family } from '../../Directive'
4
2
  import type { Interceptor } from '../../Interception'
5
3
 
6
- export class CORS implements Family, Interceptor {
4
+ export class CORS implements Interceptor {
7
5
  public readonly name = 'cors'
8
6
  public readonly mandatory = true
9
7
 
@@ -18,35 +16,39 @@ export class CORS implements Family, Interceptor {
18
16
  vary: 'origin'
19
17
  })
20
18
 
21
- public create (): null {
22
- return null
23
- }
19
+ public intercept (input: Input): Output {
20
+ const origin = input.headers.origin
24
21
 
25
- public settle (_: unknown[], input: Input, output: OutgoingMessage): void {
26
- if (input.headers.origin === undefined)
27
- return
22
+ if (origin === undefined)
23
+ return null
28
24
 
29
- output.headers ??= new Headers()
30
- output.headers.set('access-control-allow-origin', input.headers.origin)
31
- output.headers.set('access-control-expose-headers',
32
- 'authorization, content-type, content-length, etag')
33
- output.headers.append('vary', 'origin')
34
- }
25
+ if (input.method === 'OPTIONS')
26
+ return this.preflightResponse(origin)
35
27
 
36
- public intercept (input: Input): Output {
37
- if (input.method !== 'OPTIONS' || input.headers.origin === undefined)
38
- return null
28
+ input.pipelines.response.push((output) => {
29
+ output.headers ??= new Headers()
30
+ output.headers.set('access-control-allow-origin', origin)
31
+ output.headers.set('access-control-expose-headers',
32
+ 'authorization, content-type, content-length, etag')
39
33
 
40
- this.headers.set('access-control-allow-origin', input.headers.origin)
34
+ if (input.method === 'GET' || input.method === 'HEAD' || input.method === 'OPTIONS')
35
+ output.headers.append('vary', 'origin')
36
+ })
41
37
 
42
- return {
43
- status: 204,
44
- headers: this.headers
45
- }
38
+ return null
46
39
  }
47
40
 
48
41
  public allowHeader (header: string): void {
49
42
  this.allowedHeaders.add(header.toLowerCase())
50
43
  this.headers.set('access-control-allow-headers', Array.from(this.allowedHeaders).join(', '))
51
44
  }
45
+
46
+ private preflightResponse (origin: string): Output {
47
+ this.headers.set('access-control-allow-origin', origin)
48
+
49
+ return {
50
+ status: 204,
51
+ headers: this.headers
52
+ }
53
+ }
52
54
  }
@@ -7,5 +7,5 @@ import { vary } from './vary'
7
7
  import type { Family } from '../Directive'
8
8
  import type { Interceptor } from '../Interception'
9
9
 
10
- export const families: Family[] = [authorization, cache, octets, cors, vary, dev]
10
+ export const families: Family[] = [authorization, cache, octets, vary, dev]
11
11
  export const interceptors: Interceptor[] = [cors]
@@ -1,18 +1,27 @@
1
+ import { Readable } from 'stream'
1
2
  import { NotFound } from '../../HTTP'
2
3
  import * as schemas from './schemas'
4
+ import { Workflow } from './workflow'
5
+ import type { Unit } from './workflow'
3
6
  import type { Maybe } from '@toa.io/types'
4
7
  import type { Component } from '@toa.io/core'
5
8
  import type { Output } from '../../io'
6
9
  import type { Directive, Input } from './types'
10
+ import type { Remotes } from '../../Remotes'
11
+ import type { Entry } from '@toa.io/extensions.storages'
7
12
 
8
13
  export class Delete implements Directive {
9
14
  public readonly targeted = true
10
15
 
16
+ private readonly workflow?: Workflow
11
17
  private readonly discovery: Promise<Component>
12
18
  private storage: Component | null = null
13
19
 
14
- public constructor (value: null, discovery: Promise<Component>) {
15
- schemas.remove.validate(value)
20
+ public constructor (options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
21
+ schemas.remove.validate(options)
22
+
23
+ if (options?.workflow !== undefined)
24
+ this.workflow = new Workflow(options.workflow, remotes)
16
25
 
17
26
  this.discovery = discovery
18
27
  }
@@ -20,12 +29,42 @@ export class Delete implements Directive {
20
29
  public async apply (storage: string, request: Input): Promise<Output> {
21
30
  this.storage ??= await this.discovery
22
31
 
23
- const input = { storage, path: request.path }
24
- const error = await this.storage.invoke<Maybe<unknown>>('delete', { input })
32
+ const entry = await this.storage.invoke<Maybe<Entry>>('get',
33
+ { input: { storage, path: request.url } })
25
34
 
26
- if (error instanceof Error)
35
+ if (entry instanceof Error)
27
36
  throw new NotFound()
28
37
 
29
- return {}
38
+ const output: Output = {}
39
+
40
+ if (this.workflow !== undefined) {
41
+ output.status = 202
42
+ output.body = Readable.from(this.execute(request, storage, entry))
43
+ } else
44
+ await this.delete(storage, request)
45
+
46
+ return output
30
47
  }
48
+
49
+ private async delete (storage: string, request: Input): Promise<void> {
50
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
51
+ await this.storage!.invoke('delete',
52
+ { input: { storage, path: request.url } })
53
+ }
54
+
55
+ private async * execute (request: Input, storage: string, entry: Entry): AsyncGenerator {
56
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57
+ for await (const chunk of this.workflow!.execute(request, storage, entry)) {
58
+ yield chunk
59
+
60
+ if (typeof chunk === 'object' && chunk !== null && 'error' in chunk)
61
+ return
62
+ }
63
+
64
+ await this.delete(storage, request)
65
+ }
66
+ }
67
+
68
+ export interface Options {
69
+ workflow?: Unit[] | Unit
31
70
  }
@@ -12,11 +12,11 @@ import type { Directive, Input } from './types'
12
12
  export class Fetch implements Directive {
13
13
  public readonly targeted = true
14
14
 
15
- private readonly permissions: Permissions = { blob: true, meta: false }
15
+ private readonly permissions: Required<Permissions> = { blob: true, meta: false }
16
16
  private readonly discovery: Promise<Component>
17
17
  private storage: Component = null as unknown as Component
18
18
 
19
- public constructor (permissions: Partial<Permissions> | null, discovery: Promise<Component>) {
19
+ public constructor (permissions: Permissions | null, discovery: Promise<Component>) {
20
20
  schemas.fetch.validate(permissions)
21
21
 
22
22
  Object.assign(this.permissions, permissions)
@@ -26,19 +26,22 @@ export class Fetch implements Directive {
26
26
  public async apply (storage: string, request: Input): Promise<Output> {
27
27
  this.storage ??= await this.discovery
28
28
 
29
- if (request.url.slice(-5) === ':meta')
30
- return await this.get(storage, request)
31
- else
32
- return await this.fetch(storage, request)
33
- }
29
+ const variant = posix.basename(request.url).includes('.')
30
+ const metadata = request.subtype === 'octets.entry'
34
31
 
35
- private async fetch (storage: string, request: Input): Promise<Output> {
36
- const filename = posix.basename(request.url)
37
- const variant = filename.includes('.')
32
+ if (!variant && metadata)
33
+ if (this.permissions.meta)
34
+ return this.get(storage, request)
35
+ else
36
+ throw new Forbidden('Metadata is not accessible.')
38
37
 
39
38
  if (!variant && !this.permissions.blob)
40
39
  throw new Forbidden('BLOB variant must be specified.')
41
40
 
41
+ return await this.fetch(storage, request)
42
+ }
43
+
44
+ private async fetch (storage: string, request: Input): Promise<Output> {
42
45
  if ('if-none-match' in request.headers)
43
46
  return { status: 304 }
44
47
 
@@ -58,11 +61,7 @@ export class Fetch implements Directive {
58
61
  }
59
62
 
60
63
  private async get (storage: string, request: Input): Promise<Output> {
61
- if (!this.permissions.meta)
62
- throw new Forbidden('Metadata is not accessible.')
63
-
64
- const path = request.url.slice(0, -5)
65
- const input = { storage, path }
64
+ const input = { storage, path: request.url }
66
65
  const entry = await this.storage.invoke<Maybe<Entry>>('get', { input })
67
66
 
68
67
  if (entry instanceof Error)
@@ -72,9 +71,9 @@ export class Fetch implements Directive {
72
71
  }
73
72
  }
74
73
 
75
- interface Permissions {
76
- blob: boolean
77
- meta: boolean
74
+ export interface Permissions {
75
+ blob?: boolean
76
+ meta?: boolean
78
77
  }
79
78
 
80
79
  interface FetchResult {
@@ -1,5 +1,7 @@
1
- import { NotFound } from '../../HTTP'
1
+ import { posix } from 'node:path'
2
+ import { Forbidden, NotFound } from '../../HTTP'
2
3
  import * as schemas from './schemas'
4
+ import type { Entry } from '@toa.io/extensions.storages'
3
5
  import type { Maybe } from '@toa.io/types'
4
6
  import type { Component } from '@toa.io/core'
5
7
  import type { Output } from '../../io'
@@ -9,24 +11,52 @@ import type { Directive, Input } from './types'
9
11
  export class List implements Directive {
10
12
  public readonly targeted = false
11
13
 
14
+ private readonly permissions: Required<Permissions> = { meta: false }
12
15
  private readonly discovery: Promise<Component>
13
16
  private storage: Component | null = null
14
17
 
15
- public constructor (value: null, discovery: Promise<Component>) {
16
- schemas.remove.validate(value)
18
+ public constructor (permissions: Permissions | null, discovery: Promise<Component>) {
19
+ schemas.list.validate(permissions)
17
20
 
21
+ Object.assign(this.permissions, permissions)
18
22
  this.discovery = discovery
19
23
  }
20
24
 
21
25
  public async apply (storage: string, request: Input): Promise<Output> {
22
26
  this.storage ??= await this.discovery
23
27
 
24
- const input = { storage, path: request.path }
25
- const list = await this.storage.invoke<Maybe<unknown>>('list', { input })
28
+ const metadata = request.subtype === 'octets.entries'
29
+
30
+ if (metadata && !this.permissions.meta)
31
+ throw new Forbidden('Metadata is not accessible.')
32
+
33
+ const input = { storage, path: request.url }
34
+ const list = await this.storage.invoke<Maybe<string[]>>('list', { input })
26
35
 
27
36
  if (list instanceof Error)
28
37
  throw new NotFound()
29
38
 
30
- return { body: list }
39
+ const body = metadata
40
+ ? await this.expand(storage, request.url, list)
41
+ : list
42
+
43
+ return { body }
31
44
  }
45
+
46
+ private async expand (storage: string, prefix: string, list: string[]):
47
+ Promise<Array<Maybe<Entry>>> {
48
+ const promises = list.map(async (id) => {
49
+ const path = posix.join(prefix, id)
50
+ const input = { storage, path }
51
+
52
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ensured in `apply`
53
+ return this.storage!.invoke<Maybe<Entry>>('get', { input })
54
+ })
55
+
56
+ return await Promise.all(promises)
57
+ }
58
+ }
59
+
60
+ export interface Permissions {
61
+ meta?: boolean
32
62
  }
@@ -13,7 +13,7 @@ export class Permute implements Directive {
13
13
  private storage: Component | null = null
14
14
 
15
15
  public constructor (value: null, discovery: Promise<Component>) {
16
- schemas.remove.validate(value)
16
+ schemas.permute.validate(value)
17
17
 
18
18
  this.discovery = discovery
19
19
  }
@@ -24,7 +24,7 @@ export class Permute implements Directive {
24
24
  if (request.encoder === null)
25
25
  throw new NotAcceptable()
26
26
 
27
- const path = request.path
27
+ const path = request.url
28
28
  const list = await request.parse()
29
29
  const input = { storage, path, list }
30
30
  const error = await this.storage.invoke<Maybe<unknown>>('permute', { input })
@@ -1,10 +1,9 @@
1
- import { Readable } from 'node:stream'
2
- import { posix } from 'node:path'
3
1
  import { match } from 'matchacho'
4
- import { promex } from '@toa.io/generic'
5
2
  import { BadRequest, UnsupportedMediaType } from '../../HTTP'
3
+ import { cors } from '../cors'
6
4
  import * as schemas from './schemas'
7
- import type { Maybe } from '@toa.io/types'
5
+ import { Workflow } from './workflow'
6
+ import type { Unit } from './workflow'
8
7
  import type { Entry } from '@toa.io/extensions.storages'
9
8
  import type { Remotes } from '../../Remotes'
10
9
  import type { ErrorType } from 'error-value'
@@ -15,11 +14,9 @@ import type { Directive, Input } from './types'
15
14
  export class Store implements Directive {
16
15
  public readonly targeted = false
17
16
 
18
- private readonly accept: string | undefined
19
- private readonly workflow: Workflow | undefined
17
+ private readonly accept?: string
18
+ private readonly workflow?: Workflow
20
19
  private readonly discovery: Record<string, Promise<Component>> = {}
21
- private readonly remotes: Remotes
22
- private readonly components: Record<string, Component> = {}
23
20
  private storage: Component | null = null
24
21
 
25
22
  public constructor
@@ -31,19 +28,26 @@ export class Store implements Directive {
31
28
  Array, (types: string[]) => types.join(','),
32
29
  undefined)
33
30
 
34
- this.workflow = match(options?.workflow,
35
- Array, (units: Unit[]) => units,
36
- Object, (unit: Unit) => [unit],
37
- undefined)
31
+ if (options?.workflow !== undefined)
32
+ this.workflow = new Workflow(options.workflow, remotes)
38
33
 
39
34
  this.discovery.storage = discovery
40
- this.remotes = remotes
35
+
36
+ cors.allowHeader('content-meta')
41
37
  }
42
38
 
43
39
  public async apply (storage: string, request: Input): Promise<Output> {
44
40
  this.storage ??= await this.discovery.storage
45
41
 
46
- const input = { storage, request, accept: this.accept }
42
+ const input: StoreInput = { storage, request }
43
+ const meta = request.headers['content-meta']
44
+
45
+ if (this.accept !== undefined)
46
+ input.accept = this.accept
47
+
48
+ if (meta !== undefined)
49
+ input.meta = this.parseMeta(meta)
50
+
47
51
  const entry = await this.storage.invoke<Entry>('store', { input })
48
52
 
49
53
  return match<Output>(entry,
@@ -54,7 +58,7 @@ export class Store implements Directive {
54
58
  private reply (request: Input, storage: string, entry: Entry): Output {
55
59
  const body = this.workflow === undefined
56
60
  ? entry
57
- : Readable.from(this.execute(request, storage, entry))
61
+ : this.workflow.execute(request, storage, entry)
58
62
 
59
63
  return { body }
60
64
  }
@@ -66,93 +70,31 @@ export class Store implements Directive {
66
70
  error)
67
71
  }
68
72
 
69
- /* eslint-disable no-useless-return, max-depth */
70
-
71
- /**
72
- * Execute workflow units sequentially, steps within a unit in parallel.
73
- * Yield results as soon as they come.
74
- *
75
- * If you need to change this, it may take a while.
76
- */
77
- private async * execute (request: Input, storage: string, entry: Entry): AsyncGenerator {
78
- yield entry
79
-
80
- const path = posix.join(request.path, entry.id)
81
- let interrupted = false
82
-
83
- for (const unit of this.workflow!) {
84
- if (interrupted)
85
- break
86
-
87
- const steps = Object.keys(unit)
88
-
89
- // unit result promises queue
90
- const results = Array.from(steps, promex<unknown>)
91
- let next = 0
92
-
93
- // execute steps in parallel
94
- for (const step of steps)
95
- // these promises are indirectly awaited in the yield loop
96
- void (async () => {
97
- const endpoint = unit[step]
98
- const context: Context = { storage, path, entry }
99
- const result = await this.call(endpoint, context)
100
-
101
- if (interrupted)
102
- return
103
-
104
- // as a result is received, resolve the next promise from the queue
105
- const promise = results[next++]
106
-
107
- if (result instanceof Error) {
108
- interrupted = true
109
- promise.resolve({ error: { step, ...result } })
110
-
111
- // cancel pending promises
112
- results[next].resolve(null)
113
- } else
114
- promise.resolve({ [step]: result ?? null })
115
- })().catch((e) => results[next].reject(e))
116
-
117
- // yield results from the queue as they come
118
- for (const promise of results) {
119
- const result = await promise
120
-
121
- if (result === null) // canceled promise
122
- break
123
- else
124
- yield result
125
- }
126
- }
127
- }
128
-
129
- private async call (endpoint: string, context: Context): Promise<Maybe<unknown>> {
130
- const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
131
- const key = `${namespace}.${component}`
73
+ private parseMeta (value: string | string[]): Record<string, string> {
74
+ if (Array.isArray(value))
75
+ value = value.join(',')
132
76
 
133
- this.components[key] ??= await this.discover(key, namespace, component)
77
+ const meta: Record<string, string> = {}
134
78
 
135
- return await this.components[key].invoke(operation, { input: context })
136
- }
79
+ for (const pair of value.split(',')) {
80
+ const eq = pair.indexOf('=')
81
+ const key = (eq === -1 ? pair : pair.slice(0, eq)).trim()
137
82
 
138
- private async discover (key: string, namespace: string, component: string): Promise<Component> {
139
- if (this.discovery[key] === undefined)
140
- this.discovery[key] = this.remotes.discover(namespace, component)
83
+ meta[key] = eq === -1 ? 'true' : pair.slice(eq + 1).trim()
84
+ }
141
85
 
142
- return await this.discovery[key]
86
+ return meta
143
87
  }
144
88
  }
145
89
 
146
- type Unit = Record<string, string>
147
- type Workflow = Unit[]
148
-
149
- interface Options {
150
- accept: string | string[]
151
- workflow: Workflow | Unit
90
+ export interface Options {
91
+ accept?: string | string[]
92
+ workflow?: Unit[] | Unit
152
93
  }
153
94
 
154
- interface Context {
95
+ interface StoreInput {
155
96
  storage: string
156
- path: string
157
- entry: Entry
97
+ request: Input
98
+ accept?: string
99
+ meta?: Record<string, string>
158
100
  }
@@ -1,12 +1,17 @@
1
1
  import { resolve } from 'node:path'
2
2
  import schemas from '@toa.io/schemas'
3
+ import type { Permissions as FetchPermissions } from './Fetch'
4
+ import type { Permissions as ListPermissions } from './List'
5
+ import type { Options as StoreOptions } from './Store'
6
+ import type { Options as DeleteOptions } from './Delete'
7
+ import type { Schema } from '@toa.io/schemas'
3
8
 
4
9
  const path = resolve(__dirname, '../../../schemas/octets')
5
10
  const namespace = schemas.namespace(path)
6
11
 
7
- export const context = namespace.schema('context')
8
- export const store = namespace.schema('store')
9
- export const fetch = namespace.schema('fetch')
10
- export const remove = namespace.schema('delete')
11
- export const list = namespace.schema('list')
12
- export const permute = namespace.schema('permute')
12
+ export const context: Schema<string> = namespace.schema('context')
13
+ export const store: Schema<StoreOptions | null> = namespace.schema('store')
14
+ export const fetch: Schema<FetchPermissions | null> = namespace.schema('fetch')
15
+ export const remove: Schema<DeleteOptions | null> = namespace.schema('delete')
16
+ export const list: Schema<ListPermissions | null> = namespace.schema('list')
17
+ export const permute: Schema<null> = namespace.schema('permute')