@owlmeans/client-entrypoint 0.1.18-rc.4 → 0.1.18-rc.40
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/README.md +169 -49
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-entrypoint/SKILL.md +33 -30
- package/build/entrypoint.d.ts +2 -3
- package/build/entrypoint.d.ts.map +1 -1
- package/build/entrypoint.js +25 -38
- package/build/entrypoint.js.map +1 -1
- package/build/failure.d.ts +19 -0
- package/build/failure.d.ts.map +1 -0
- package/build/failure.js +25 -0
- package/build/failure.js.map +1 -0
- package/build/helper.d.ts +2 -3
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +0 -15
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -2
- package/build/index.js.map +1 -1
- package/build/protocol.d.ts +9 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +15 -0
- package/build/protocol.js.map +1 -0
- package/build/types.d.ts +39 -9
- package/build/types.d.ts.map +1 -1
- package/build/utils/entrypoint.d.ts +2 -4
- package/build/utils/entrypoint.d.ts.map +1 -1
- package/build/utils/entrypoint.js +0 -9
- package/build/utils/entrypoint.js.map +1 -1
- package/build/utils/handler.d.ts +3 -3
- package/build/utils/handler.d.ts.map +1 -1
- package/build/utils/handler.js +54 -31
- package/build/utils/handler.js.map +1 -1
- package/package.json +10 -10
- package/src/entrypoint.ts +35 -48
- package/src/failure.ts +41 -0
- package/src/helper.ts +2 -26
- package/src/index.ts +3 -2
- package/src/protocol.ts +39 -0
- package/src/types.ts +50 -11
- package/src/utils/entrypoint.ts +2 -17
- package/src/utils/handler.ts +64 -40
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { materializeEntrypoint, protocols } from '@owlmeans/entrypoint'
|
|
2
|
+
import type { EntrypointProtocolDeclaration, EntrypointTree } from '@owlmeans/entrypoint'
|
|
3
|
+
import { bindMaterializedEntrypoint } from './entrypoint.js'
|
|
4
|
+
import type {
|
|
5
|
+
ClientEntrypointOptions, ClientProtocolEntrypoint, RefedEntrypointHandler,
|
|
6
|
+
} from './types.js'
|
|
7
|
+
|
|
8
|
+
/** Bind a shared protocol to the client context without changing the declaration itself. */
|
|
9
|
+
export const bind = <Protocol extends EntrypointProtocolDeclaration>(
|
|
10
|
+
protocol: Protocol,
|
|
11
|
+
options?: ClientEntrypointOptions,
|
|
12
|
+
): ClientProtocolEntrypoint<Protocol> => {
|
|
13
|
+
const bound = Object.assign(bindMaterializedEntrypoint(
|
|
14
|
+
materializeEntrypoint(protocol),
|
|
15
|
+
undefined,
|
|
16
|
+
options,
|
|
17
|
+
), { protocol })
|
|
18
|
+
|
|
19
|
+
return bound as ClientProtocolEntrypoint<Protocol>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Materialize every protocol in an exported declaration tree for a browser context. */
|
|
23
|
+
export const bindAll = (tree: EntrypointTree): ClientProtocolEntrypoint<EntrypointProtocolDeclaration>[] =>
|
|
24
|
+
protocols(tree).map(protocol => bind(protocol))
|
|
25
|
+
|
|
26
|
+
/** Bind a frontend protocol to the component that renders it. */
|
|
27
|
+
export const bindScreen = <Protocol extends EntrypointProtocolDeclaration>(
|
|
28
|
+
protocol: Protocol,
|
|
29
|
+
handler: RefedEntrypointHandler,
|
|
30
|
+
options?: ClientEntrypointOptions,
|
|
31
|
+
): ClientProtocolEntrypoint<Protocol> => {
|
|
32
|
+
const bound = Object.assign(bindMaterializedEntrypoint(
|
|
33
|
+
materializeEntrypoint(protocol),
|
|
34
|
+
handler,
|
|
35
|
+
options,
|
|
36
|
+
), { protocol })
|
|
37
|
+
|
|
38
|
+
return bound as ClientProtocolEntrypoint<Protocol>
|
|
39
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,18 +1,62 @@
|
|
|
1
1
|
import type { ClientRouteModel, ClientRouteOptions } from '@owlmeans/client-route'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AbstractRequest, CommonEntrypoint, CommonEntrypointOptions, EntrypointHandler,
|
|
4
|
+
EntrypointOutcome, EntrypointProtocolDeclaration, RegisteredEntrypoint, RequestOf,
|
|
5
|
+
} from '@owlmeans/entrypoint'
|
|
3
6
|
|
|
4
7
|
export interface ClientEntrypoint<T = {}, R extends ClientRequest = ClientRequest> extends CommonEntrypoint {
|
|
5
8
|
route: ClientRouteModel
|
|
9
|
+
/**
|
|
10
|
+
* Address the entrypoint and resolve to what it answered. The reply's error is thrown, so a
|
|
11
|
+
* caller that only needs the value never inspects an outcome.
|
|
12
|
+
*/
|
|
6
13
|
call: EntrypointCall<T, R>
|
|
14
|
+
/**
|
|
15
|
+
* The same round trip as {@link ClientEntrypoint.call}, resolving to the value AND the outcome.
|
|
16
|
+
* Use it only where the outcome decides what happens next.
|
|
17
|
+
*/
|
|
18
|
+
invoke: EntrypointInvoke<T, R>
|
|
19
|
+
/**
|
|
20
|
+
* Build the URL this entrypoint addresses, with `:params` filled in and the query appended.
|
|
21
|
+
* Absolute when the route belongs to another service, or when `absolute` is asked for.
|
|
22
|
+
*/
|
|
23
|
+
url: EntrypointUrl<R>
|
|
7
24
|
validate: EntrypointFilter<R>
|
|
8
|
-
getPath: (partial?: boolean) => string
|
|
9
25
|
request: (request?: Partial<R>) => R
|
|
10
26
|
}
|
|
11
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The context-bound client view of a protocol. Unlike the legacy `ClientEntrypoint` generic,
|
|
30
|
+
* its request and reply types are read directly from the declaration and cannot be replaced at a
|
|
31
|
+
* call site.
|
|
32
|
+
*/
|
|
33
|
+
export type ClientProtocolEntrypoint<Protocol extends EntrypointProtocolDeclaration> =
|
|
34
|
+
Omit<ClientEntrypoint, 'call' | 'invoke' | 'url' | 'validate' | 'request'>
|
|
35
|
+
& RegisteredEntrypoint<RequestOf<Protocol>, import('@owlmeans/entrypoint').ResponseOf<Protocol>>
|
|
36
|
+
& {
|
|
37
|
+
readonly protocol: Protocol
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
export interface EntrypointCall<T, Req extends ClientRequest = ClientRequest> {
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
41
|
+
<Type extends T, R extends Req = Req>(req?: Partial<R>): Promise<Type>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EntrypointInvoke<T, Req extends ClientRequest = ClientRequest> {
|
|
45
|
+
<Type extends T, R extends Req = Req>(req?: Partial<R>): Promise<EntrypointReply<Type>>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EntrypointReply<T> {
|
|
49
|
+
value: T
|
|
50
|
+
outcome: EntrypointOutcome
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface EntrypointUrl<Req extends ClientRequest = ClientRequest> {
|
|
54
|
+
<R extends Req = Req>(req?: Partial<R>, opts?: EntrypointUrlOptions): Promise<string>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EntrypointUrlOptions {
|
|
58
|
+
/** Force a fully qualified URL even when the route belongs to the current service. */
|
|
59
|
+
absolute?: boolean
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
export interface EntrypointFilter<Req extends AbstractRequest = AbstractRequest> {
|
|
@@ -20,14 +64,9 @@ export interface EntrypointFilter<Req extends AbstractRequest = AbstractRequest>
|
|
|
20
64
|
}
|
|
21
65
|
|
|
22
66
|
export interface ClientRequest<T extends {} = {}> extends AbstractRequest<T> {
|
|
23
|
-
full?: boolean
|
|
24
67
|
}
|
|
25
68
|
|
|
26
69
|
export interface ClientEntrypointOptions extends CommonEntrypointOptions {
|
|
27
|
-
/**
|
|
28
|
-
* Force entrypoint to be elevated even if it is already elevated
|
|
29
|
-
*/
|
|
30
|
-
force?: boolean
|
|
31
70
|
routeOptions?: ClientRouteOptions
|
|
32
71
|
validateOnCall?: boolean
|
|
33
72
|
}
|
|
@@ -36,6 +75,6 @@ export interface EntrypointRef<T, R extends AbstractRequest = AbstractRequest> {
|
|
|
36
75
|
ref?: ClientEntrypoint<T, R>
|
|
37
76
|
}
|
|
38
77
|
|
|
39
|
-
export interface RefedEntrypointHandler<T, R extends AbstractRequest = AbstractRequest> {
|
|
78
|
+
export interface RefedEntrypointHandler<T = {}, R extends AbstractRequest = AbstractRequest> {
|
|
40
79
|
(ref: EntrypointRef<T, R>): EntrypointHandler
|
|
41
80
|
}
|
package/src/utils/entrypoint.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import Ajv from 'ajv'
|
|
2
2
|
import type { ErrorObject } from 'ajv'
|
|
3
|
-
import type { EntrypointFilter
|
|
3
|
+
import type { EntrypointFilter } from '../types.js'
|
|
4
4
|
import { ClientValidationError } from '../errors.js'
|
|
5
5
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
6
|
-
import type { EntrypointRef
|
|
6
|
+
import type { EntrypointRef } from '../types.js'
|
|
7
7
|
import formatsPlugin from 'ajv-formats'
|
|
8
8
|
|
|
9
|
-
export { entrypoint as makeBasicEntrypoint } from '@owlmeans/entrypoint'
|
|
10
9
|
export { isEntrypoint } from '@owlmeans/entrypoint/utils'
|
|
11
10
|
|
|
12
11
|
const ajv = new Ajv({ strict: false })
|
|
@@ -50,17 +49,3 @@ export const validate: <T, R extends AbstractRequest = AbstractRequest>(ref: Ent
|
|
|
50
49
|
|
|
51
50
|
return true
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
export const normalizeHelperParams = <T, R extends AbstractRequest = AbstractRequest>(
|
|
55
|
-
handler?: RefedEntrypointHandler<T, R> | ClientEntrypointOptions | boolean,
|
|
56
|
-
opts?: ClientEntrypointOptions | boolean
|
|
57
|
-
): [RefedEntrypointHandler<T, R> | undefined, ClientEntrypointOptions | undefined] => {
|
|
58
|
-
if (typeof handler !== 'function' && handler != null) {
|
|
59
|
-
opts = handler as ClientEntrypointOptions | boolean
|
|
60
|
-
handler = undefined
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
opts = typeof opts === 'boolean' ? { validateOnCall: opts } : opts
|
|
64
|
-
|
|
65
|
-
return [handler, opts]
|
|
66
|
-
}
|
package/src/utils/handler.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { ApiClient } from '@owlmeans/api'
|
|
2
2
|
import { DEFAULT_KEY } from '@owlmeans/client-config'
|
|
3
3
|
import type { ClientConfig, ClientContext } from '@owlmeans/client-context'
|
|
4
|
-
import type { AbstractRequest, EntrypointHandler } from '@owlmeans/entrypoint'
|
|
5
|
-
import { EntrypointOutcome, provideResponse } from '@owlmeans/entrypoint'
|
|
6
|
-
import type { ClientEntrypoint,
|
|
4
|
+
import type { AbstractRequest, EntrypointHandler, EntrypointTransport } from '@owlmeans/entrypoint'
|
|
5
|
+
import { EntrypointOutcome, provideResponse, transportAlias } from '@owlmeans/entrypoint'
|
|
6
|
+
import type { ClientEntrypoint, EntrypointInvoke, EntrypointUrlOptions, ClientEntrypointOptions, EntrypointRef, ClientRequest } from '../types.js'
|
|
7
7
|
import { validate } from './entrypoint.js'
|
|
8
8
|
import { extractParams } from '@owlmeans/client-route'
|
|
9
|
-
import { PARAM } from '@owlmeans/route'
|
|
9
|
+
import { PARAM, RouteMethod, RouteProtocols } from '@owlmeans/route'
|
|
10
10
|
import { stringify } from 'qs'
|
|
11
11
|
import { assertContext } from '@owlmeans/context'
|
|
12
12
|
import { makeSecurityHelper } from '@owlmeans/config'
|
|
13
|
+
import { ENTRYPOINT_FAILURE_SERVICE } from '../failure.js'
|
|
14
|
+
import type { EntrypointFailureService } from '../failure.js'
|
|
13
15
|
|
|
14
16
|
type Config = ClientConfig
|
|
15
17
|
interface Context<C extends Config = Config> extends ClientContext<C> { }
|
|
@@ -25,13 +27,23 @@ export const apiHandler: <
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const ep = context.entrypoint<ClientEntrypoint>(req.alias)
|
|
28
|
-
const route = await ep.route.resolve<Config, Context>(context)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// A route names the protocol it answers on. Whatever is bound under that protocol takes the
|
|
32
|
+
// call, so a consumer writes `ep.call(...)` without learning which transport ran.
|
|
33
|
+
const transport = transportAlias(ep.route.route.protocol)
|
|
34
|
+
if (context.hasService(transport)) {
|
|
35
|
+
req.path = ep.path()
|
|
36
|
+
|
|
37
|
+
return context.service<EntrypointTransport>(transport).handle(req, res)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Which web client carries this call is decided by the service the route answers on — the one it
|
|
41
|
+
// names, or the one the context picks for its app type when it names none.
|
|
42
|
+
const route = ep.service()
|
|
43
|
+
|
|
44
|
+
const alias: string | undefined = typeof context.cfg.webService === 'string'
|
|
31
45
|
? context.cfg.webService
|
|
32
|
-
:
|
|
33
|
-
? context.cfg.webService[route.service] ?? context.cfg.webService[DEFAULT_KEY]
|
|
34
|
-
: context.cfg.webService[DEFAULT_KEY])
|
|
46
|
+
: context.cfg.webService[route.service] ?? context.cfg.webService[DEFAULT_KEY]
|
|
35
47
|
|
|
36
48
|
if (alias == null) {
|
|
37
49
|
throw new SyntaxError(`Can't cast web service alias for ${ep.alias} entrypoint`)
|
|
@@ -39,15 +51,15 @@ export const apiHandler: <
|
|
|
39
51
|
|
|
40
52
|
const service: ApiClient = context.service(alias)
|
|
41
53
|
|
|
42
|
-
req.path = ep.
|
|
54
|
+
req.path = ep.path()
|
|
43
55
|
|
|
44
56
|
return service.handler(req, res as any)
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
export const
|
|
59
|
+
export const apiInvoke: <
|
|
48
60
|
T, R extends AbstractRequest = AbstractRequest
|
|
49
|
-
>(ref: EntrypointRef<T, R>, opts?: ClientEntrypointOptions) =>
|
|
50
|
-
(ref, opts) => (async (req
|
|
61
|
+
>(ref: EntrypointRef<T, R>, opts?: ClientEntrypointOptions) => EntrypointInvoke<T, R> =
|
|
62
|
+
(ref, opts) => (async (req) => {
|
|
51
63
|
const ep = ref.ref
|
|
52
64
|
if (ep == null) {
|
|
53
65
|
throw new SyntaxError('Try to make API call before the entrypoint is created')
|
|
@@ -56,21 +68,24 @@ export const apiCall: <
|
|
|
56
68
|
if (ctx == null) {
|
|
57
69
|
throw new SyntaxError(`No context provided in apiCall for ${ep.alias} entrypoint`)
|
|
58
70
|
}
|
|
59
|
-
await ep.route.resolve(ctx)
|
|
60
71
|
|
|
61
72
|
if (req?.canceled) {
|
|
62
|
-
return
|
|
73
|
+
return { value: null, outcome: EntrypointOutcome.Ok }
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
const request: AbstractRequest = {
|
|
66
77
|
alias: ep.alias,
|
|
67
78
|
params: req?.params ?? {},
|
|
68
|
-
body
|
|
79
|
+
// JSON transports must send a body for mutation methods even when the contract has no
|
|
80
|
+
// payload. Keeping that rule here makes every typed caller use `call({ params })`, never
|
|
81
|
+
// an SDK-specific `{ body: {} }` workaround.
|
|
82
|
+
body: req?.body ?? ([RouteMethod.POST, RouteMethod.PUT, RouteMethod.PATCH]
|
|
83
|
+
.includes(ep.route.route.method ?? RouteMethod.GET) ? {} : undefined),
|
|
69
84
|
headers: req?.headers ?? {},
|
|
70
85
|
query: req?.query ?? {},
|
|
71
86
|
host: req?.host,
|
|
72
87
|
base: req?.base,
|
|
73
|
-
path: ep.
|
|
88
|
+
path: ep.path(),
|
|
74
89
|
timeout: req?.timeout,
|
|
75
90
|
signal: req?.signal,
|
|
76
91
|
}
|
|
@@ -89,50 +104,59 @@ export const apiCall: <
|
|
|
89
104
|
throw e
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
|
-
if (res != null) {
|
|
93
|
-
await apiHandler(ref)(request, res)
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
107
|
const reply = provideResponse<unknown>()
|
|
97
108
|
if (ctx == null && ep.ctx == null) {
|
|
98
109
|
throw new SyntaxError(`Use entrypoint ${ep.alias} without context`)
|
|
99
110
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
throw reply.error
|
|
111
|
+
try {
|
|
112
|
+
await apiHandler(ref)(request, reply)
|
|
113
|
+
if (reply.error != null) throw reply.error
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (ctx.hasService(ENTRYPOINT_FAILURE_SERVICE)) {
|
|
116
|
+
await ctx.service<EntrypointFailureService>(ENTRYPOINT_FAILURE_SERVICE)
|
|
117
|
+
.notify({ alias: ep.alias, request, error })
|
|
118
|
+
}
|
|
119
|
+
throw error
|
|
103
120
|
}
|
|
104
121
|
|
|
105
|
-
return
|
|
106
|
-
}) as
|
|
122
|
+
return { value: reply.value ?? null, outcome: reply.outcome ?? EntrypointOutcome.Ok }
|
|
123
|
+
}) as EntrypointInvoke<any>
|
|
107
124
|
|
|
108
|
-
export const
|
|
125
|
+
export const entrypointUrl: <
|
|
109
126
|
T, R extends ClientRequest = ClientRequest
|
|
110
|
-
>(ref: EntrypointRef<T, R>, opts?:
|
|
127
|
+
>(ref: EntrypointRef<T, R>, req?: Partial<R>, opts?: EntrypointUrlOptions) => Promise<string> = async (ref, req, opts) => {
|
|
111
128
|
const ep = ref.ref
|
|
112
129
|
if (ep == null) {
|
|
113
130
|
throw new SyntaxError('Try to make URL before the entrypoint is created')
|
|
114
131
|
}
|
|
115
132
|
const ctx = ep.ctx
|
|
116
133
|
if (ctx == null) {
|
|
117
|
-
throw new SyntaxError(`No context provided in
|
|
134
|
+
throw new SyntaxError(`No context provided in entrypointUrl for ${ep.alias} entrypoint`)
|
|
118
135
|
}
|
|
119
|
-
await ep.route.resolve(ctx)
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
// The base is left off here: an in-service URL is addressed relative to it, and the absolute
|
|
138
|
+
// branch below has `makeUrl` prepend it.
|
|
139
|
+
const epPath = ep.path()
|
|
140
|
+
const pathParams = extractParams(epPath)
|
|
122
141
|
let path = pathParams.reduce((p, param) => {
|
|
123
142
|
return p.replace(`${PARAM}${param}`, `${req?.params?.[param as keyof typeof req.params]}`)
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
}, epPath) + (req?.query != null ? `?${stringify(req?.query)}` : '')
|
|
144
|
+
|
|
145
|
+
// A socket address is always absolute, whoever owns the route. `new WebSocket(path)` resolves a
|
|
146
|
+
// relative value against the PAGE's origin, which in a split deployment is the web host and not
|
|
147
|
+
// the service that answers the upgrade — so the handshake goes somewhere that never speaks it
|
|
148
|
+
// and dies on a gateway timeout. Only a same-origin HTTP route benefits from staying relative.
|
|
149
|
+
const socket = ep.route.route.protocol === RouteProtocols.SOCKET
|
|
150
|
+
|
|
151
|
+
// Locality is a question about the RESOLVED service, not about what the declaration happens to
|
|
152
|
+
// name: a route that names no service belongs to the asking context, and addressing it absolutely
|
|
153
|
+
// would turn in-app navigation into a full page load.
|
|
154
|
+
if (socket || !ep.isLocal() || opts?.absolute === true) {
|
|
129
155
|
const helper = makeSecurityHelper(ctx)
|
|
130
156
|
path = helper.makeUrl(
|
|
131
|
-
ep.
|
|
157
|
+
ep.address(), path, { host: req?.host, base: req?.base, forceUnsecure: req?.unsecure }
|
|
132
158
|
)
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return [path as any, EntrypointOutcome.Ok]
|
|
161
|
+
return path
|
|
138
162
|
}
|