@owlmeans/client-entrypoint 0.1.18-rc.5 → 0.1.18-rc.50
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 +170 -49
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-entrypoint/SKILL.md +33 -30
- package/build/consts.d.ts +2 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +2 -0
- package/build/consts.js.map +1 -0
- package/build/entrypoint.d.ts +2 -3
- package/build/entrypoint.d.ts.map +1 -1
- package/build/entrypoint.js +27 -39
- package/build/entrypoint.js.map +1 -1
- package/build/failure.d.ts +5 -0
- package/build/failure.d.ts.map +1 -0
- package/build/failure.js +27 -0
- package/build/failure.js.map +1 -0
- package/build/helper/types.d.ts +10 -0
- package/build/helper/types.d.ts.map +1 -0
- package/build/helper/types.js +2 -0
- package/build/helper/types.js.map +1 -0
- package/build/helper.d.ts +5 -6
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +22 -34
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +6 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -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/stab.d.ts +4 -0
- package/build/stab.d.ts.map +1 -0
- package/build/stab.js +5 -0
- package/build/stab.js.map +1 -0
- package/build/types.d.ts +53 -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/types.d.ts +15 -0
- package/build/utils/handler/types.d.ts.map +1 -0
- package/build/utils/handler/types.js +2 -0
- package/build/utils/handler/types.js.map +1 -0
- package/build/utils/handler.d.ts +7 -5
- package/build/utils/handler.d.ts.map +1 -1
- package/build/utils/handler.js +126 -95
- package/build/utils/handler.js.map +1 -1
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/types.local.d.ts +5 -0
- package/build/utils/types.local.d.ts.map +1 -0
- package/build/utils/types.local.js +2 -0
- package/build/utils/types.local.js.map +1 -0
- package/package.json +13 -12
- package/src/consts.ts +1 -0
- package/src/entrypoint.ts +37 -49
- package/src/failure.ts +26 -0
- package/src/helper/types.ts +10 -0
- package/src/helper.ts +30 -49
- package/src/index.ts +6 -2
- package/src/protocol.ts +39 -0
- package/src/stab.ts +6 -0
- package/src/types.ts +62 -11
- package/src/utils/entrypoint.ts +2 -17
- package/src/utils/handler/types.ts +15 -0
- package/src/utils/handler.ts +106 -77
- package/src/utils/index.ts +1 -0
- package/src/utils/types.local.ts +5 -0
package/src/utils/handler.ts
CHANGED
|
@@ -1,53 +1,62 @@
|
|
|
1
1
|
import type { ApiClient } from '@owlmeans/api'
|
|
2
2
|
import { DEFAULT_KEY } from '@owlmeans/client-config'
|
|
3
|
-
import type
|
|
4
|
-
import type {
|
|
5
|
-
import { EntrypointOutcome, provideResponse } from '@owlmeans/entrypoint'
|
|
6
|
-
import type { ClientEntrypoint, EntrypointCall, ClientEntrypointOptions, EntrypointRef, ClientRequest } from '../types.js'
|
|
3
|
+
import { type AbstractRequest, type EntrypointHandler, type EntrypointTransport, EntrypointOutcome, provideResponse, transportAlias } from '@owlmeans/entrypoint'
|
|
4
|
+
import type { ClientEntrypoint, EntrypointInvoke, EntrypointUrlOptions, ClientEntrypointOptions, EntrypointRef, ClientRequest, EntrypointFailureService } from '../types.js'
|
|
7
5
|
import { validate } from './entrypoint.js'
|
|
8
|
-
import {
|
|
9
|
-
import { PARAM } from '@owlmeans/route'
|
|
6
|
+
import { clientRouteHelper } from '@owlmeans/client-route'
|
|
7
|
+
import { PARAM, RouteMethod, RouteProtocols } from '@owlmeans/route'
|
|
10
8
|
import { stringify } from 'qs'
|
|
11
|
-
import { assertContext } from '@owlmeans/context'
|
|
9
|
+
import { assertContext, memoHelper } from '@owlmeans/context'
|
|
12
10
|
import { makeSecurityHelper } from '@owlmeans/config'
|
|
11
|
+
import { ENTRYPOINT_FAILURE_SERVICE } from '../consts.js'
|
|
12
|
+
import { logger } from '@owlmeans/log'
|
|
13
|
+
import type { Config, Context } from './types.local.js'
|
|
14
|
+
import type { ApiCallHelper } from './handler/types.js'
|
|
15
|
+
|
|
16
|
+
const log = logger('client-entrypoint')
|
|
17
|
+
|
|
18
|
+
export const makeApiCallHelper = <T, R extends AbstractRequest = AbstractRequest>(
|
|
19
|
+
ref: EntrypointRef<T, R>
|
|
20
|
+
): ApiCallHelper<T, R> => {
|
|
21
|
+
const apiHandler: EntrypointHandler = async (req, res) => {
|
|
22
|
+
const location = `client-entrypoint:api-handler:${ref.ref?.alias}`
|
|
23
|
+
const context = assertContext<Config, Context>(ref.ref?.ctx as Context, location)
|
|
24
|
+
|
|
25
|
+
if (context.cfg.webService == null) {
|
|
26
|
+
throw new SyntaxError('No webService provided')
|
|
27
|
+
}
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
interface Context<C extends Config = Config> extends ClientContext<C> { }
|
|
29
|
+
const ep = context.entrypoint<ClientEntrypoint>(req.alias)
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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()
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
37
|
+
return context.service<EntrypointTransport>(transport).handle(req, res)
|
|
38
|
+
}
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
|
|
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()
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
? context.cfg.webService[route.service] ?? context.cfg.webService[DEFAULT_KEY]
|
|
34
|
-
: context.cfg.webService[DEFAULT_KEY])
|
|
44
|
+
const alias: string | undefined = typeof context.cfg.webService === 'string'
|
|
45
|
+
? context.cfg.webService
|
|
46
|
+
: context.cfg.webService[route.service] ?? context.cfg.webService[DEFAULT_KEY]
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
if (alias == null) {
|
|
49
|
+
throw new SyntaxError(`Can't cast web service alias for ${ep.alias} entrypoint`)
|
|
50
|
+
}
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
const service: ApiClient = context.service(alias)
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
req.path = ep.path()
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
}
|
|
56
|
+
return service.handler(req, res as any)
|
|
57
|
+
}
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
T, R extends AbstractRequest = AbstractRequest
|
|
49
|
-
>(ref: EntrypointRef<T, R>, opts?: ClientEntrypointOptions) => EntrypointCall<T, R> =
|
|
50
|
-
(ref, opts) => (async (req, res) => {
|
|
59
|
+
const apiInvoke = (opts?: ClientEntrypointOptions): EntrypointInvoke<T, R> => (async (req: Partial<R> | undefined) => {
|
|
51
60
|
const ep = ref.ref
|
|
52
61
|
if (ep == null) {
|
|
53
62
|
throw new SyntaxError('Try to make API call before the entrypoint is created')
|
|
@@ -56,21 +65,24 @@ export const apiCall: <
|
|
|
56
65
|
if (ctx == null) {
|
|
57
66
|
throw new SyntaxError(`No context provided in apiCall for ${ep.alias} entrypoint`)
|
|
58
67
|
}
|
|
59
|
-
await ep.route.resolve(ctx)
|
|
60
68
|
|
|
61
69
|
if (req?.canceled) {
|
|
62
|
-
return
|
|
70
|
+
return { value: null, outcome: EntrypointOutcome.Ok }
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
const request: AbstractRequest = {
|
|
66
74
|
alias: ep.alias,
|
|
67
75
|
params: req?.params ?? {},
|
|
68
|
-
body
|
|
76
|
+
// JSON transports must send a body for mutation methods even when the contract has no
|
|
77
|
+
// payload. Keeping that rule here makes every typed caller use `call({ params })`, never
|
|
78
|
+
// an SDK-specific `{ body: {} }` workaround.
|
|
79
|
+
body: req?.body ?? ([RouteMethod.POST, RouteMethod.PUT, RouteMethod.PATCH]
|
|
80
|
+
.includes(ep.route.route.method ?? RouteMethod.GET) ? {} : undefined),
|
|
69
81
|
headers: req?.headers ?? {},
|
|
70
82
|
query: req?.query ?? {},
|
|
71
83
|
host: req?.host,
|
|
72
84
|
base: req?.base,
|
|
73
|
-
path: ep.
|
|
85
|
+
path: ep.path(),
|
|
74
86
|
timeout: req?.timeout,
|
|
75
87
|
signal: req?.signal,
|
|
76
88
|
}
|
|
@@ -85,54 +97,71 @@ export const apiCall: <
|
|
|
85
97
|
try {
|
|
86
98
|
await validate(ref)(request)
|
|
87
99
|
} catch (e) {
|
|
88
|
-
|
|
100
|
+
log.error('Request validation failed', { alias: ep.alias, error: e })
|
|
89
101
|
throw e
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
|
-
if (res != null) {
|
|
93
|
-
await apiHandler(ref)(request, res)
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
104
|
const reply = provideResponse<unknown>()
|
|
97
105
|
if (ctx == null && ep.ctx == null) {
|
|
98
106
|
throw new SyntaxError(`Use entrypoint ${ep.alias} without context`)
|
|
99
107
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
throw reply.error
|
|
108
|
+
try {
|
|
109
|
+
await apiHandler(request, reply)
|
|
110
|
+
if (reply.error != null) throw reply.error
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (ctx.hasService(ENTRYPOINT_FAILURE_SERVICE)) {
|
|
113
|
+
await ctx.service<EntrypointFailureService>(ENTRYPOINT_FAILURE_SERVICE)
|
|
114
|
+
.notify({ alias: ep.alias, request, error })
|
|
115
|
+
}
|
|
116
|
+
throw error
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
return
|
|
106
|
-
}) as
|
|
119
|
+
return { value: reply.value ?? null, outcome: reply.outcome ?? EntrypointOutcome.Ok }
|
|
120
|
+
}) as EntrypointInvoke<any>
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
throw new SyntaxError(`No context provided in urlCall for ${ep.alias} entrypoint`)
|
|
118
|
-
}
|
|
119
|
-
await ep.route.resolve(ctx)
|
|
120
|
-
|
|
121
|
-
const pathParams = extractParams(ep.getPath())
|
|
122
|
-
let path = pathParams.reduce((p, param) => {
|
|
123
|
-
return p.replace(`${PARAM}${param}`, `${req?.params?.[param as keyof typeof req.params]}`)
|
|
124
|
-
}, ep.getPath()) + (req?.query != null ? `?${stringify(req?.query)}` : '')
|
|
125
|
-
|
|
126
|
-
if (ep.route.route.service !== null && (
|
|
127
|
-
ctx.cfg.service !== ep.route.route.service
|
|
128
|
-
|| req?.full === true)) {
|
|
129
|
-
const helper = makeSecurityHelper(ctx)
|
|
130
|
-
path = helper.makeUrl(
|
|
131
|
-
ep.route.route, path, { host: req?.host, base: req?.base, forceUnsecure: req?.unsecure }
|
|
132
|
-
)
|
|
133
|
-
}
|
|
122
|
+
const entrypointUrl = async (req?: Partial<R>, opts?: EntrypointUrlOptions): Promise<string> => {
|
|
123
|
+
const ep = ref.ref
|
|
124
|
+
if (ep == null) {
|
|
125
|
+
throw new SyntaxError('Try to make URL before the entrypoint is created')
|
|
126
|
+
}
|
|
127
|
+
const ctx = ep.ctx
|
|
128
|
+
if (ctx == null) {
|
|
129
|
+
throw new SyntaxError(`No context provided in entrypointUrl for ${ep.alias} entrypoint`)
|
|
130
|
+
}
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
// The base is left off here: an in-service URL is addressed relative to it, and the absolute
|
|
133
|
+
// branch below has `makeUrl` prepend it.
|
|
134
|
+
const epPath = ep.path()
|
|
135
|
+
const pathParams = clientRouteHelper.extractParams(epPath)
|
|
136
|
+
let path = pathParams.reduce((p, param) => {
|
|
137
|
+
return p.replace(`${PARAM}${param}`, `${req?.params?.[param as keyof typeof req.params]}`)
|
|
138
|
+
}, epPath) + (req?.query != null ? `?${stringify(req?.query)}` : '')
|
|
139
|
+
|
|
140
|
+
// A socket address is always absolute, whoever owns the route. `new WebSocket(path)` resolves a
|
|
141
|
+
// relative value against the PAGE's origin, which in a split deployment is the web host and not
|
|
142
|
+
// the service that answers the upgrade — so the handshake goes somewhere that never speaks it
|
|
143
|
+
// and dies on a gateway timeout. Only a same-origin HTTP route benefits from staying relative.
|
|
144
|
+
const socket = ep.route.route.protocol === RouteProtocols.SOCKET
|
|
145
|
+
|
|
146
|
+
// Locality is a question about the RESOLVED service, not about what the declaration happens to
|
|
147
|
+
// name: a route that names no service belongs to the asking context, and addressing it absolutely
|
|
148
|
+
// would turn in-app navigation into a full page load.
|
|
149
|
+
if (socket || !ep.isLocal() || opts?.absolute === true) {
|
|
150
|
+
const helper = makeSecurityHelper(ctx)
|
|
151
|
+
path = helper.makeUrl(
|
|
152
|
+
ep.address(), path, { host: req?.host, base: req?.base, forceUnsecure: req?.unsecure }
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return path
|
|
157
|
+
}
|
|
136
158
|
|
|
137
|
-
return
|
|
159
|
+
return { apiHandler, apiInvoke, entrypointUrl }
|
|
138
160
|
}
|
|
161
|
+
|
|
162
|
+
export const apiCallOf = memoHelper.oncePer(makeApiCallHelper)
|
|
163
|
+
|
|
164
|
+
/** @deprecated compat:factory-refactor — use `makeApiCallHelper(ref).entrypointUrl(…)` */
|
|
165
|
+
export const entrypointUrl = <T, R extends ClientRequest = ClientRequest>(
|
|
166
|
+
ref: EntrypointRef<T, R>, req?: Partial<R>, opts?: EntrypointUrlOptions
|
|
167
|
+
): Promise<string> => makeApiCallHelper(ref).entrypointUrl(req, opts)
|
package/src/utils/index.ts
CHANGED