@toa.io/extensions.exposition 1.0.0-alpha.54 → 1.0.0-alpha.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/documentation/introspection.md +37 -23
- package/documentation/query.md +21 -0
- package/features/identity.basic.feature +2 -0
- package/features/introspection.feature +78 -1
- package/features/io.feature +0 -1
- package/features/query.feature +24 -0
- package/features/steps/components/echo/manifest.toa.yaml +4 -2
- package/package.json +9 -8
- package/schemas/query.cos.yaml +4 -10
- package/source/Endpoint.ts +32 -2
- package/source/Gateway.ts +5 -11
- package/source/HTTP/Server.ts +1 -2
- package/source/Introspection.ts +11 -0
- package/source/Mapping.ts +56 -19
- package/source/Query.test.ts +3 -3
- package/source/Query.ts +90 -17
- package/source/RTD/Endpoint.ts +1 -1
- package/source/RTD/Method.ts +15 -2
- package/source/RTD/Node.ts +19 -11
- package/source/RTD/syntax/types.ts +3 -2
- package/source/Remotes.ts +0 -7
- package/source/directives/io/Input.ts +2 -2
- package/source/manifest.ts +1 -1
- package/transpiled/Endpoint.d.ts +2 -1
- package/transpiled/Endpoint.js +22 -2
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Gateway.js +5 -8
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Server.js +1 -2
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/Introspection.d.ts +9 -0
- package/transpiled/Introspection.js +3 -0
- package/transpiled/Introspection.js.map +1 -0
- package/transpiled/Mapping.d.ts +9 -1
- package/transpiled/Mapping.js +44 -18
- package/transpiled/Mapping.js.map +1 -1
- package/transpiled/Query.d.ts +10 -2
- package/transpiled/Query.js +67 -15
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Endpoint.d.ts +1 -1
- package/transpiled/RTD/Method.d.ts +4 -1
- package/transpiled/RTD/Method.js +11 -2
- package/transpiled/RTD/Method.js.map +1 -1
- package/transpiled/RTD/Node.d.ts +2 -1
- package/transpiled/RTD/Node.js +14 -10
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/syntax/types.d.ts +3 -2
- package/transpiled/RTD/syntax/types.js.map +1 -1
- package/transpiled/Remotes.d.ts +0 -2
- package/transpiled/Remotes.js +0 -5
- package/transpiled/Remotes.js.map +1 -1
- package/transpiled/directives/io/Input.js +2 -2
- package/transpiled/directives/io/Input.js.map +1 -1
- package/transpiled/manifest.js +1 -1
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
package/schemas/query.cos.yaml
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
_: true
|
|
2
1
|
id?: string
|
|
3
2
|
criteria?: string
|
|
4
3
|
sort?: string
|
|
5
|
-
omit
|
|
4
|
+
omit?:
|
|
6
5
|
$ref: range
|
|
7
|
-
|
|
8
|
-
value: 0
|
|
9
|
-
range: [0, 1000]
|
|
10
|
-
limit:
|
|
6
|
+
limit?:
|
|
11
7
|
$ref: range
|
|
12
|
-
default:
|
|
13
|
-
value: 10
|
|
14
|
-
range: [1, 1000]
|
|
15
|
-
required: [value]
|
|
16
8
|
selectors?: [string]
|
|
17
9
|
projection?: [string]
|
|
10
|
+
parameters?: [string]
|
|
11
|
+
_: true
|
package/source/Endpoint.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Mapping } from './Mapping'
|
|
2
2
|
import * as http from './HTTP'
|
|
3
|
+
import type { Introspection, Schema } from './Introspection'
|
|
3
4
|
import type { Remote } from '@toa.io/core'
|
|
4
5
|
import type { Remotes } from './Remotes'
|
|
5
6
|
import type { Context } from './Context'
|
|
@@ -53,10 +54,39 @@ export class Endpoint implements RTD.Endpoint {
|
|
|
53
54
|
return message
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
public async explain (): Promise<
|
|
57
|
+
public async explain (parameters: RTD.Parameter[]): Promise<Introspection> {
|
|
57
58
|
this.remote ??= await this.discovery
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
61
|
+
const operation = await this.remote.explain(this.endpoint)
|
|
62
|
+
|
|
63
|
+
let route: Record<string, Schema> | null = null
|
|
64
|
+
|
|
65
|
+
if (operation.input?.type === 'object')
|
|
66
|
+
for (const parameter of parameters) {
|
|
67
|
+
const schema = operation.input.properties[parameter.name]
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line max-depth
|
|
70
|
+
if (schema !== undefined) {
|
|
71
|
+
route ??= {}
|
|
72
|
+
route[parameter.name] = schema
|
|
73
|
+
|
|
74
|
+
delete operation.input.properties[parameter.name]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const query = this.mapping.explain(operation)
|
|
79
|
+
const introspection: Introspection = {}
|
|
80
|
+
|
|
81
|
+
if (route !== null)
|
|
82
|
+
introspection.route = route
|
|
83
|
+
|
|
84
|
+
if (query !== null)
|
|
85
|
+
introspection.query = query
|
|
86
|
+
|
|
87
|
+
Object.assign(introspection, operation)
|
|
88
|
+
|
|
89
|
+
return introspection
|
|
60
90
|
}
|
|
61
91
|
|
|
62
92
|
public async close (): Promise<void> {
|
package/source/Gateway.ts
CHANGED
|
@@ -32,7 +32,7 @@ export class Gateway extends Connector {
|
|
|
32
32
|
const { node, parameters } = this.match(context)
|
|
33
33
|
|
|
34
34
|
if (context.request.method === 'OPTIONS')
|
|
35
|
-
return await this.explain(node)
|
|
35
|
+
return await this.explain(node, parameters)
|
|
36
36
|
|
|
37
37
|
if (!(context.request.method in node.methods))
|
|
38
38
|
throw new http.MethodNotAllowed()
|
|
@@ -101,18 +101,12 @@ export class Gateway extends Connector {
|
|
|
101
101
|
.catch(rethrow) as http.OutgoingMessage
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
private async explain (node: Node): Promise<http.OutgoingMessage> {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const explaining = Object.entries(node.methods)
|
|
108
|
-
.map(async ([verb, method]) => (methods[verb] = await method.explain()))
|
|
109
|
-
|
|
110
|
-
await Promise.all(explaining)
|
|
111
|
-
|
|
112
|
-
const allow = [...Object.keys(node.methods), 'OPTIONS'].join(', ')
|
|
104
|
+
private async explain (node: Node, parameters: Parameter[]): Promise<http.OutgoingMessage> {
|
|
105
|
+
const body = await node.explain(parameters)
|
|
106
|
+
const allow = [...Object.keys(node.methods)].join(', ')
|
|
113
107
|
const headers = new Headers({ allow })
|
|
114
108
|
|
|
115
|
-
return { body
|
|
109
|
+
return { body, headers }
|
|
116
110
|
}
|
|
117
111
|
|
|
118
112
|
private async discover (): Promise<void> {
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -117,9 +117,8 @@ export class Server extends Connector {
|
|
|
117
117
|
response.statusCode = exception instanceof Exception ? exception.status : 500
|
|
118
118
|
|
|
119
119
|
const message: OutgoingMessage = { status: response.statusCode }
|
|
120
|
-
const verbose = exception instanceof ClientError || this.properties.debug
|
|
121
120
|
|
|
122
|
-
if (
|
|
121
|
+
if (exception instanceof ClientError || this.properties.debug)
|
|
123
122
|
message.body =
|
|
124
123
|
exception instanceof Exception
|
|
125
124
|
? exception.body
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Remote } from '@toa.io/core'
|
|
2
|
+
|
|
3
|
+
export interface Introspection {
|
|
4
|
+
route?: Record<string, Schema>
|
|
5
|
+
query?: Record<string, Schema>
|
|
6
|
+
input?: Schema
|
|
7
|
+
output?: Schema
|
|
8
|
+
errors?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Schema = Awaited<ReturnType<Remote['explain']>>['input']
|
package/source/Mapping.ts
CHANGED
|
@@ -1,51 +1,79 @@
|
|
|
1
1
|
import { type Parameter } from './RTD'
|
|
2
2
|
import { Query } from './Query'
|
|
3
|
+
import type { Introspection, Schema } from './Introspection'
|
|
4
|
+
import type { QueryString } from './Query'
|
|
3
5
|
import type * as http from './HTTP'
|
|
4
6
|
import type * as syntax from './RTD/syntax'
|
|
5
7
|
import type * as core from '@toa.io/core'
|
|
6
8
|
|
|
7
9
|
export abstract class Mapping {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
protected readonly query: Query
|
|
11
|
+
|
|
12
|
+
public constructor (query: Query) {
|
|
13
|
+
this.query = query
|
|
14
|
+
}
|
|
11
15
|
|
|
16
|
+
public static create (query?: syntax.Query): Mapping {
|
|
12
17
|
const q = new Query(query)
|
|
13
18
|
|
|
14
|
-
return
|
|
19
|
+
return queryable(query)
|
|
20
|
+
? new QueryableMapping(q)
|
|
21
|
+
: new InputMapping(q)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public explain (introspection: Introspection): Record<string, Schema> | null {
|
|
25
|
+
return this.query.explain(introspection)
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
protected assign (input: any, qs: QueryString): void {
|
|
29
|
+
if (qs.parameters !== null) {
|
|
30
|
+
if (typeof input !== 'object' || input === null)
|
|
31
|
+
throw new Error('Input must be an object to embed query parameters')
|
|
32
|
+
|
|
33
|
+
Object.assign(input, qs.parameters)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public abstract fit (input: any, query: http.Query, parameters: Parameter[]): core.Request
|
|
18
38
|
}
|
|
19
39
|
|
|
20
40
|
class QueryableMapping extends Mapping {
|
|
21
|
-
|
|
41
|
+
public fit (input: any, query: http.Query, parameters: Parameter[]): core.Request {
|
|
42
|
+
const request: core.Request = {}
|
|
43
|
+
const qs = this.query.fit(query, parameters)
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
if (input === undefined && qs.parameters !== null)
|
|
46
|
+
input = {}
|
|
25
47
|
|
|
26
|
-
this.
|
|
27
|
-
}
|
|
48
|
+
this.assign(input, qs)
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
|
|
50
|
+
if (input !== undefined)
|
|
51
|
+
request.input = input
|
|
31
52
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
if (qs.query !== null)
|
|
54
|
+
request.query = qs.query
|
|
55
|
+
|
|
56
|
+
return request
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
59
|
|
|
39
60
|
class InputMapping extends Mapping {
|
|
40
|
-
public fit (input: any,
|
|
61
|
+
public fit (input: any, query: http.Query, parameters: Parameter[]): core.Request {
|
|
41
62
|
const request: core.Request = {}
|
|
63
|
+
const qs = this.query.fit(query, parameters)
|
|
42
64
|
|
|
43
|
-
if (input === undefined && parameters.length > 0)
|
|
65
|
+
if (input === undefined && (parameters.length > 0 || qs.parameters !== null))
|
|
44
66
|
input = {}
|
|
45
67
|
|
|
46
|
-
if (
|
|
68
|
+
if (parameters.length > 0) {
|
|
69
|
+
if (typeof input !== 'object' || input === null)
|
|
70
|
+
throw new Error('Input must be an object to embed route parameters')
|
|
71
|
+
|
|
47
72
|
for (const parameter of parameters)
|
|
48
73
|
input[parameter.name] = parameter.value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.assign(input, qs)
|
|
49
77
|
|
|
50
78
|
if (input !== undefined)
|
|
51
79
|
request.input = input
|
|
@@ -53,3 +81,12 @@ class InputMapping extends Mapping {
|
|
|
53
81
|
return request
|
|
54
82
|
}
|
|
55
83
|
}
|
|
84
|
+
|
|
85
|
+
export function queryable (query?: syntax.Query): boolean {
|
|
86
|
+
if (query === undefined || query === null)
|
|
87
|
+
return false
|
|
88
|
+
|
|
89
|
+
const keys = Object.keys(query)
|
|
90
|
+
|
|
91
|
+
return !(keys.length === 1 && keys[0] === 'parameters')
|
|
92
|
+
}
|
package/source/Query.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ it('should combine request criteria', async () => {
|
|
|
16
16
|
const instance = new Query(query)
|
|
17
17
|
const result = instance.fit({ criteria: 'qux==4' }, parameters)
|
|
18
18
|
|
|
19
|
-
expect(result.criteria).toStrictEqual('(bar==2;baz==3);(foo==1);(qux==4)')
|
|
19
|
+
expect(result.query!.criteria).toStrictEqual('(bar==2;baz==3);(foo==1);(qux==4)')
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
it('should set id parameter as query.id', async () => {
|
|
@@ -32,6 +32,6 @@ it('should set id parameter as query.id', async () => {
|
|
|
32
32
|
const instance = new Query(query)
|
|
33
33
|
const result = instance.fit({}, parameters)
|
|
34
34
|
|
|
35
|
-
expect(result.criteria).toBeUndefined()
|
|
36
|
-
expect(result.id).toStrictEqual(id)
|
|
35
|
+
expect(result.query!.criteria).toBeUndefined()
|
|
36
|
+
expect(result.query!.id).toStrictEqual(id)
|
|
37
37
|
})
|
package/source/Query.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
1
2
|
import * as http from './HTTP'
|
|
2
3
|
import { type Parameter } from './RTD'
|
|
3
4
|
import * as schemas from './schemas'
|
|
5
|
+
import { queryable } from './Mapping'
|
|
6
|
+
import type { Introspection, Schema } from './Introspection'
|
|
4
7
|
import type * as syntax from './RTD/syntax'
|
|
5
8
|
import type * as core from '@toa.io/core'
|
|
6
9
|
|
|
@@ -8,35 +11,97 @@ export class Query {
|
|
|
8
11
|
private readonly query: syntax.Query
|
|
9
12
|
private readonly closed: boolean = false
|
|
10
13
|
private readonly prepend: ',' | ';' = ';'
|
|
14
|
+
private readonly queryable: boolean
|
|
11
15
|
|
|
12
|
-
public constructor (query: syntax.Query) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
public constructor (query: syntax.Query = {}) {
|
|
17
|
+
this.queryable = queryable(query)
|
|
18
|
+
|
|
19
|
+
if (this.queryable) {
|
|
20
|
+
query.omit ??= { value: 0, range: [0, 1000] }
|
|
21
|
+
query.limit ??= { value: 10, range: [1, 100] }
|
|
18
22
|
|
|
19
|
-
if (query.criteria
|
|
20
|
-
|
|
23
|
+
if (query.criteria !== undefined) {
|
|
24
|
+
// eslint-disable-next-line max-depth
|
|
25
|
+
if (query.criteria.endsWith(';'))
|
|
26
|
+
query.criteria = query.criteria.slice(0, -1)
|
|
27
|
+
else
|
|
28
|
+
this.closed = true
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
// eslint-disable-next-line max-depth
|
|
31
|
+
if (query.criteria.startsWith(',') || query.criteria.startsWith(';')) {
|
|
32
|
+
this.prepend = query.criteria[0] as ',' | ';'
|
|
33
|
+
|
|
34
|
+
query.criteria = query.criteria.slice(1)
|
|
35
|
+
}
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
this.query = query
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
public fit (query: http.Query, parameters: Parameter[]):
|
|
30
|
-
const
|
|
42
|
+
public fit (query: http.Query, parameters: Parameter[]): QueryString {
|
|
43
|
+
const qs = this.split(query)
|
|
31
44
|
|
|
32
|
-
if (
|
|
33
|
-
|
|
45
|
+
if (qs.query !== null) {
|
|
46
|
+
const error = schemas.querystring.fit(qs.query)
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.fitSort(query)
|
|
48
|
+
if (error !== null)
|
|
49
|
+
throw new http.BadRequest('Query ' + error.message)
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
this.fitCriteria(qs.query, parameters)
|
|
52
|
+
this.fitRanges(qs.query)
|
|
53
|
+
this.fitSort(qs.query)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
query: qs.query as core.Query,
|
|
58
|
+
parameters: qs.parameters
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public explain (introspection: Introspection): Record<string, Schema> | null {
|
|
63
|
+
if (this.query?.parameters === undefined || introspection.input?.type !== 'object')
|
|
64
|
+
return null
|
|
65
|
+
|
|
66
|
+
let query: Record<string, Schema> | null = null
|
|
67
|
+
|
|
68
|
+
for (const parameter of this.query.parameters) {
|
|
69
|
+
const schema = introspection.input.properties[parameter]
|
|
70
|
+
|
|
71
|
+
if (schema !== undefined) {
|
|
72
|
+
query ??= {}
|
|
73
|
+
query[parameter] = schema
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
delete introspection.input.properties[parameter]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return query
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private split (query: http.Query): {
|
|
83
|
+
query: http.Query | null
|
|
84
|
+
parameters: Record<string, string> | null
|
|
85
|
+
} {
|
|
86
|
+
let parameters: Record<string, string> | null = null
|
|
87
|
+
|
|
88
|
+
if (this.query?.parameters !== undefined)
|
|
89
|
+
for (const key in query)
|
|
90
|
+
// eslint-disable-next-line max-depth
|
|
91
|
+
if (this.query.parameters.includes(key)) {
|
|
92
|
+
parameters ??= {}
|
|
93
|
+
parameters[key] = query[key] as string
|
|
94
|
+
|
|
95
|
+
delete query[key]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!this.queryable && Object.keys(query).length === 0)
|
|
99
|
+
query = null!
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
query,
|
|
103
|
+
parameters
|
|
104
|
+
}
|
|
40
105
|
}
|
|
41
106
|
|
|
42
107
|
private fitCriteria (query: http.Query, parameters: Parameter[]): void {
|
|
@@ -77,6 +142,9 @@ export class Query {
|
|
|
77
142
|
private fitRanges (qs: http.Query): void {
|
|
78
143
|
const query = qs as core.Query
|
|
79
144
|
|
|
145
|
+
assert.ok(this.query.limit !== undefined, 'Query limit must be defined')
|
|
146
|
+
assert.ok(this.query.omit !== undefined, 'Query limit range must be defined')
|
|
147
|
+
|
|
80
148
|
if (qs.limit !== undefined)
|
|
81
149
|
query.limit = fit(qs.limit, this.query.limit.range, 'limit')
|
|
82
150
|
else
|
|
@@ -114,3 +182,8 @@ interface CriteriaGroup {
|
|
|
114
182
|
criteria: string
|
|
115
183
|
operator: ',' | ';'
|
|
116
184
|
}
|
|
185
|
+
|
|
186
|
+
export interface QueryString {
|
|
187
|
+
query: core.Query | null
|
|
188
|
+
parameters: Record<string, string> | null
|
|
189
|
+
}
|
package/source/RTD/Endpoint.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type * as RTD from './index'
|
|
|
6
6
|
export interface Endpoint {
|
|
7
7
|
call: (context: http.Context, parameters: RTD.Parameter[]) => Promise<http.OutgoingMessage>
|
|
8
8
|
|
|
9
|
-
explain: () => unknown
|
|
9
|
+
explain: (parameters: RTD.Parameter[]) => Promise<unknown>
|
|
10
10
|
|
|
11
11
|
close: () => Promise<void>
|
|
12
12
|
}
|
package/source/RTD/Method.ts
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
+
import type { Parameter } from './Match'
|
|
1
2
|
import type { Endpoint } from './Endpoint'
|
|
2
3
|
import type { Directives } from './Directives'
|
|
3
4
|
|
|
4
5
|
export class Method {
|
|
5
6
|
public readonly endpoint: Endpoint | null
|
|
6
7
|
public readonly directives: Directives
|
|
8
|
+
private introspection: unknown | null = null
|
|
9
|
+
private introspecting: Promise<unknown> | null = null
|
|
7
10
|
|
|
8
11
|
public constructor (endpoint: Endpoint | null, directives: Directives) {
|
|
9
12
|
this.endpoint = endpoint
|
|
10
13
|
this.directives = directives
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
public async explain (): Promise<unknown> {
|
|
14
|
-
|
|
16
|
+
public async explain (parameters: Parameter[]): Promise<unknown> {
|
|
17
|
+
if (this.introspection !== null)
|
|
18
|
+
return this.introspection
|
|
19
|
+
|
|
20
|
+
if (this.introspecting === null)
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
22
|
+
this.introspecting = this.endpoint?.explain(parameters)!
|
|
23
|
+
|
|
24
|
+
this.introspection = await this.introspecting
|
|
25
|
+
|
|
26
|
+
return this.introspection
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
public async close (): Promise<void> {
|
|
30
|
+
console.log('!!! Closing method')
|
|
18
31
|
await this.endpoint?.close()
|
|
19
32
|
}
|
|
20
33
|
}
|
package/source/RTD/Node.ts
CHANGED
|
@@ -35,38 +35,46 @@ export class Node {
|
|
|
35
35
|
public merge (node: Node): void {
|
|
36
36
|
this.intermediate = node.intermediate
|
|
37
37
|
|
|
38
|
-
if (
|
|
39
|
-
this.replace(node)
|
|
40
|
-
else
|
|
38
|
+
if (this.protected)
|
|
41
39
|
this.append(node)
|
|
40
|
+
else
|
|
41
|
+
this.replace(node)
|
|
42
42
|
|
|
43
43
|
this.sort()
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
public async explain (parameters: Parameter[]): Promise<Record<string, unknown>> {
|
|
47
|
+
const methods: Record<string, unknown> = {}
|
|
48
|
+
|
|
49
|
+
const explained = Object.entries(this.methods)
|
|
50
|
+
.map(async ([verb, method]) =>
|
|
51
|
+
(methods[verb] = await method.explain(parameters)))
|
|
52
|
+
|
|
53
|
+
await Promise.all(explained)
|
|
54
|
+
|
|
55
|
+
return methods
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
private replace (node: Node): void {
|
|
47
59
|
const methods = Object.values(this.methods)
|
|
48
60
|
|
|
49
61
|
this.routes = node.routes
|
|
50
62
|
this.methods = node.methods
|
|
51
63
|
|
|
64
|
+
// race condition is really unlikely
|
|
52
65
|
for (const method of methods)
|
|
53
66
|
void method.close()
|
|
54
|
-
|
|
55
|
-
// race condition is really unlikely
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
private append (node: Node): void {
|
|
59
70
|
for (const route of node.routes)
|
|
60
|
-
this.
|
|
71
|
+
this.route(route)
|
|
61
72
|
|
|
62
73
|
for (const [verb, method] of Object.entries(node.methods))
|
|
63
|
-
|
|
64
|
-
console.warn(`Overriding of the protected method ${verb} is not permitted.`)
|
|
65
|
-
else
|
|
66
|
-
this.methods[verb] = method
|
|
74
|
+
this.methods[verb] = method
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
private
|
|
77
|
+
private route (candidate: Route): void {
|
|
70
78
|
for (const route of this.routes)
|
|
71
79
|
if (candidate.equals(route)) {
|
|
72
80
|
route.merge(candidate)
|
|
@@ -35,10 +35,11 @@ export interface Query {
|
|
|
35
35
|
id?: string
|
|
36
36
|
criteria?: string
|
|
37
37
|
sort?: string
|
|
38
|
-
omit
|
|
39
|
-
limit
|
|
38
|
+
omit?: Range
|
|
39
|
+
limit?: Range
|
|
40
40
|
selectors?: string[]
|
|
41
41
|
projection?: string[]
|
|
42
|
+
parameters?: string[]
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface Range {
|
package/source/Remotes.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { type Bootloader } from './Factory'
|
|
|
3
3
|
|
|
4
4
|
export class Remotes extends Connector {
|
|
5
5
|
private readonly boot: Bootloader
|
|
6
|
-
private readonly remotes: Record<string, Promise<Remote>> = {}
|
|
7
6
|
|
|
8
7
|
public constructor (boot: Bootloader) {
|
|
9
8
|
super()
|
|
@@ -13,12 +12,6 @@ export class Remotes extends Connector {
|
|
|
13
12
|
public async discover (namespace: string, name: string): Promise<Remote> {
|
|
14
13
|
const locator = new Locator(name, namespace)
|
|
15
14
|
|
|
16
|
-
this.remotes[locator.id] ??= this.create(locator)
|
|
17
|
-
|
|
18
|
-
return await this.remotes[locator.id]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private async create (locator: Locator): Promise<Remote> {
|
|
22
15
|
const remote = await this.boot.remote(locator)
|
|
23
16
|
|
|
24
17
|
this.depends(remote)
|
|
@@ -23,13 +23,13 @@ export class Input implements Directive {
|
|
|
23
23
|
try {
|
|
24
24
|
schemas.message.validate(body)
|
|
25
25
|
} catch {
|
|
26
|
-
throw new BadRequest('Invalid request body
|
|
26
|
+
throw new BadRequest('Invalid request body')
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const property = this.violation(body)
|
|
30
30
|
|
|
31
31
|
if (property !== undefined)
|
|
32
|
-
throw new BadRequest(`Unexpected input: ${property}
|
|
32
|
+
throw new BadRequest(`Unexpected input: ${property}`)
|
|
33
33
|
|
|
34
34
|
return body
|
|
35
35
|
}
|
package/source/manifest.ts
CHANGED
|
@@ -22,7 +22,7 @@ function wrap (declaration: object, namespace: string, name: string): object {
|
|
|
22
22
|
const path = (namespace === undefined || namespace === 'default' ? '' : '/' + namespace) +
|
|
23
23
|
'/' + name
|
|
24
24
|
|
|
25
|
-
return { [path]:
|
|
25
|
+
return { [path]: declaration }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function specify (node: Node, manifest: Manifest): void {
|
package/transpiled/Endpoint.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Mapping } from './Mapping';
|
|
2
2
|
import * as http from './HTTP';
|
|
3
|
+
import type { Introspection } from './Introspection';
|
|
3
4
|
import type { Remote } from '@toa.io/core';
|
|
4
5
|
import type { Remotes } from './Remotes';
|
|
5
6
|
import type { Context } from './Context';
|
|
@@ -11,7 +12,7 @@ export declare class Endpoint implements RTD.Endpoint {
|
|
|
11
12
|
private remote;
|
|
12
13
|
constructor(endpoint: string, mapping: Mapping, discovery: Promise<Remote>);
|
|
13
14
|
call(context: http.Context, parameters: RTD.Parameter[]): Promise<http.OutgoingMessage>;
|
|
14
|
-
explain(): Promise<
|
|
15
|
+
explain(parameters: RTD.Parameter[]): Promise<Introspection>;
|
|
15
16
|
close(): Promise<void>;
|
|
16
17
|
private query;
|
|
17
18
|
private version;
|
package/transpiled/Endpoint.js
CHANGED
|
@@ -61,9 +61,29 @@ class Endpoint {
|
|
|
61
61
|
message.body = reply;
|
|
62
62
|
return message;
|
|
63
63
|
}
|
|
64
|
-
async explain() {
|
|
64
|
+
async explain(parameters) {
|
|
65
65
|
this.remote ??= await this.discovery;
|
|
66
|
-
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
67
|
+
const operation = await this.remote.explain(this.endpoint);
|
|
68
|
+
let route = null;
|
|
69
|
+
if (operation.input?.type === 'object')
|
|
70
|
+
for (const parameter of parameters) {
|
|
71
|
+
const schema = operation.input.properties[parameter.name];
|
|
72
|
+
// eslint-disable-next-line max-depth
|
|
73
|
+
if (schema !== undefined) {
|
|
74
|
+
route ??= {};
|
|
75
|
+
route[parameter.name] = schema;
|
|
76
|
+
delete operation.input.properties[parameter.name];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const query = this.mapping.explain(operation);
|
|
80
|
+
const introspection = {};
|
|
81
|
+
if (route !== null)
|
|
82
|
+
introspection.route = route;
|
|
83
|
+
if (query !== null)
|
|
84
|
+
introspection.query = query;
|
|
85
|
+
Object.assign(introspection, operation);
|
|
86
|
+
return introspection;
|
|
67
87
|
}
|
|
68
88
|
async close() {
|
|
69
89
|
this.remote ??= await this.discovery;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Endpoint.js","sourceRoot":"","sources":["../source/Endpoint.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAmC;AACnC,6CAA8B;
|
|
1
|
+
{"version":3,"file":"Endpoint.js","sourceRoot":"","sources":["../source/Endpoint.ts"],"names":[],"mappings":"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"}
|
package/transpiled/Gateway.js
CHANGED
|
@@ -48,7 +48,7 @@ class Gateway extends core_1.Connector {
|
|
|
48
48
|
return interception;
|
|
49
49
|
const { node, parameters } = this.match(context);
|
|
50
50
|
if (context.request.method === 'OPTIONS')
|
|
51
|
-
return await this.explain(node);
|
|
51
|
+
return await this.explain(node, parameters);
|
|
52
52
|
if (!(context.request.method in node.methods))
|
|
53
53
|
throw new http.MethodNotAllowed();
|
|
54
54
|
const method = node.methods[context.request.method];
|
|
@@ -91,14 +91,11 @@ class Gateway extends core_1.Connector {
|
|
|
91
91
|
.call(context, parameters)
|
|
92
92
|
.catch(exceptions_1.rethrow);
|
|
93
93
|
}
|
|
94
|
-
async explain(node) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
.map(async ([verb, method]) => (methods[verb] = await method.explain()));
|
|
98
|
-
await Promise.all(explaining);
|
|
99
|
-
const allow = [...Object.keys(node.methods), 'OPTIONS'].join(', ');
|
|
94
|
+
async explain(node, parameters) {
|
|
95
|
+
const body = await node.explain(parameters);
|
|
96
|
+
const allow = [...Object.keys(node.methods)].join(', ');
|
|
100
97
|
const headers = new Headers({ allow });
|
|
101
|
-
return { body
|
|
98
|
+
return { body, headers };
|
|
102
99
|
}
|
|
103
100
|
async discover() {
|
|
104
101
|
await this.broadcast.receive('expose', this.merge.bind(this));
|