@toa.io/extensions.exposition 1.0.0-alpha.287 → 1.0.0-alpha.289
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/CHANGELOG.md +19 -0
- package/components/identity.basic/tsconfig.tsbuildinfo +1 -1
- package/components/identity.clients/tsconfig.tsbuildinfo +1 -1
- package/components/identity.credentials/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/tsconfig.tsbuildinfo +1 -1
- package/components/identity.grants/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/manifest.toa.yaml +2 -1
- package/components/identity.passkeys/source/errors.test.ts +34 -0
- package/components/identity.passkeys/source/webauthn.test.ts +26 -0
- package/components/identity.passkeys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/types/toa.d.ts +1 -1
- package/components/identity.roles/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
- package/documentation/access.md +23 -4
- package/documentation/discovery.md +81 -0
- package/documentation/help.md +122 -0
- package/documentation/introspection.md +42 -9
- package/documentation/mcp.md +13 -16
- package/documentation/octets.md +20 -0
- package/features/cors.feature +31 -1
- package/features/dev.feature +2 -0
- package/features/discovery.feature +213 -0
- package/features/discovery.ui.feature +213 -0
- package/features/help.feature +512 -0
- package/features/introspection.feature +94 -8
- package/features/map.feature +1 -0
- package/features/mcp.feature +74 -12
- package/features/methods.feature +2 -1
- package/features/oauth.grants.feature +3 -1
- package/features/octets.download.feature +1 -0
- package/features/octets.feature +59 -0
- package/features/octets.meta.feature +1 -0
- package/features/site/_app/immutable/asset.js +1 -0
- package/features/site/favicon.ico +0 -0
- package/features/site/index.html +10 -0
- package/features/steps/Components.ts +4 -2
- package/features/steps/Database.ts +7 -1
- package/features/steps/Gateway.ts +23 -21
- package/features/steps/Parameters.ts +37 -16
- package/features/steps/Realtime.ts +3 -3
- package/package.json +7 -5
- package/readme.md +3 -0
- package/source/Directive.ts +4 -11
- package/source/Discovery/Explorer.ts +34 -0
- package/source/Discovery/Site.test.ts +147 -0
- package/source/Discovery/Site.ts +202 -0
- package/source/Discovery/index.ts +3 -0
- package/source/Discovery/tree.test.ts +175 -0
- package/source/Discovery/tree.ts +77 -0
- package/source/Discovery/trunk.ts +33 -0
- package/source/Endpoint.ts +4 -0
- package/source/Factory.ts +3 -86
- package/source/Gateway.ts +34 -4
- package/source/HTTP/Context.ts +7 -0
- package/source/HTTP/Probe.ts +0 -11
- package/source/HTTP/Server.ts +2 -3
- package/source/HTTP/messages.ts +4 -2
- package/source/Introspection.ts +58 -7
- package/source/MCP/schema.ts +16 -5
- package/source/MCP/tools.ts +14 -32
- package/source/Mapping.ts +5 -0
- package/source/Query.ts +17 -2
- package/source/RTD/Directives.ts +10 -0
- package/source/RTD/Endpoint.ts +8 -1
- package/source/RTD/Node.ts +20 -6
- package/source/RTD/Tree.ts +19 -9
- package/source/RTD/factory.ts +37 -1
- package/source/RTD/segment.ts +21 -0
- package/source/const.ts +26 -0
- package/source/deployment.ts +2 -2
- package/source/directives/auth/Anonymous.test.ts +31 -5
- package/source/directives/auth/Anonymous.ts +15 -3
- package/source/directives/auth/Anyone.ts +6 -0
- package/source/directives/auth/Assert.ts +5 -0
- package/source/directives/auth/Delegate.ts +5 -2
- package/source/directives/auth/Federation.ts +11 -0
- package/source/directives/auth/Id.ts +15 -0
- package/source/directives/auth/Input.ts +5 -0
- package/source/directives/auth/Role.ts +17 -0
- package/source/directives/cors/CORS.test.ts +75 -0
- package/source/directives/cors/CORS.ts +10 -3
- package/source/directives/help/Family.ts +122 -0
- package/source/directives/help/Help.test.ts +227 -0
- package/source/directives/help/Help.ts +47 -0
- package/source/directives/help/Parameters.ts +47 -0
- package/source/directives/help/described.ts +57 -0
- package/source/directives/help/index.ts +45 -0
- package/source/directives/index.ts +8 -1
- package/source/directives/map/Headers.ts +6 -6
- package/source/directives/mcp/MCP.ts +12 -21
- package/source/directives/mcp/Tool.test.ts +22 -68
- package/source/directives/mcp/Tool.ts +14 -41
- package/source/directives/octets/Directive.ts +7 -0
- package/source/directives/octets/Octets.ts +8 -0
- package/source/directives/octets/Put.ts +10 -0
- package/source/manifest.ts +1 -1
- package/source/root.ts +2 -2
- package/source/service.ts +94 -0
- package/source/shortcuts.ts +14 -0
- package/transpiled/Directive.d.ts +1 -1
- package/transpiled/Directive.js +3 -11
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Discovery/Explorer.d.ts +11 -0
- package/transpiled/Discovery/Explorer.js +29 -0
- package/transpiled/Discovery/Explorer.js.map +1 -0
- package/transpiled/Discovery/Site.d.ts +43 -0
- package/transpiled/Discovery/Site.js +171 -0
- package/transpiled/Discovery/Site.js.map +1 -0
- package/transpiled/Discovery/index.d.ts +3 -0
- package/transpiled/Discovery/index.js +4 -0
- package/transpiled/Discovery/index.js.map +1 -0
- package/transpiled/Discovery/tree.d.ts +26 -0
- package/transpiled/Discovery/tree.js +50 -0
- package/transpiled/Discovery/tree.js.map +1 -0
- package/transpiled/Discovery/trunk.d.ts +13 -0
- package/transpiled/Discovery/trunk.js +29 -0
- package/transpiled/Discovery/trunk.js.map +1 -0
- package/transpiled/Endpoint.d.ts +2 -1
- package/transpiled/Endpoint.js +3 -0
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.js +3 -61
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +4 -0
- package/transpiled/Gateway.js +29 -4
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +6 -0
- package/transpiled/HTTP/Context.js +6 -0
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Probe.d.ts +0 -10
- package/transpiled/HTTP/Probe.js +0 -10
- package/transpiled/HTTP/Probe.js.map +1 -1
- package/transpiled/HTTP/Server.d.ts +0 -1
- package/transpiled/HTTP/Server.js +2 -2
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/messages.js +4 -2
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/Introspection.d.ts +38 -6
- package/transpiled/Introspection.js +18 -1
- package/transpiled/Introspection.js.map +1 -1
- package/transpiled/MCP/schema.d.ts +2 -2
- package/transpiled/MCP/schema.js +11 -5
- package/transpiled/MCP/schema.js.map +1 -1
- package/transpiled/MCP/tools.d.ts +1 -1
- package/transpiled/MCP/tools.js +12 -28
- package/transpiled/MCP/tools.js.map +1 -1
- package/transpiled/Mapping.d.ts +2 -0
- package/transpiled/Mapping.js +4 -0
- package/transpiled/Mapping.js.map +1 -1
- package/transpiled/Query.d.ts +12 -0
- package/transpiled/Query.js +16 -2
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +8 -0
- package/transpiled/RTD/Endpoint.d.ts +7 -1
- package/transpiled/RTD/Node.d.ts +11 -2
- package/transpiled/RTD/Node.js +12 -2
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/Tree.js +15 -8
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/RTD/factory.js +27 -1
- package/transpiled/RTD/factory.js.map +1 -1
- package/transpiled/RTD/segment.d.ts +7 -0
- package/transpiled/RTD/segment.js +17 -0
- package/transpiled/RTD/segment.js.map +1 -1
- package/transpiled/const.d.ts +21 -0
- package/transpiled/const.js +21 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/deployment.js +2 -2
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/Anonymous.d.ts +11 -2
- package/transpiled/directives/auth/Anonymous.js +13 -3
- package/transpiled/directives/auth/Anonymous.js.map +1 -1
- package/transpiled/directives/auth/Anyone.d.ts +3 -0
- package/transpiled/directives/auth/Anyone.js +4 -0
- package/transpiled/directives/auth/Anyone.js.map +1 -1
- package/transpiled/directives/auth/Assert.d.ts +2 -0
- package/transpiled/directives/auth/Assert.js +4 -0
- package/transpiled/directives/auth/Assert.js.map +1 -1
- package/transpiled/directives/auth/Delegate.d.ts +4 -1
- package/transpiled/directives/auth/Delegate.js +5 -2
- package/transpiled/directives/auth/Delegate.js.map +1 -1
- package/transpiled/directives/auth/Federation.d.ts +5 -0
- package/transpiled/directives/auth/Federation.js +8 -0
- package/transpiled/directives/auth/Federation.js.map +1 -1
- package/transpiled/directives/auth/Id.d.ts +9 -0
- package/transpiled/directives/auth/Id.js +12 -0
- package/transpiled/directives/auth/Id.js.map +1 -1
- package/transpiled/directives/auth/Input.d.ts +2 -0
- package/transpiled/directives/auth/Input.js +4 -0
- package/transpiled/directives/auth/Input.js.map +1 -1
- package/transpiled/directives/auth/Role.d.ts +7 -0
- package/transpiled/directives/auth/Role.js +11 -0
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/cors/CORS.js +9 -2
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/help/Family.d.ts +28 -0
- package/transpiled/directives/help/Family.js +83 -0
- package/transpiled/directives/help/Family.js.map +1 -0
- package/transpiled/directives/help/Help.d.ts +22 -0
- package/transpiled/directives/help/Help.js +36 -0
- package/transpiled/directives/help/Help.js.map +1 -0
- package/transpiled/directives/help/Parameters.d.ts +18 -0
- package/transpiled/directives/help/Parameters.js +34 -0
- package/transpiled/directives/help/Parameters.js.map +1 -0
- package/transpiled/directives/help/described.d.ts +16 -0
- package/transpiled/directives/help/described.js +23 -0
- package/transpiled/directives/help/described.js.map +1 -0
- package/transpiled/directives/help/index.d.ts +22 -0
- package/transpiled/directives/help/index.js +34 -0
- package/transpiled/directives/help/index.js.map +1 -0
- package/transpiled/directives/index.d.ts +4 -0
- package/transpiled/directives/index.js +8 -1
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/map/Headers.d.ts +5 -0
- package/transpiled/directives/map/Headers.js +7 -5
- package/transpiled/directives/map/Headers.js.map +1 -1
- package/transpiled/directives/mcp/MCP.d.ts +8 -5
- package/transpiled/directives/mcp/MCP.js +9 -13
- package/transpiled/directives/mcp/MCP.js.map +1 -1
- package/transpiled/directives/mcp/Tool.d.ts +6 -12
- package/transpiled/directives/mcp/Tool.js +10 -23
- package/transpiled/directives/mcp/Tool.js.map +1 -1
- package/transpiled/directives/octets/Directive.d.ts +6 -0
- package/transpiled/directives/octets/Directive.js.map +1 -1
- package/transpiled/directives/octets/Octets.d.ts +3 -0
- package/transpiled/directives/octets/Octets.js +6 -0
- package/transpiled/directives/octets/Octets.js.map +1 -1
- package/transpiled/directives/octets/Put.d.ts +3 -0
- package/transpiled/directives/octets/Put.js +8 -0
- package/transpiled/directives/octets/Put.js.map +1 -1
- package/transpiled/manifest.js +1 -1
- package/transpiled/root.js +2 -2
- package/transpiled/root.js.map +1 -1
- package/transpiled/service.d.ts +7 -0
- package/transpiled/service.js +74 -0
- package/transpiled/service.js.map +1 -0
- package/transpiled/shortcuts.d.ts +3 -0
- package/transpiled/shortcuts.js +13 -0
- package/transpiled/shortcuts.js.map +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/env.js +1 -0
- package/ui/dist/_app/immutable/assets/0.Bl7vLmIK.css +2 -0
- package/ui/dist/_app/immutable/assets/2.BC0-Jhqu.css +1 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/ui/dist/_app/immutable/chunks/CDMRPWCx.js +6 -0
- package/ui/dist/_app/immutable/chunks/DJTKIUs8.js +1 -0
- package/ui/dist/_app/immutable/chunks/DjIYL2zC.js +3 -0
- package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/ui/dist/_app/immutable/chunks/z-DVUbQC.js +1 -0
- package/ui/dist/_app/immutable/entry/app.5S2Jtv46.js +2 -0
- package/ui/dist/_app/immutable/entry/start.Bqi6IBmH.js +1 -0
- package/ui/dist/_app/immutable/nodes/0.D4LwNilH.js +1 -0
- package/ui/dist/_app/immutable/nodes/1.CSRya9So.js +1 -0
- package/ui/dist/_app/immutable/nodes/2.BtiGmR6I.js +14 -0
- package/ui/dist/_app/version.json +1 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/favicon-96x96.png +0 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/index.html +66 -0
- package/components/identity.bans/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.basic/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.clients/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.federation/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.grants/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.keys/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.otp/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.passkeys/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.roles/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.tokens/migrations/0001-system-properties.yaml +0 -14
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describing } from '../Introspection.js'
|
|
2
|
+
import { template } from '../RPC/names.js'
|
|
3
|
+
import { variables } from '../RTD/segment.js'
|
|
4
|
+
import { guarded, resource, type Described } from '../directives/help/index.js'
|
|
5
|
+
import type * as http from '../HTTP/index.js'
|
|
6
|
+
import type { Tree } from '../RTD/index.js'
|
|
7
|
+
import type { Introspection } from '../Introspection.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every route in the tree this caller may reach, keyed by the template it answers at, and
|
|
11
|
+
* each described exactly as `OPTIONS` on that path describes it — so a key can be taken
|
|
12
|
+
* from the answer and sent back as a request.
|
|
13
|
+
*
|
|
14
|
+
* A route a name cannot spell is here, unlike at `/.rpc` and `/.mcp`: HTTP addresses a
|
|
15
|
+
* route by its path, so leaving one out would make this disagree with the router. A
|
|
16
|
+
* resource no method of which this caller may reach is not here at all — `OPTIONS` answers
|
|
17
|
+
* `403` to one, and an empty entry would still say it exists.
|
|
18
|
+
*
|
|
19
|
+
* Sorted, because a branch is merged whenever its tenant answers and two replicas would
|
|
20
|
+
* otherwise order the same tree differently. Verbs keep the order they were declared in,
|
|
21
|
+
* which is the order `OPTIONS` answers them in.
|
|
22
|
+
*/
|
|
23
|
+
export async function describe(tree: Tree, request: http.Context): Promise<Discovered> {
|
|
24
|
+
const context = describing(request)
|
|
25
|
+
const routes = new Map<string, Mounted>()
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Sequentially: there is no I/O to overlap — an endpoint's own description is read once
|
|
29
|
+
* and memoized — so awaiting the whole tree at once would only allocate every clone of
|
|
30
|
+
* it at the same moment.
|
|
31
|
+
*/
|
|
32
|
+
for (const { segments, verb, method } of tree.walk()) {
|
|
33
|
+
const introspection = await method.explain(context, variables(segments))
|
|
34
|
+
|
|
35
|
+
if (introspection === null) continue
|
|
36
|
+
|
|
37
|
+
const route = template(segments)
|
|
38
|
+
let mounted = routes.get(route)
|
|
39
|
+
|
|
40
|
+
if (mounted === undefined) {
|
|
41
|
+
mounted = { described: resource(method.directives), methods: {} }
|
|
42
|
+
routes.set(route, mounted)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// two declarations can walk to one template — `/a/b` and `/a: { /b: }` — and the walk
|
|
46
|
+
// is in the order `match` tries them, so the first is the one a request would reach
|
|
47
|
+
mounted.methods[verb] ??= introspection
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const described: Record<string, Resource> = {}
|
|
51
|
+
|
|
52
|
+
for (const route of Array.from(routes.keys()).sort()) {
|
|
53
|
+
const { described: stated, methods } = routes.get(route)!
|
|
54
|
+
|
|
55
|
+
// what the resource is, beside the methods it serves; a verb is upper case and cannot
|
|
56
|
+
// collide with either key
|
|
57
|
+
described[route] = { ...stated, ...guarded(methods), ...methods }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { routes: described }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What the tree answers. An object, so that what is said of the whole of it has somewhere to go. */
|
|
64
|
+
export interface Discovered {
|
|
65
|
+
routes: Record<string, Resource>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What one resource is, and what it serves; a verb is upper case, and nothing else here is. */
|
|
69
|
+
export interface Resource extends Described {
|
|
70
|
+
[verb: string]: unknown
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One route template as the walk found it, before the two are answered as one. */
|
|
74
|
+
interface Mounted {
|
|
75
|
+
described: Described | null
|
|
76
|
+
methods: Record<string, Introspection>
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Negotiator from 'negotiator'
|
|
2
|
+
import { types } from '../HTTP/formats/index.js'
|
|
3
|
+
import { DISCOVERY } from '../const.js'
|
|
4
|
+
import type { Context, OutgoingMessage } from '../HTTP/index.js'
|
|
5
|
+
|
|
6
|
+
/** What a browser asks for, and what nothing else here answers with. */
|
|
7
|
+
const HTML = 'text/html'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where a browser that asked for the trunk is sent. An application serves what it declares,
|
|
11
|
+
* and `/` is usually not one of those — a person who typed the address into a browser is
|
|
12
|
+
* looking for something to look at, and the page is the only thing here that is one.
|
|
13
|
+
*
|
|
14
|
+
* A client that asked for something in particular gets what it asked for: only an `accept`
|
|
15
|
+
* that prefers a page, or one that names no preference at all, is sent here. The second is
|
|
16
|
+
* what an unfurler sends — several ask for anything, and a link to an application would
|
|
17
|
+
* otherwise show nothing — and what `curl` sends, which the page serves no worse than the
|
|
18
|
+
* `405` it used to get.
|
|
19
|
+
*/
|
|
20
|
+
export function looking(context: Context): OutgoingMessage | null {
|
|
21
|
+
if (context.request.method !== 'GET' || context.procedural) return null
|
|
22
|
+
|
|
23
|
+
const accept = context.request.headers.accept
|
|
24
|
+
|
|
25
|
+
if (accept !== undefined && accept.trim() !== ANYTHING) {
|
|
26
|
+
if (new Negotiator(context.request).mediaType([...types, HTML]) !== HTML) return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { status: 302, headers: new Headers({ location: DISCOVERY + '/' }) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An `accept` that names nothing, which is the same as naming none. */
|
|
33
|
+
const ANYTHING = '*/*'
|
package/source/Endpoint.ts
CHANGED
|
@@ -52,6 +52,10 @@ export class Endpoint implements RTD.Endpoint {
|
|
|
52
52
|
return message
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
public selection(): Record<string, Schema> | null {
|
|
56
|
+
return this.mapping.selection()
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
public async explain(parameters: RTD.Parameter[]): Promise<Introspection> {
|
|
56
60
|
this.introspection ??= await this.introspect(parameters)
|
|
57
61
|
|
package/source/Factory.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
1
|
import { createHash } from 'node:crypto'
|
|
3
|
-
import { console, traces, type LevelName, type TracesOptions } from 'openspan'
|
|
4
2
|
import { Tenant } from './Tenant.js'
|
|
5
|
-
import {
|
|
6
|
-
import { Remotes } from './Remotes.js'
|
|
7
|
-
import { Tree } from './RTD/index.js'
|
|
8
|
-
import { EndpointsFactory } from './Endpoint.js'
|
|
9
|
-
import { families, interceptors } from './directives/index.js'
|
|
10
|
-
import { DirectivesFactory } from './Directive.js'
|
|
11
|
-
import { Composition } from './Composition.js'
|
|
12
|
-
import * as root from './root.js'
|
|
13
|
-
import { ATOM_GROUP } from './const.js'
|
|
14
|
-
import { Interception } from './Interception.js'
|
|
15
|
-
import { Dispatcher } from './RPC/index.js'
|
|
16
|
-
import { Server as Model } from './MCP/index.js'
|
|
17
|
-
import * as http from './HTTP/index.js'
|
|
3
|
+
import { CHANNEL } from './const.js'
|
|
18
4
|
import type { Branch } from './Branch.js'
|
|
19
5
|
import type { syntax } from './RTD/index.js'
|
|
20
6
|
import type { Broadcast } from './Gateway.js'
|
|
@@ -46,80 +32,11 @@ export class Factory implements extensions.Factory {
|
|
|
46
32
|
}
|
|
47
33
|
|
|
48
34
|
public async service(): Promise<Connector | null> {
|
|
49
|
-
|
|
50
|
-
process.env.TOA_EXPOSITION_PROPERTIES,
|
|
51
|
-
'TOA_EXPOSITION_PROPERTIES is undefined'
|
|
52
|
-
)
|
|
35
|
+
const { service } = await import('./service.js')
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const options = JSON.parse(process.env.TOA_EXPOSITION_PROPERTIES) as http.Options
|
|
57
|
-
const broadcast: Broadcast = await this.host.broadcast(CHANNEL)
|
|
58
|
-
const server = http.Server.create({ ...options })
|
|
59
|
-
const remotes = new Remotes(this.host)
|
|
60
|
-
const node = root.resolve()
|
|
61
|
-
const methods = new EndpointsFactory(remotes)
|
|
62
|
-
const directives = new DirectivesFactory(families, remotes, this.host, options)
|
|
63
|
-
const interception = new Interception(interceptors, options)
|
|
64
|
-
const tree = new Tree(node, methods, directives)
|
|
65
|
-
|
|
66
|
-
const composition = new Composition(this.host)
|
|
67
|
-
const dispatcher = options.rpc === undefined ? null : new Dispatcher(options.rpc)
|
|
68
|
-
const mcp = options.mcp === undefined ? null : new Model(options.mcp, tree)
|
|
69
|
-
const gateway = new Gateway(
|
|
70
|
-
broadcast,
|
|
71
|
-
tree,
|
|
72
|
-
interception,
|
|
73
|
-
directives,
|
|
74
|
-
dispatcher,
|
|
75
|
-
mcp
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
gateway.depends(remotes)
|
|
79
|
-
gateway.depends(composition)
|
|
80
|
-
// what the directives meter through; one atom per process, connected once
|
|
81
|
-
gateway.depends(this.host.atom(ATOM_GROUP))
|
|
82
|
-
|
|
83
|
-
server.attach(gateway.process.bind(gateway))
|
|
84
|
-
server.depends(gateway)
|
|
85
|
-
|
|
86
|
-
return server
|
|
37
|
+
return await service(this.host)
|
|
87
38
|
}
|
|
88
39
|
}
|
|
89
40
|
|
|
90
|
-
const CHANNEL = 'exposition'
|
|
91
|
-
const LOGS_PREFIX = 'TOA_TELEMETRY_LOGS'
|
|
92
|
-
const TRACES_ENV = 'TOA_TELEMETRY_TRACES'
|
|
93
|
-
|
|
94
|
-
function configureLogs(): void {
|
|
95
|
-
const globEnv = process.env[LOGS_PREFIX]
|
|
96
|
-
const level: LevelName = process.env.TOA_DEV === '1' ? 'trace' : 'info'
|
|
97
|
-
const options =
|
|
98
|
-
globEnv === undefined ? { level } : (JSON.parse(globEnv) as { level?: LevelName })
|
|
99
|
-
|
|
100
|
-
console.configure({ level: options.level ?? level })
|
|
101
|
-
|
|
102
|
-
const tracesEnv = process.env[TRACES_ENV]
|
|
103
|
-
|
|
104
|
-
traces(
|
|
105
|
-
tracesEnv === undefined ? development() : (JSON.parse(tracesEnv) as TracesOptions)
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Tracing is off unless it is configured. The console exporter is a local development
|
|
111
|
-
* mechanism, so it is turned on for `toa dev` and for a boot trace the CLI has already
|
|
112
|
-
* asked for (`runtime/boot/src/span.js`), and nowhere else — a deployment that wants
|
|
113
|
-
* traces annotates `telemetry.traces.exporters`.
|
|
114
|
-
*
|
|
115
|
-
* The gateway boots without the telemetry extension, hence the copy of
|
|
116
|
-
* `extensions/telemetry/source/extension.ts`.
|
|
117
|
-
*/
|
|
118
|
-
function development(): TracesOptions {
|
|
119
|
-
const local = process.env.TOA_DEV === '1' || process.env.TOA_BOOT_TRACE === '1'
|
|
120
|
-
|
|
121
|
-
return local ? { exporters: { console: {} } } : {}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
41
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
125
42
|
export type Host = extensions.Host
|
package/source/Gateway.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { console } from 'openspan'
|
|
|
4
4
|
import { Connector } from '@toa.io/core'
|
|
5
5
|
import type { bindings } from '@toa.io/core/types'
|
|
6
6
|
import * as http from './HTTP/index.js'
|
|
7
|
-
import { MCP, RPC } from './const.js'
|
|
7
|
+
import { DISCOVERY, MCP, RPC } from './const.js'
|
|
8
8
|
import { rethrow } from './exceptions.js'
|
|
9
9
|
import { decide } from './Branch.js'
|
|
10
|
+
import { describing } from './Introspection.js'
|
|
11
|
+
import { Explorer, looking } from './Discovery/index.js'
|
|
10
12
|
import type { Interception } from './Interception.js'
|
|
11
13
|
import type { Dispatcher } from './RPC/index.js'
|
|
12
14
|
import type { Server } from './MCP/index.js'
|
|
@@ -32,6 +34,9 @@ export class Gateway extends Connector {
|
|
|
32
34
|
|
|
33
35
|
/** And the same of MCP. */
|
|
34
36
|
private readonly mcp: Server | null
|
|
37
|
+
|
|
38
|
+
/** The tree, for every route at once. Served always, and so not nullable. */
|
|
39
|
+
private readonly explorer: Explorer
|
|
35
40
|
private readonly branches = new Map<string, Exposed>()
|
|
36
41
|
private lastMerge = 0
|
|
37
42
|
private widestGap = 0
|
|
@@ -56,6 +61,7 @@ export class Gateway extends Connector {
|
|
|
56
61
|
this.directives = directives
|
|
57
62
|
this.dispatcher = dispatcher
|
|
58
63
|
this.mcp = mcp
|
|
64
|
+
this.explorer = new Explorer(tree)
|
|
59
65
|
|
|
60
66
|
this.depends(broadcast)
|
|
61
67
|
}
|
|
@@ -89,9 +95,28 @@ export class Gateway extends Connector {
|
|
|
89
95
|
if (this.mcp !== null && context.url.pathname === MCP)
|
|
90
96
|
return await this.mcp.process(context, route)
|
|
91
97
|
|
|
98
|
+
// the page under this prefix is an interceptor's, and has already answered
|
|
99
|
+
if (context.url.pathname === DISCOVERY || context.url.pathname === DISCOVERY + '/')
|
|
100
|
+
return await this.explorer.process(context)
|
|
101
|
+
|
|
102
|
+
// a browser that asked for the trunk of an API is looking for the page, and where the
|
|
103
|
+
// application answers there itself, that is what answers
|
|
104
|
+
if (context.url.pathname === '/' && !this.serves(context)) {
|
|
105
|
+
const page = looking(context)
|
|
106
|
+
|
|
107
|
+
if (page !== null) return page
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
return await this.route(context)
|
|
93
111
|
}
|
|
94
112
|
|
|
113
|
+
/** Whether the application serves the trunk with the verb the request was made with. */
|
|
114
|
+
private serves(context: http.Context): boolean {
|
|
115
|
+
const match = this.tree.match('/')
|
|
116
|
+
|
|
117
|
+
return match !== null && context.request.method in match.node.methods
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
/**
|
|
96
121
|
* One call: everything that needs a node. A request makes one of these, and a request
|
|
97
122
|
* that carries several calls makes one per call.
|
|
@@ -200,14 +225,19 @@ export class Gateway extends Connector {
|
|
|
200
225
|
node: Node,
|
|
201
226
|
parameters: Parameter[]
|
|
202
227
|
): Promise<http.OutgoingMessage> {
|
|
203
|
-
const
|
|
204
|
-
const verbs = Object.keys(
|
|
228
|
+
const { described, methods } = await node.explain(describing(context), parameters)
|
|
229
|
+
const verbs = Object.keys(methods)
|
|
205
230
|
|
|
206
231
|
// what a caller may not use is not a resource to them, and describing it would be the leak
|
|
207
232
|
if (verbs.length === 0 && Object.keys(node.methods).length > 0)
|
|
208
233
|
throw new http.Forbidden()
|
|
209
234
|
|
|
210
|
-
|
|
235
|
+
// what the resource is, beside the methods it serves; a verb is upper case and cannot
|
|
236
|
+
// collide with either key
|
|
237
|
+
return {
|
|
238
|
+
body: { ...described, ...methods },
|
|
239
|
+
headers: new Headers({ allow: verbs.join(', ') })
|
|
240
|
+
}
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
private async discover(): Promise<void> {
|
package/source/HTTP/Context.ts
CHANGED
|
@@ -26,6 +26,13 @@ export class Context {
|
|
|
26
26
|
*/
|
|
27
27
|
public readonly procedural: boolean = false
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Whether this describes a resource rather than calling one. What builds a describing
|
|
31
|
+
* context says so, and a directive that answers differently to a description than to a
|
|
32
|
+
* call reads it here.
|
|
33
|
+
*/
|
|
34
|
+
public readonly exploratory: boolean = false
|
|
35
|
+
|
|
29
36
|
public readonly pipelines: Pipelines = {
|
|
30
37
|
body: [],
|
|
31
38
|
response: []
|
package/source/HTTP/Probe.ts
CHANGED
|
@@ -96,15 +96,4 @@ export class Probe {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
* Reserved for this probe. `8001` is the Telemetry readiness probe's, and `toa export` refuses a
|
|
101
|
-
* port claimed twice — `toa mono` and a local run put every service in one process.
|
|
102
|
-
*/
|
|
103
|
-
export const PROBE = 8004
|
|
104
99
|
export const PATH = '/.ready'
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The initial delay of the readiness probe. The server does not sleep for it: whoever
|
|
108
|
-
* probes is the one that waits, and doing it here as well only delayed the process twice.
|
|
109
|
-
*/
|
|
110
|
-
export const DELAY = 3 // seconds
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { Connector } from '@toa.io/core'
|
|
|
10
10
|
import { type OutgoingMessage, write } from './messages.js'
|
|
11
11
|
import { ClientError, Exception } from './exceptions.js'
|
|
12
12
|
import { Context } from './Context.js'
|
|
13
|
-
import {
|
|
13
|
+
import { Probe } from './Probe.js'
|
|
14
|
+
import { PORT, PROBE } from '../const.js'
|
|
14
15
|
import type { IncomingMessage, Protocol, ServerResponse } from './types.js'
|
|
15
16
|
import type { Bouncer, MCP, OAuth, RPC } from '../Annotation.js'
|
|
16
17
|
|
|
@@ -331,8 +332,6 @@ function errorAttributes(
|
|
|
331
332
|
return attributes
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
export const PORT = 8000
|
|
335
|
-
|
|
336
335
|
export const DRAIN = 10 // seconds
|
|
337
336
|
|
|
338
337
|
/** Megabytes a single HTTP/2 session may hold, over Node's default of 10. */
|
package/source/HTTP/messages.ts
CHANGED
|
@@ -5,13 +5,15 @@ import * as contentType from 'content-type'
|
|
|
5
5
|
import { console } from 'openspan'
|
|
6
6
|
import { type Format, decoders } from './formats/index.js'
|
|
7
7
|
import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions.js'
|
|
8
|
+
import { environment } from '@toa.io/generic'
|
|
8
9
|
import type { Context } from './Context.js'
|
|
9
10
|
import type { ServerResponse } from './types.js'
|
|
10
11
|
|
|
12
|
+
const context = environment.get('TOA_CONTEXT')
|
|
13
|
+
const env = environment.get('TOA_ENV')
|
|
11
14
|
const server =
|
|
12
15
|
`Exposition/${JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version}` +
|
|
13
|
-
((
|
|
14
|
-
(process.env.TOA_ENV === undefined ? '' : `/${process.env.TOA_ENV}`))
|
|
16
|
+
((context === undefined ? '' : ` ${context}`) + (env === undefined ? '' : `/${env}`))
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Applies what the request accumulated in `pipelines.response` — an `io:output` restriction,
|
package/source/Introspection.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Remote } from '@toa.io/core'
|
|
2
|
+
import type { Context } from './HTTP/index.js'
|
|
2
3
|
|
|
3
4
|
export interface Introspection {
|
|
4
5
|
/**
|
|
@@ -11,24 +12,68 @@ export interface Introspection {
|
|
|
11
12
|
|
|
12
13
|
/** what a person is shown where a client lists this method, which a name is not */
|
|
13
14
|
title?: string
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* whether reaching it takes nothing at all — `auth:anonymous`. Said rather than left
|
|
18
|
+
* unsaid, because it is also what refuses a caller presenting a credential: a client
|
|
19
|
+
* cannot know that from an answer that says nothing.
|
|
20
|
+
*/
|
|
21
|
+
anonymous?: boolean
|
|
22
|
+
|
|
23
|
+
/** whether reaching it takes being someone, whoever — `auth:anyone` and its like */
|
|
24
|
+
authenticated?: boolean
|
|
25
|
+
|
|
26
|
+
/** whether reaching it is being the identity it is about — `auth:id` */
|
|
27
|
+
private?: boolean
|
|
28
|
+
|
|
29
|
+
/** whether reaching it takes a role — `auth:role` */
|
|
30
|
+
protected?: boolean
|
|
31
|
+
|
|
32
|
+
/** and whether that role is one of the `system` scope */
|
|
33
|
+
system?: boolean
|
|
34
|
+
|
|
35
|
+
/** whether the route publishes this method to a model — [`mcp:tool`](mcp.md) */
|
|
36
|
+
mcp?: boolean
|
|
14
37
|
route?: Record<string, Schema>
|
|
15
38
|
query?: Record<string, Schema>
|
|
16
39
|
|
|
17
|
-
/** what
|
|
18
|
-
|
|
40
|
+
/** what the body is, where it is a file rather than a value — [`octets:put`](octets.md) */
|
|
41
|
+
octets?: Octets
|
|
19
42
|
input?: Schema
|
|
20
43
|
output?: Schema
|
|
21
44
|
errors?: string[]
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
/**
|
|
25
|
-
export interface
|
|
26
|
-
header
|
|
27
|
-
|
|
47
|
+
/** What sending a file to a resource takes, which is not something a schema states. */
|
|
48
|
+
export interface Octets {
|
|
49
|
+
/** what may be sent, in the syntax of an `accept` header; anything where unstated */
|
|
50
|
+
accept?: string
|
|
51
|
+
|
|
52
|
+
/** the largest body it takes, as it is written and as a refusal reports it */
|
|
53
|
+
limit: string
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the reply arrives as a stream of parts rather than as one object: a workflow
|
|
57
|
+
* runs on what was stored, and each step answers as it finishes. The first part is the
|
|
58
|
+
* entry itself, which is what a caller that wants nothing else reads and stops.
|
|
59
|
+
*/
|
|
60
|
+
stream?: boolean
|
|
28
61
|
}
|
|
29
62
|
|
|
30
63
|
export type Schema = Awaited<ReturnType<Remote['explain']>>['input']
|
|
31
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The same request, as a description of a resource rather than a call to one. A directive
|
|
67
|
+
* that answers differently to the two reads `exploratory` — `Anonymous` is the one that
|
|
68
|
+
* does, because the rule refusing a credential at an `anonymous` route is about a reply a
|
|
69
|
+
* cache would hold, and a description is not one.
|
|
70
|
+
*/
|
|
71
|
+
export function describing(context: Context): Context {
|
|
72
|
+
return Object.create(context, {
|
|
73
|
+
exploratory: { value: true, enumerable: true }
|
|
74
|
+
}) as Context
|
|
75
|
+
}
|
|
76
|
+
|
|
32
77
|
/**
|
|
33
78
|
* The same, in the order the shape states — whatever order the families that filled it ran
|
|
34
79
|
* in. What a resource says about itself is read, so it is written the way it is documented.
|
|
@@ -45,9 +90,15 @@ export function order(introspection: Introspection): Introspection {
|
|
|
45
90
|
const KEYS = [
|
|
46
91
|
'title',
|
|
47
92
|
'description',
|
|
93
|
+
'anonymous',
|
|
94
|
+
'authenticated',
|
|
95
|
+
'private',
|
|
96
|
+
'protected',
|
|
97
|
+
'system',
|
|
98
|
+
'mcp',
|
|
48
99
|
'route',
|
|
49
100
|
'query',
|
|
50
|
-
'
|
|
101
|
+
'octets',
|
|
51
102
|
'input',
|
|
52
103
|
'output',
|
|
53
104
|
'errors'
|
package/source/MCP/schema.ts
CHANGED
|
@@ -8,7 +8,12 @@ import type { Annotations } from './types.js'
|
|
|
8
8
|
* A `map:headers` property is not here: a call carries no headers of its own, so there is
|
|
9
9
|
* nowhere for a model to put one.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
// eslint-disable-next-line max-params
|
|
12
|
+
export function input(
|
|
13
|
+
introspection: Introspection,
|
|
14
|
+
variables: string[],
|
|
15
|
+
selection: Record<string, Schema> | null
|
|
16
|
+
): object {
|
|
12
17
|
const properties: Record<string, unknown> = {}
|
|
13
18
|
const required: string[] = []
|
|
14
19
|
|
|
@@ -23,15 +28,21 @@ export function input(introspection: Introspection, variables: string[]): object
|
|
|
23
28
|
required.push(variable)
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
/*
|
|
32
|
+
* It is a querystring on the wire, which is nothing a model knows or needs to; what it is
|
|
33
|
+
* to a caller is the part of a call that picks what the call is about. What selects
|
|
34
|
+
* records is stated here and not in the resource's own description, where it would be the
|
|
35
|
+
* same sentence on every queryable resource there is.
|
|
36
|
+
*/
|
|
37
|
+
const query = { ...introspection.query, ...selection }
|
|
38
|
+
|
|
39
|
+
if (Object.keys(query).length > 0)
|
|
29
40
|
properties.query = {
|
|
30
41
|
type: 'object',
|
|
31
42
|
description:
|
|
32
43
|
'Which records the call works on: what to match, in what order, ' +
|
|
33
44
|
'and how many at once.',
|
|
34
|
-
properties:
|
|
45
|
+
properties: query
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
const body = introspection.input as Shape | null | undefined
|
package/source/MCP/tools.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { address, name, split } from '../RPC/names.js'
|
|
|
4
4
|
import { METHOD_NOT_FOUND, failure, refusal, response } from './errors.js'
|
|
5
5
|
import { annotations, input, output } from './schema.js'
|
|
6
6
|
import { FAMILY, MCP, type Tool as Declaration } from '../directives/mcp/index.js'
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import { describing } from '../Introspection.js'
|
|
8
|
+
import { variables } from '../RTD/segment.js'
|
|
9
|
+
import type { Tree } from '../RTD/index.js'
|
|
9
10
|
import type { Params, Result, Tool } from './types.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -20,28 +21,26 @@ import type { Params, Result, Tool } from './types.js'
|
|
|
20
21
|
*
|
|
21
22
|
* Sorted, because the revision asks for an order a client can cache on.
|
|
22
23
|
*/
|
|
23
|
-
export async function list(tree: Tree,
|
|
24
|
+
export async function list(tree: Tree, request: http.Context): Promise<Tool[]> {
|
|
24
25
|
const tools: Tool[] = []
|
|
25
26
|
|
|
26
27
|
/*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* A description, not the request asking. What refuses a credentialed request at an
|
|
29
|
+
* `anonymous` route does not refuse the call a tool makes, and a list that says otherwise
|
|
30
|
+
* disagrees with what `tools/call` then does.
|
|
30
31
|
*/
|
|
31
|
-
const
|
|
32
|
-
procedural: { value: true, enumerable: true }
|
|
33
|
-
})
|
|
32
|
+
const context = describing(request)
|
|
34
33
|
|
|
35
34
|
for (const { segments, verb, method } of tree.walk()) {
|
|
36
|
-
if (MCP.published(method.directives.declared<Declaration>(FAMILY))
|
|
35
|
+
if (!MCP.published(method.directives.declared<Declaration>(FAMILY))) continue
|
|
37
36
|
|
|
38
37
|
const named = name(segments, verb)
|
|
39
38
|
|
|
40
39
|
// a route a name cannot spell is a route nothing addresses, here or at `/.rpc`
|
|
41
40
|
if (named === null) continue
|
|
42
41
|
|
|
43
|
-
const
|
|
44
|
-
const introspection = await method.explain(
|
|
42
|
+
const params = variables(segments)
|
|
43
|
+
const introspection = await method.explain(context, params)
|
|
45
44
|
|
|
46
45
|
if (introspection === null) continue
|
|
47
46
|
|
|
@@ -56,7 +55,8 @@ export async function list(tree: Tree, context: http.Context): Promise<Tool[]> {
|
|
|
56
55
|
...(described === undefined ? {} : { description: described }),
|
|
57
56
|
inputSchema: input(
|
|
58
57
|
introspection,
|
|
59
|
-
|
|
58
|
+
params.map((param) => param.name),
|
|
59
|
+
method.endpoint?.selection() ?? null
|
|
60
60
|
),
|
|
61
61
|
...(schema === undefined ? {} : { outputSchema: schema }),
|
|
62
62
|
...(hints === undefined ? {} : { annotations: hints })
|
|
@@ -129,7 +129,7 @@ export interface Scope {
|
|
|
129
129
|
function published(tree: Tree, named: string): boolean {
|
|
130
130
|
for (const { segments, verb, method } of tree.walk())
|
|
131
131
|
if (name(segments, verb) === named)
|
|
132
|
-
return MCP.published(method.directives.declared<Declaration>(FAMILY))
|
|
132
|
+
return MCP.published(method.directives.declared<Declaration>(FAMILY))
|
|
133
133
|
|
|
134
134
|
return false
|
|
135
135
|
}
|
|
@@ -142,21 +142,3 @@ function result(body: unknown): Result {
|
|
|
142
142
|
structuredContent: body
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* What a route template takes, by name. Describing has no values for them — a template is
|
|
148
|
-
* not a path — and nothing that describes a method reads one.
|
|
149
|
-
*/
|
|
150
|
-
function parameters(segments: Segment[]): Parameter[] {
|
|
151
|
-
const params: Parameter[] = []
|
|
152
|
-
|
|
153
|
-
for (const segment of segments) {
|
|
154
|
-
if (segment.fragment !== null) continue
|
|
155
|
-
|
|
156
|
-
if (segment.wildcard === true) params.push({ name: '**', value: '' })
|
|
157
|
-
else if (segment.placeholder !== null)
|
|
158
|
-
params.push({ name: segment.placeholder, value: '' })
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return params
|
|
162
|
-
}
|
package/source/Mapping.ts
CHANGED
|
@@ -24,6 +24,11 @@ export abstract class Mapping {
|
|
|
24
24
|
return this.query.explain(introspection)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** What picks the records a call is about; see `Query.selection`. */
|
|
28
|
+
public selection(): Record<string, Schema> | null {
|
|
29
|
+
return this.query.selection()
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
protected assign(input: any, qs: QueryString): void {
|
|
28
33
|
if (qs.parameters !== null) {
|
|
29
34
|
if (typeof input !== 'object' || input === null)
|
package/source/Query.ts
CHANGED
|
@@ -82,6 +82,11 @@ export class Query {
|
|
|
82
82
|
* Only what it actually accepts — a criteria the declaration closes is refused, and so is
|
|
83
83
|
* a search where none was asked for.
|
|
84
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* The querystring parameters this resource declares, which are properties of its own
|
|
87
|
+
* taken out of the input. What selects records is not among them: that is the same of
|
|
88
|
+
* every queryable resource, and `selection` is where it is stated.
|
|
89
|
+
*/
|
|
85
90
|
public explain(introspection: Introspection): Record<string, Schema> | null {
|
|
86
91
|
let query: Record<string, Schema> | null = null
|
|
87
92
|
|
|
@@ -96,9 +101,19 @@ export class Query {
|
|
|
96
101
|
query[parameter] = schema
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
return query
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* What picks the records a call is about, which is the querystring the gateway reads
|
|
109
|
+
* rather than anything the resource declares — the same for every queryable resource,
|
|
110
|
+
* and so not what one says about itself. A procedure states it, because there it is
|
|
111
|
+
* something the caller sends.
|
|
112
|
+
*/
|
|
113
|
+
public selection(): Record<string, Schema> | null {
|
|
114
|
+
if (!this.queryable) return null
|
|
100
115
|
|
|
101
|
-
query
|
|
116
|
+
const query: Record<string, Schema> = {}
|
|
102
117
|
|
|
103
118
|
if (!this.closed)
|
|
104
119
|
query.criteria = keyword(
|