ts-procedures 10.1.2 → 10.2.1
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 +33 -86
- package/README.md +11 -6
- package/agent_config/bin/postinstall.mjs +20 -89
- package/agent_config/bin/setup.mjs +51 -264
- package/agent_config/lib/install-skills.mjs +108 -0
- package/agent_config/{claude-code/skills → skills}/ts-procedures/anti-patterns.md +8 -0
- package/agent_config/{claude-code/skills → skills}/ts-procedures/api-reference.md +25 -22
- package/agent_config/{claude-code/skills → skills}/ts-procedures/patterns.md +24 -13
- package/build/adapters/hono/envelope-parity.test.js +6 -17
- package/build/adapters/hono/envelope-parity.test.js.map +1 -1
- package/build/adapters/hono/handlers/http-stream.d.ts +2 -1
- package/build/adapters/hono/handlers/http-stream.js +9 -2
- package/build/adapters/hono/handlers/http-stream.js.map +1 -1
- package/build/adapters/hono/handlers/http.d.ts +2 -1
- package/build/adapters/hono/handlers/http.js +9 -2
- package/build/adapters/hono/handlers/http.js.map +1 -1
- package/build/adapters/hono/handlers/rpc.d.ts +2 -1
- package/build/adapters/hono/handlers/rpc.js +9 -2
- package/build/adapters/hono/handlers/rpc.js.map +1 -1
- package/build/adapters/hono/handlers/stream.d.ts +2 -1
- package/build/adapters/hono/handlers/stream.js +9 -2
- package/build/adapters/hono/handlers/stream.js.map +1 -1
- package/build/adapters/hono/index.d.ts +14 -7
- package/build/adapters/hono/index.js +45 -5
- package/build/adapters/hono/index.js.map +1 -1
- package/build/adapters/hono/index.test.js +55 -0
- package/build/adapters/hono/index.test.js.map +1 -1
- package/build/adapters/hono/request.d.ts +7 -0
- package/build/adapters/hono/request.js +15 -0
- package/build/adapters/hono/request.js.map +1 -1
- package/build/adapters/hono/types.d.ts +31 -0
- package/build/adapters/hono/types.js.map +1 -1
- package/build/codegen/bin/cli.js +1 -1
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/constants.js +2 -2
- package/build/codegen/constants.js.map +1 -1
- package/build/codegen/emit/api-route.js +1 -1
- package/build/codegen/emit/api-route.js.map +1 -1
- package/build/codegen/emit/scope-file.js +1 -1
- package/build/codegen/emit/scope-file.js.map +1 -1
- package/build/codegen/emit-errors.integration.test.js +2 -2
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/codegen/resolve-envelope.js +1 -1
- package/build/codegen/resolve-envelope.js.map +1 -1
- package/build/codegen/test-helpers/run-tsc.js +1 -1
- package/build/codegen/test-helpers/run-tsc.js.map +1 -1
- package/build/core/create-http-stream.d.ts +3 -14
- package/build/core/create-http-stream.js +4 -2
- package/build/core/create-http-stream.js.map +1 -1
- package/build/core/create-http.d.ts +2 -9
- package/build/core/create-http.js +3 -2
- package/build/core/create-http.js.map +1 -1
- package/build/core/create-http.test-d.js +104 -0
- package/build/core/create-http.test-d.js.map +1 -0
- package/build/core/create-stream.js +6 -2
- package/build/core/create-stream.js.map +1 -1
- package/build/core/create.js +5 -4
- package/build/core/create.js.map +1 -1
- package/build/core/create.test.js +63 -0
- package/build/core/create.test.js.map +1 -1
- package/build/core/internal.d.ts +16 -0
- package/build/core/internal.js +22 -0
- package/build/core/internal.js.map +1 -1
- package/build/core/procedures.d.ts +9 -16
- package/build/core/procedures.js +4 -0
- package/build/core/procedures.js.map +1 -1
- package/build/core/types.d.ts +85 -4
- package/build/errors.d.ts +33 -0
- package/build/errors.js +91 -0
- package/build/errors.js.map +1 -0
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +122 -0
- package/build/errors.test.js.map +1 -0
- package/build/exports.d.ts +1 -2
- package/build/exports.js +0 -4
- package/build/exports.js.map +1 -1
- package/build/implementations/http/doc-registry.d.ts +28 -0
- package/build/implementations/http/doc-registry.js +96 -0
- package/build/implementations/http/doc-registry.js.map +1 -0
- package/build/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/implementations/http/doc-registry.test.js +516 -0
- package/build/implementations/http/doc-registry.test.js.map +1 -0
- package/build/implementations/http/error-taxonomy.d.ts +249 -0
- package/build/implementations/http/error-taxonomy.js +252 -0
- package/build/implementations/http/error-taxonomy.js.map +1 -0
- package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/error-taxonomy.test.js +399 -0
- package/build/implementations/http/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +125 -0
- package/build/implementations/http/express-rpc/index.js +216 -0
- package/build/implementations/http/express-rpc/index.js.map +1 -0
- package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/index.test.js +684 -0
- package/build/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/implementations/http/express-rpc/types.js +2 -0
- package/build/implementations/http/express-rpc/types.js.map +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-api/index.d.ts +151 -0
- package/build/implementations/http/hono-api/index.js +344 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +992 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.d.ts +130 -0
- package/build/implementations/http/hono-rpc/index.js +209 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +828 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +159 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-stream/index.d.ts +171 -0
- package/build/implementations/http/hono-stream/index.js +415 -0
- package/build/implementations/http/hono-stream/index.js.map +1 -0
- package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/index.test.js +1383 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/implementations/http/hono-stream/types.js +2 -0
- package/build/implementations/http/hono-stream/types.js.map +1 -0
- package/build/implementations/http/on-request-error.test.d.ts +1 -0
- package/build/implementations/http/on-request-error.test.js +173 -0
- package/build/implementations/http/on-request-error.test.js.map +1 -0
- package/build/implementations/http/route-errors.test.d.ts +1 -0
- package/build/implementations/http/route-errors.test.js +139 -0
- package/build/implementations/http/route-errors.test.js.map +1 -0
- package/build/implementations/types.d.ts +192 -0
- package/build/implementations/types.js +2 -0
- package/build/implementations/types.js.map +1 -0
- package/build/index.d.ts +165 -0
- package/build/index.js +258 -0
- package/build/index.js.map +1 -0
- package/build/index.test.d.ts +1 -0
- package/build/index.test.js +920 -0
- package/build/index.test.js.map +1 -0
- package/build/schema/compute-schema.js +1 -5
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/compute-schema.test.js +0 -13
- package/build/schema/compute-schema.test.js.map +1 -1
- package/build/schema/extract-json-schema.d.ts +2 -0
- package/build/schema/extract-json-schema.js +12 -0
- package/build/schema/extract-json-schema.js.map +1 -0
- package/build/schema/extract-json-schema.test.d.ts +1 -0
- package/build/schema/extract-json-schema.test.js +23 -0
- package/build/schema/extract-json-schema.test.js.map +1 -0
- package/build/schema/parser.d.ts +28 -0
- package/build/schema/parser.js +170 -0
- package/build/schema/parser.js.map +1 -0
- package/build/schema/parser.test.d.ts +1 -0
- package/build/schema/parser.test.js +120 -0
- package/build/schema/parser.test.js.map +1 -0
- package/build/schema/resolve-schema-lib.d.ts +12 -0
- package/build/schema/resolve-schema-lib.js +11 -0
- package/build/schema/resolve-schema-lib.js.map +1 -0
- package/build/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/schema/resolve-schema-lib.test.js +17 -0
- package/build/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/schema/typebox.d.ts +1 -1
- package/build/schema/typebox.js +2 -20
- package/build/schema/typebox.js.map +1 -1
- package/build/schema/typebox.test.js +0 -10
- package/build/schema/typebox.test.js.map +1 -1
- package/build/schema/types.d.ts +8 -0
- package/build/schema/types.js +2 -0
- package/build/schema/types.js.map +1 -0
- package/build/server/context.d.ts +12 -2
- package/build/server/context.js +49 -4
- package/build/server/context.js.map +1 -1
- package/build/server/context.test.js +32 -1
- package/build/server/context.test.js.map +1 -1
- package/build/server/doc-registry.js +0 -3
- package/build/server/doc-registry.js.map +1 -1
- package/build/server/doc-registry.test.js +0 -31
- package/build/server/doc-registry.test.js.map +1 -1
- package/build/server/index.d.ts +0 -1
- package/build/server/index.js +0 -1
- package/build/server/index.js.map +1 -1
- package/build/server/types.d.ts +6 -27
- package/build/stack-utils.d.ts +25 -0
- package/build/stack-utils.js +95 -0
- package/build/stack-utils.js.map +1 -0
- package/build/stack-utils.test.d.ts +1 -0
- package/build/stack-utils.test.js +80 -0
- package/build/stack-utils.test.js.map +1 -0
- package/docs/ai-agent-setup.md +19 -27
- package/docs/client-error-handling.md +1 -1
- package/docs/core.md +8 -0
- package/docs/http-integrations.md +42 -28
- package/package.json +8 -8
- package/src/adapters/astro/README.md +2 -0
- package/src/adapters/hono/envelope-parity.test.ts +6 -18
- package/src/adapters/hono/handlers/http-stream.ts +12 -3
- package/src/adapters/hono/handlers/http.ts +12 -3
- package/src/adapters/hono/handlers/rpc.ts +12 -3
- package/src/adapters/hono/handlers/stream.ts +12 -3
- package/src/adapters/hono/index.test.ts +70 -0
- package/src/adapters/hono/index.ts +53 -11
- package/src/adapters/hono/request.ts +18 -0
- package/src/adapters/hono/types.ts +37 -0
- package/src/codegen/bin/cli.ts +1 -1
- package/src/codegen/constants.ts +2 -2
- package/src/codegen/emit/api-route.ts +1 -1
- package/src/codegen/emit/scope-file.ts +1 -2
- package/src/codegen/emit-errors.integration.test.ts +2 -2
- package/src/codegen/resolve-envelope.ts +1 -1
- package/src/codegen/test-helpers/run-tsc.ts +0 -1
- package/src/core/create-http-stream.ts +20 -13
- package/src/core/create-http.test-d.ts +154 -0
- package/src/core/create-http.ts +19 -8
- package/src/core/create-stream.ts +12 -4
- package/src/core/create.test.ts +74 -0
- package/src/core/create.ts +10 -5
- package/src/core/internal.ts +35 -0
- package/src/core/procedures.ts +4 -0
- package/src/core/types.ts +99 -4
- package/src/exports.ts +6 -4
- package/src/schema/compute-schema.test.ts +0 -16
- package/src/schema/compute-schema.ts +1 -5
- package/src/schema/typebox.test.ts +0 -12
- package/src/schema/typebox.ts +2 -21
- package/src/server/context.test.ts +39 -1
- package/src/server/context.ts +54 -4
- package/src/server/doc-registry.test.ts +0 -34
- package/src/server/doc-registry.ts +0 -3
- package/src/server/index.ts +0 -1
- package/src/server/types.ts +8 -27
- package/agent_config/claude-code/.claude-plugin/plugin.json +0 -5
- package/agent_config/claude-code/agents/ts-procedures-architect.md +0 -138
- package/agent_config/copilot/copilot-instructions.md +0 -521
- package/agent_config/cursor/cursorrules +0 -521
- package/agent_config/lib/install-claude.mjs +0 -57
- package/build/schema/strip-internal-keywords.d.ts +0 -18
- package/build/schema/strip-internal-keywords.js +0 -33
- package/build/schema/strip-internal-keywords.js.map +0 -1
- package/build/schema/strip-internal-keywords.test.js +0 -38
- package/build/schema/strip-internal-keywords.test.js.map +0 -1
- package/build/server/spec-version.d.ts +0 -24
- package/build/server/spec-version.js +0 -26
- package/build/server/spec-version.js.map +0 -1
- package/build/version.d.ts +0 -9
- package/build/version.js +0 -11
- package/build/version.js.map +0 -1
- package/docs/doc-envelope-spec-v1.md +0 -296
- package/docs/doc-envelope-v1.schema.json +0 -253
- package/src/schema/strip-internal-keywords.test.ts +0 -43
- package/src/schema/strip-internal-keywords.ts +0 -36
- package/src/server/spec-version.ts +0 -27
- package/src/version.ts +0 -11
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/SKILL.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/checklist.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/astro-catchall.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/client.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/hono.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/procedure.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/stream-procedure.md +0 -0
- /package/build/{schema/strip-internal-keywords.test.d.ts → core/create-http.test-d.d.ts} +0 -0
package/src/core/create-http.ts
CHANGED
|
@@ -5,12 +5,21 @@ import { captureDefinitionInfo } from './definition-site.js'
|
|
|
5
5
|
import { applyPathPrefix, checkPathParamConsistency } from './http-route.js'
|
|
6
6
|
import {
|
|
7
7
|
assertNotDuplicate,
|
|
8
|
+
runWithMiddlewares,
|
|
8
9
|
shouldSkipValidation,
|
|
9
10
|
toProcedureError,
|
|
10
11
|
validateReqChannels,
|
|
11
12
|
} from './internal.js'
|
|
12
13
|
import type { FactoryRuntime } from './internal.js'
|
|
13
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
HttpReq,
|
|
16
|
+
ProcedureResult,
|
|
17
|
+
TCreateHttpConfig,
|
|
18
|
+
THttpProcedureRegistration,
|
|
19
|
+
THttpReqInput,
|
|
20
|
+
THttpResInput,
|
|
21
|
+
TLocalContext,
|
|
22
|
+
} from './types.js'
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
25
|
* The handler return shape is gated by the DECLARED response schema, not by
|
|
@@ -26,10 +35,6 @@ export type HttpReturn<TRes> =
|
|
|
26
35
|
: TRes extends { body: infer B } ? Infer<B>
|
|
27
36
|
: void
|
|
28
37
|
|
|
29
|
-
type HttpReq<TReq> = TReq extends Record<string, unknown>
|
|
30
|
-
? Prettify<{ [K in keyof TReq]: Infer<TReq[K]> }>
|
|
31
|
-
: undefined
|
|
32
|
-
|
|
33
38
|
export function makeCreateHttp<TContext>(runtime: FactoryRuntime<TContext, any>) {
|
|
34
39
|
/**
|
|
35
40
|
* Defines a REST-shaped HTTP procedure with per-channel input validation
|
|
@@ -40,8 +45,8 @@ export function makeCreateHttp<TContext>(runtime: FactoryRuntime<TContext, any>)
|
|
|
40
45
|
*/
|
|
41
46
|
return function CreateHttp<
|
|
42
47
|
TName extends string,
|
|
43
|
-
TReq extends
|
|
44
|
-
TRes extends
|
|
48
|
+
TReq extends THttpReqInput | undefined = undefined,
|
|
49
|
+
TRes extends THttpResInput | undefined = undefined,
|
|
45
50
|
TErrorKey extends string = string,
|
|
46
51
|
>(
|
|
47
52
|
name: TName,
|
|
@@ -103,7 +108,13 @@ export function makeCreateHttp<TContext>(runtime: FactoryRuntime<TContext, any>)
|
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
const localCtx: TLocalContext = { error: errorFactory }
|
|
106
|
-
|
|
111
|
+
const fullCtx = { ...ctx, ...localCtx } as any
|
|
112
|
+
return await runWithMiddlewares(
|
|
113
|
+
runtime.middlewares,
|
|
114
|
+
fullCtx,
|
|
115
|
+
req,
|
|
116
|
+
(c, r) => handler(c, r as any)
|
|
117
|
+
)
|
|
107
118
|
} catch (error: any) {
|
|
108
119
|
throw toProcedureError(name, definitionInfo, error)
|
|
109
120
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
assertNotDuplicate,
|
|
8
8
|
combineSignals,
|
|
9
9
|
iterateWithGuards,
|
|
10
|
+
runWithMiddlewares,
|
|
10
11
|
shouldSkipValidation,
|
|
11
12
|
validateParams,
|
|
12
13
|
} from './internal.js'
|
|
@@ -89,10 +90,17 @@ export function makeCreateStream<TContext, TExtendedConfig>(runtime: FactoryRunt
|
|
|
89
90
|
|
|
90
91
|
// params is correctly typed at the public API boundary via conditional
|
|
91
92
|
// type; cast here because TS cannot narrow generics inside implementations
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
93
|
+
const fullCtx = { ...ctx, ...streamCtx } as Prettify<TContext & TStreamContext>
|
|
94
|
+
|
|
95
|
+
// Apply factory middleware around obtaining the user generator.
|
|
96
|
+
// Middlewares can short-circuit (return a different generator or throw)
|
|
97
|
+
// or augment before the stream starts.
|
|
98
|
+
const userGenerator = runWithMiddlewares(
|
|
99
|
+
runtime.middlewares,
|
|
100
|
+
fullCtx,
|
|
101
|
+
params,
|
|
102
|
+
(c, p) => handler(c as any, p as any)
|
|
103
|
+
) as AsyncGenerator<any, any, unknown>
|
|
96
104
|
|
|
97
105
|
return yield* iterateWithGuards(userGenerator[Symbol.asyncIterator](), {
|
|
98
106
|
name,
|
package/src/core/create.test.ts
CHANGED
|
@@ -364,6 +364,80 @@ describe('Procedures', () => {
|
|
|
364
364
|
})
|
|
365
365
|
})
|
|
366
366
|
|
|
367
|
+
describe('Procedures({ middleware }) - factory level, ctx + input aware', () => {
|
|
368
|
+
test('middleware runs with same ctx the handler sees and can inspect input', async () => {
|
|
369
|
+
const calls: any[] = []
|
|
370
|
+
|
|
371
|
+
const { Create } = Procedures<{ userId: string }>({
|
|
372
|
+
middleware: [
|
|
373
|
+
({ ctx, input, next }) => {
|
|
374
|
+
calls.push({ userId: ctx.userId, input })
|
|
375
|
+
// "parse the incoming call" using ctx + input
|
|
376
|
+
if ((input as any)?.orgId === 'evil') {
|
|
377
|
+
throw ctx.error('tenant violation')
|
|
378
|
+
}
|
|
379
|
+
return next()
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const { GetThing } = Create('GetThing', { schema: { params: Type.Object({ orgId: Type.String() }) } }, async (ctx, p) => {
|
|
385
|
+
return { seenByHandler: ctx.userId, org: (p as any).orgId }
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
await GetThing({ userId: 'u1' }, { orgId: 'acme' })
|
|
389
|
+
expect(calls).toHaveLength(1)
|
|
390
|
+
expect(calls[0]).toEqual({ userId: 'u1', input: { orgId: 'acme' } })
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('middleware can augment ctx and transform input for the handler', async () => {
|
|
394
|
+
const { Create } = Procedures<{ base: string }>({
|
|
395
|
+
middleware: [
|
|
396
|
+
({ ctx, input, next }) => {
|
|
397
|
+
return next({
|
|
398
|
+
ctx: { ...ctx, extra: 'added' },
|
|
399
|
+
input: { ...(input as any), injected: true },
|
|
400
|
+
})
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const { Echo } = Create('Echo', { schema: { params: Type.Object({ n: Type.Number() }) } }, async (ctx, p: any) => {
|
|
406
|
+
return { base: ctx.base, extra: (ctx as any).extra, received: p }
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const result = await Echo({ base: 'b' }, { n: 42 })
|
|
410
|
+
expect(result).toEqual({
|
|
411
|
+
base: 'b',
|
|
412
|
+
extra: 'added',
|
|
413
|
+
received: { n: 42, injected: true },
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('middleware error uses the factory ctx.error and is wrapped as ProcedureError', async () => {
|
|
418
|
+
const { Create } = Procedures<{}>({
|
|
419
|
+
middleware: [({ ctx }) => { throw ctx.error('from factory mw') }],
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const { Fail } = Create('Fail', {}, async () => 'no')
|
|
423
|
+
await expect(Fail({}, {})).rejects.toThrow('from factory mw')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('multiple factory middlewares compose (order + next)', async () => {
|
|
427
|
+
const order: string[] = []
|
|
428
|
+
const { Create } = Procedures({
|
|
429
|
+
middleware: [
|
|
430
|
+
async ({ next }) => { order.push('mw1'); return next() },
|
|
431
|
+
async ({ next }) => { order.push('mw2'); return next() },
|
|
432
|
+
],
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
const { Ok } = Create('Ok', {}, async () => 'ok')
|
|
436
|
+
await Ok({}, {})
|
|
437
|
+
expect(order).toEqual(['mw1', 'mw2'])
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
367
441
|
describe('Procedures - Definition Location in Errors', () => {
|
|
368
442
|
test('ProcedureValidationError includes definition location', async () => {
|
|
369
443
|
const { Create } = Procedures()
|
package/src/core/create.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { captureDefinitionInfo } from './definition-site.js'
|
|
|
5
5
|
import {
|
|
6
6
|
assertNoHttpFields,
|
|
7
7
|
assertNotDuplicate,
|
|
8
|
+
runWithMiddlewares,
|
|
8
9
|
shouldSkipValidation,
|
|
9
10
|
toProcedureError,
|
|
10
11
|
validateParams,
|
|
@@ -76,11 +77,15 @@ export function makeCreate<TContext, TExtendedConfig>(runtime: FactoryRuntime<TC
|
|
|
76
77
|
error: errorFactory,
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const fullCtx = { ...ctx, ...localCtx } as Prettify<TContext & TLocalContext>
|
|
81
|
+
|
|
82
|
+
// Factory middleware sees the exact same ctx + validated input as the
|
|
83
|
+
// user handler. Supports next({ ctx?, input? }) augmentation.
|
|
84
|
+
return await runWithMiddlewares(
|
|
85
|
+
runtime.middlewares,
|
|
86
|
+
fullCtx,
|
|
87
|
+
params,
|
|
88
|
+
(c, p) => handler(c as any, p as any)
|
|
84
89
|
)
|
|
85
90
|
} catch (error: any) {
|
|
86
91
|
throw toProcedureError(name, definitionInfo, error)
|
package/src/core/internal.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type FactoryRuntime<TContext, TExtendedConfig> = {
|
|
|
20
20
|
adapters: readonly SchemaAdapter[]
|
|
21
21
|
compile: ValidatorCompiler
|
|
22
22
|
httpDefaults?: { pathPrefix?: string; scope?: string }
|
|
23
|
+
/** Factory-level middleware applied to every procedure from this factory. */
|
|
24
|
+
middlewares: Array<(opts: { ctx: any; input: unknown; next: (o?: any) => any }) => any>
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const HTTP_FIELDS = ['path', 'method', 'req', 'res', 'successStatus'] as const
|
|
@@ -142,6 +144,39 @@ export function combineSignals(incoming: AbortSignal | undefined, own: AbortSign
|
|
|
142
144
|
return incoming ? AbortSignal.any([incoming, own]) : own
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Apply factory-defined middlewares around a core invocation.
|
|
149
|
+
* Middlewares run from first to last; each sees the (possibly augmented) ctx + input.
|
|
150
|
+
* Supports both unary results and async generators (for streams).
|
|
151
|
+
*/
|
|
152
|
+
export function runWithMiddlewares(
|
|
153
|
+
middlewares: Array<(opts: { ctx: any; input: unknown; next: (o?: any) => any }) => any>,
|
|
154
|
+
ctx: any,
|
|
155
|
+
input: unknown,
|
|
156
|
+
core: (c: any, i: unknown) => any
|
|
157
|
+
): any {
|
|
158
|
+
if (!middlewares.length) {
|
|
159
|
+
return core(ctx, input)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build the chain from the inside out
|
|
163
|
+
let current = core
|
|
164
|
+
|
|
165
|
+
for (let i = middlewares.length - 1; i >= 0; i--) {
|
|
166
|
+
const mw = middlewares[i]!
|
|
167
|
+
const previous = current
|
|
168
|
+
current = (c: any, i: unknown) =>
|
|
169
|
+
mw({
|
|
170
|
+
ctx: c,
|
|
171
|
+
input: i,
|
|
172
|
+
next: (o: any = {}) => previous(o.ctx ?? c, o.input ?? i),
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return current(ctx, input)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
145
180
|
/**
|
|
146
181
|
* Drives a user stream iterator with the framework guarantees shared by
|
|
147
182
|
* CreateStream and CreateHttpStream:
|
package/src/core/procedures.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { AnyProcedureRegistration, ProceduresOptions, TNoContextProvided }
|
|
|
18
18
|
* schema: { adapters: [myZodAdapter] }, // TypeBox is built in
|
|
19
19
|
* http: { pathPrefix: '/v1', scope: 'billing' }, // CreateHttp* defaults
|
|
20
20
|
* onCreate: (procedure) => log(procedure.kind, procedure.name),
|
|
21
|
+
* // middleware: [ myFactoryGuard ], // runs with final ctx + input for every proc
|
|
21
22
|
* })
|
|
22
23
|
* ```
|
|
23
24
|
*
|
|
@@ -26,6 +27,8 @@ import type { AnyProcedureRegistration, ProceduresOptions, TNoContextProvided }
|
|
|
26
27
|
* - `TExtendedConfig` — extra per-procedure config fields (e.g. `scope`,
|
|
27
28
|
* `version`, auth flags) carried through to registrations, `info`, and
|
|
28
29
|
* `onCreate`.
|
|
30
|
+
* - `middleware` — factory-level middleware with access to the final ctx + input
|
|
31
|
+
* ("parse the incoming call") for authz, enrichment, etc. See `FactoryMiddleware`.
|
|
29
32
|
*/
|
|
30
33
|
export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unknown>(
|
|
31
34
|
options?: ProceduresOptions<TContext, TExtendedConfig>,
|
|
@@ -39,6 +42,7 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
39
42
|
adapters: [...(options?.schema?.adapters ?? []), typeboxAdapter],
|
|
40
43
|
compile: createValidatorCompiler(options?.validation === false ? undefined : options?.validation),
|
|
41
44
|
httpDefaults: options?.http,
|
|
45
|
+
middlewares: options?.middleware ?? [],
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
const Create = makeCreate<TContext, TExtendedConfig>(runtime)
|
package/src/core/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as AJV from 'ajv'
|
|
2
2
|
import type { ProcedureError } from './errors.js'
|
|
3
3
|
import type { SchemaAdapter } from '../schema/adapter.js'
|
|
4
|
-
import type { TJSONSchema } from '../schema/json-schema.js'
|
|
4
|
+
import type { Infer, Prettify, TJSONSchema } from '../schema/json-schema.js'
|
|
5
5
|
import type { Validate } from '../schema/compile.js'
|
|
6
6
|
|
|
7
7
|
export type TNoContextProvided = unknown
|
|
@@ -69,7 +69,64 @@ export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = u
|
|
|
69
69
|
|
|
70
70
|
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Canonical shape for `schema.req` channels.
|
|
74
|
+
* Used both to constrain generics and to provide autocomplete when writing
|
|
75
|
+
* `CreateHttp` / `CreateHttpStream` config objects.
|
|
76
|
+
*
|
|
77
|
+
* Note: RPC `Create` / `CreateStream` use a flatter `schema: { params?, yieldType?, returnType? }`
|
|
78
|
+
* directly (no extra container generic), so they don't need an equivalent named input shape.
|
|
79
|
+
*/
|
|
80
|
+
export type THttpReqInput = {
|
|
81
|
+
pathParams?: unknown
|
|
82
|
+
query?: unknown
|
|
83
|
+
body?: unknown
|
|
84
|
+
headers?: unknown
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Canonical shape for `schema.res` on CreateHttp (CreateHttpStream only supports `headers`).
|
|
89
|
+
*/
|
|
90
|
+
export type THttpResInput = {
|
|
91
|
+
body?: unknown
|
|
92
|
+
headers?: unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @internal Helper to surface literal channel keys for autocomplete
|
|
97
|
+
* while still threading the user's concrete generic schemas through for inference.
|
|
98
|
+
*/
|
|
99
|
+
export type HttpSchemaInput<Base, T> = Base & (T extends undefined ? {} : T)
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @internal Shared helper for typing the destructured `req` argument passed to
|
|
103
|
+
* CreateHttp / CreateHttpStream handlers. Mirrors the per-channel Infer.
|
|
104
|
+
*/
|
|
105
|
+
export type HttpReq<TReq> = TReq extends Record<string, unknown>
|
|
106
|
+
? Prettify<{ [K in keyof TReq]: Infer<TReq[K]> }>
|
|
107
|
+
: undefined
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Shape used for the `schema` option of `CreateHttpStream`.
|
|
111
|
+
* Reuses the same req input shape as CreateHttp for autocomplete consistency.
|
|
112
|
+
*/
|
|
113
|
+
export type THttpStreamSchema<
|
|
114
|
+
TReq extends THttpReqInput | undefined = undefined,
|
|
115
|
+
TYield = unknown,
|
|
116
|
+
TReturn = void,
|
|
117
|
+
TResHeaders = undefined,
|
|
118
|
+
> = {
|
|
119
|
+
req?: HttpSchemaInput<THttpReqInput, TReq>
|
|
120
|
+
yield?: TYield
|
|
121
|
+
returnType?: TReturn
|
|
122
|
+
res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type TCreateHttpConfig<
|
|
126
|
+
TReq extends THttpReqInput | undefined = undefined,
|
|
127
|
+
TRes extends THttpResInput | undefined = undefined,
|
|
128
|
+
TErrorKey extends string = string,
|
|
129
|
+
> = {
|
|
73
130
|
path: string
|
|
74
131
|
method: HttpMethod
|
|
75
132
|
successStatus?: number
|
|
@@ -78,8 +135,10 @@ export type TCreateHttpConfig<TReq, TRes, TErrorKey extends string = string> = {
|
|
|
78
135
|
description?: string
|
|
79
136
|
/** Optional — a no-input, no-output route (e.g. a 204 logout) may omit it. */
|
|
80
137
|
schema?: {
|
|
81
|
-
|
|
82
|
-
|
|
138
|
+
// HttpSchemaInput surfaces the channel keys for IDE autocomplete / excess-property checking
|
|
139
|
+
// while the generic part carries the caller's concrete schema (for Infer + handler typing).
|
|
140
|
+
req?: HttpSchemaInput<THttpReqInput, TReq>
|
|
141
|
+
res?: HttpSchemaInput<THttpResInput, TRes>
|
|
83
142
|
}
|
|
84
143
|
}
|
|
85
144
|
|
|
@@ -193,4 +252,40 @@ export type ProceduresOptions<TContext = unknown, TExtendedConfig = unknown> = {
|
|
|
193
252
|
pathPrefix?: string
|
|
194
253
|
scope?: string
|
|
195
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Optional middleware that runs for *every* procedure defined via this factory.
|
|
258
|
+
* Runs after schema validation but before the user handler, with the *same*
|
|
259
|
+
* context the handler receives (including injected `error()` and `signal`).
|
|
260
|
+
*
|
|
261
|
+
* Use this for cross-cutting logic that needs the resolved `TContext` + the
|
|
262
|
+
* incoming call data ("parse the incoming call"):
|
|
263
|
+
* - Authorization / tenancy checks using `ctx.user` + `input`
|
|
264
|
+
* - Enriching ctx for all procedures on the factory
|
|
265
|
+
* - Auditing, feature flags, etc.
|
|
266
|
+
*
|
|
267
|
+
* The middleware receives a `next` function. Call `await next({ ctx?, input? })`
|
|
268
|
+
* to continue (optionally with augmented ctx or transformed input). You may
|
|
269
|
+
* also return/throw early.
|
|
270
|
+
*
|
|
271
|
+
* Works for both direct calls and all adapters. Complements (runs after)
|
|
272
|
+
* builder-level middleware on HonoAppBuilder.
|
|
273
|
+
*/
|
|
274
|
+
middleware?: FactoryMiddleware<TContext>[]
|
|
196
275
|
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Middleware defined on a Procedures factory.
|
|
279
|
+
*
|
|
280
|
+
* - `ctx` is the fully resolved context (your TContext + `error()` + `signal`).
|
|
281
|
+
* - `input` is the (already schema-validated) call argument:
|
|
282
|
+
* - For Create / CreateStream: the `params` object
|
|
283
|
+
* - For CreateHttp / CreateHttpStream: the `{ pathParams?, query?, body?, headers? }`
|
|
284
|
+
* - Call `next()` (or `next({ ctx: augmented, input: transformed })`) to proceed.
|
|
285
|
+
*/
|
|
286
|
+
export type FactoryMiddleware<TContext = unknown> = (opts: {
|
|
287
|
+
ctx: Prettify<TContext & TLocalContext>
|
|
288
|
+
input: unknown
|
|
289
|
+
next: (opts?: { ctx?: any; input?: unknown }) => Promise<any> | AsyncGenerator<any, any, unknown>
|
|
290
|
+
}) => Promise<any> | AsyncGenerator<any, any, unknown> | void
|
|
291
|
+
|
package/src/exports.ts
CHANGED
|
@@ -23,10 +23,16 @@ export type {
|
|
|
23
23
|
THttpProcedureRegistration,
|
|
24
24
|
THttpStreamProcedureRegistration,
|
|
25
25
|
TCreateHttpConfig,
|
|
26
|
+
THttpReqInput,
|
|
27
|
+
THttpResInput,
|
|
28
|
+
THttpStreamSchema,
|
|
29
|
+
HttpSchemaInput,
|
|
30
|
+
HttpReq,
|
|
26
31
|
TLocalContext,
|
|
27
32
|
TStreamContext,
|
|
28
33
|
TNoContextProvided,
|
|
29
34
|
HttpMethod,
|
|
35
|
+
FactoryMiddleware,
|
|
30
36
|
} from './core/types.js'
|
|
31
37
|
export type { HttpReturn } from './core/create-http.js'
|
|
32
38
|
export {
|
|
@@ -56,9 +62,5 @@ export type { Infer, TSchemaLib, TJSONSchema, Prettify } from './schema/json-sch
|
|
|
56
62
|
|
|
57
63
|
// Doc envelope — offline codegen input
|
|
58
64
|
export { writeDocEnvelope } from './server/doc-envelope.js'
|
|
59
|
-
// `generatorVersion` metadata lives on `ts-procedures/server`
|
|
60
|
-
// (DOC_ENVELOPE_GENERATOR_VERSION) — it's niche telemetry. The spec version is
|
|
61
|
-
// the one consumers branch on, so it gets the prominent top-level export.
|
|
62
|
-
export { DOC_ENVELOPE_SPEC_VERSION } from './server/spec-version.js'
|
|
63
65
|
export type { DocEnvelopeSource } from './server/doc-envelope.js'
|
|
64
66
|
export type { DocEnvelope } from './server/types.js'
|
|
@@ -245,22 +245,6 @@ describe('computeSchema', () => {
|
|
|
245
245
|
).toThrow(/Error extracting json schema schema\.req\.query/)
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
-
it('handles Type.Optional(Type.Unsafe(...)) without leaking ~markers into strict AJV or output', () => {
|
|
249
|
-
// typebox >= 1.2.0: this composition emits `~unsafe`/`~optional` as
|
|
250
|
-
// enumerable props, which crashed strict AJV and polluted the JSON Schema.
|
|
251
|
-
const schema = {
|
|
252
|
-
params: Type.Object({ x: Type.Optional(Type.Unsafe({ type: 'string' })) }),
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const result = computeSchema('test-procedure', schema, options)
|
|
256
|
-
|
|
257
|
-
// No registration crash, and the stored JSON Schema is clean.
|
|
258
|
-
expect(JSON.stringify(result.jsonSchema.params)).not.toMatch(/~/)
|
|
259
|
-
expect((result.jsonSchema.params as any).properties.x).toEqual({ type: 'string' })
|
|
260
|
-
// Validator works against the cleaned schema.
|
|
261
|
-
expect(result.validations.params!({ x: 'hello' })).toEqual({})
|
|
262
|
-
})
|
|
263
|
-
|
|
264
248
|
it('throws a compile error when the schema is recognized but invalid JSON Schema', () => {
|
|
265
249
|
// Looks like a TypeBox schema (has ~kind) but AJV rejects it at compile time
|
|
266
250
|
const recognizedButInvalid = { '~kind': 'Object', type: 'not-a-real-type' }
|
|
@@ -2,7 +2,6 @@ import { ProcedureRegistrationError } from '../core/errors.js'
|
|
|
2
2
|
import type { DefinitionInfo } from '../core/definition-site.js'
|
|
3
3
|
import { extractJsonSchema } from './adapter.js'
|
|
4
4
|
import type { SchemaAdapter } from './adapter.js'
|
|
5
|
-
import { stripInternalKeywords } from './strip-internal-keywords.js'
|
|
6
5
|
import type { TJSONSchema } from './json-schema.js'
|
|
7
6
|
import type { TSchemaValidationError, Validate, ValidatorCompiler } from './compile.js'
|
|
8
7
|
|
|
@@ -68,10 +67,7 @@ function extractChannel(ctx: ComputeContext, channel: string, raw: unknown): TJS
|
|
|
68
67
|
`Error extracting json schema ${channel} - it might be empty or not recognized by any schema adapter (registered: ${known})`,
|
|
69
68
|
)
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
// compositions leak as enumerable properties — they break strict AJV and
|
|
73
|
-
// pollute serialized docs/codegen. See strip-internal-keywords.ts.
|
|
74
|
-
return stripInternalKeywords(extracted)
|
|
70
|
+
return extracted
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
/** Extracts and compiles one validated channel. */
|
|
@@ -9,18 +9,6 @@ describe('typebox schema detection', () => {
|
|
|
9
9
|
expect(isTypeboxSchema(typebox)).toBe(true)
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
test('it recognizes a top-level Type.Unsafe schema (~unsafe marker, no ~kind)', () => {
|
|
13
|
-
// typebox >= 1.2.0: Type.Unsafe carries `~unsafe` INSTEAD of `~kind`.
|
|
14
|
-
expect(isTypeboxSchema(Type.Unsafe({ type: 'string' }))).toBe(true)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('it recognizes any future ~-prefixed marker (convention, not a name list)', () => {
|
|
18
|
-
// Detection keys off TypeBox's `~` namespace convention, so a marker that
|
|
19
|
-
// doesn't exist yet is still recognized without a code change.
|
|
20
|
-
const futureMarker = { '~somethingNew': true, type: 'object' }
|
|
21
|
-
expect(isTypeboxSchema(futureMarker)).toBe(true)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
12
|
test('it rejects non-TypeBox values', () => {
|
|
25
13
|
expect(isTypeboxSchema({ type: 'object' })).toBe(false)
|
|
26
14
|
expect(isTypeboxSchema('string value')).toBe(false)
|
package/src/schema/typebox.ts
CHANGED
|
@@ -3,29 +3,10 @@ import type { TJSONSchema } from './json-schema.js'
|
|
|
3
3
|
|
|
4
4
|
const TYPEBOX_KIND = Symbol.for('TypeBox.Kind')
|
|
5
5
|
|
|
6
|
-
/** TypeBox v1 prefixes every internal marker key with `~`. */
|
|
7
|
-
const TILDE = '~'.charCodeAt(0)
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* TypeBox v1 namespaces all of its internal markers under a `~` prefix
|
|
11
|
-
* (`~kind`, `~unsafe`, `~optional`, ...) — non-enumerable own properties that
|
|
12
|
-
* stay out of the serialized JSON Schema. Detecting the *convention* rather
|
|
13
|
-
* than specific marker names means new markers (e.g. `~unsafe`, added in
|
|
14
|
-
* typebox 1.2.0 and used by `Type.Unsafe(...)` *instead of* `~kind`) are
|
|
15
|
-
* recognized without a code change. Reflect.ownKeys is required because these
|
|
16
|
-
* markers are non-enumerable.
|
|
17
|
-
*/
|
|
18
|
-
function hasTypeBoxV1Marker(schema: object): boolean {
|
|
19
|
-
for (const key of Reflect.ownKeys(schema)) {
|
|
20
|
-
if (typeof key === 'string' && key.charCodeAt(0) === TILDE) return true
|
|
21
|
-
}
|
|
22
|
-
return false
|
|
23
|
-
}
|
|
24
|
-
|
|
25
6
|
/**
|
|
26
7
|
* Built-in adapter for TypeBox (`import { Type } from 'typebox'`).
|
|
27
8
|
*
|
|
28
|
-
* Detection covers both typebox v1 (
|
|
9
|
+
* Detection covers both typebox v1 (`~kind` property) and legacy
|
|
29
10
|
* `@sinclair/typebox` 0.3x (`Symbol.for('TypeBox.Kind')`). TypeBox schemas
|
|
30
11
|
* already ARE JSON Schema, so conversion is the identity.
|
|
31
12
|
*/
|
|
@@ -33,7 +14,7 @@ export const typeboxAdapter: SchemaAdapter = {
|
|
|
33
14
|
name: 'typebox',
|
|
34
15
|
detect(schema: unknown): boolean {
|
|
35
16
|
if (typeof schema !== 'object' || schema === null) return false
|
|
36
|
-
return
|
|
17
|
+
return '~kind' in schema || TYPEBOX_KIND in schema
|
|
37
18
|
},
|
|
38
19
|
toJsonSchema(schema: unknown): TJSONSchema {
|
|
39
20
|
return schema as TJSONSchema
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { resolveFactoryContext } from './context.js'
|
|
2
|
+
import { resolveFactoryContext, composeContextInits } from './context.js'
|
|
3
3
|
|
|
4
4
|
describe('resolveFactoryContext', () => {
|
|
5
5
|
it('returns a plain value as-is', async () => {
|
|
@@ -19,4 +19,42 @@ describe('resolveFactoryContext', () => {
|
|
|
19
19
|
resolveFactoryContext(async () => ({ ready: true }), null),
|
|
20
20
|
).resolves.toEqual({ ready: true })
|
|
21
21
|
})
|
|
22
|
+
|
|
23
|
+
it('resolves an array of inits with shallow merge (later wins)', async () => {
|
|
24
|
+
const raw = { id: 'r1' }
|
|
25
|
+
const composed = [
|
|
26
|
+
{ a: 1 },
|
|
27
|
+
(r: any) => ({ b: r.id }),
|
|
28
|
+
async () => ({ c: 3 }),
|
|
29
|
+
]
|
|
30
|
+
await expect(resolveFactoryContext(composed as any, raw)).resolves.toEqual({
|
|
31
|
+
a: 1,
|
|
32
|
+
b: 'r1',
|
|
33
|
+
c: 3,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('later layers override earlier keys', async () => {
|
|
38
|
+
const layers = [{ tenant: 't1' }, { tenant: 't2', extra: true }]
|
|
39
|
+
await expect(resolveFactoryContext(layers as any, {})).resolves.toEqual({
|
|
40
|
+
tenant: 't2',
|
|
41
|
+
extra: true,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('composeContextInits', () => {
|
|
47
|
+
it('returns a reusable composed initializer', async () => {
|
|
48
|
+
const init = composeContextInits<{ token: string | undefined }, { token?: string }>(
|
|
49
|
+
{ base: true } as any,
|
|
50
|
+
(r: { token?: string }) => ({ token: r.token }),
|
|
51
|
+
)
|
|
52
|
+
const out = await resolveFactoryContext(init, { token: 'abc' })
|
|
53
|
+
expect(out).toEqual({ base: true, token: 'abc' })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('handles empty compose as empty object', async () => {
|
|
57
|
+
const init = composeContextInits()
|
|
58
|
+
await expect(resolveFactoryContext(init, {})).resolves.toEqual({})
|
|
59
|
+
})
|
|
22
60
|
})
|
package/src/server/context.ts
CHANGED
|
@@ -2,17 +2,67 @@
|
|
|
2
2
|
* A factory context as accepted by server adapters' `register()`: either the
|
|
3
3
|
* context value itself, or a function deriving it from the adapter's request
|
|
4
4
|
* context per request (sync or async).
|
|
5
|
+
*
|
|
6
|
+
* Arrays are supported for composition (resolved left-to-right with shallow
|
|
7
|
+
* merge of object results). Use `composeContextInits` for a reusable composed
|
|
8
|
+
* initializer.
|
|
5
9
|
*/
|
|
6
10
|
export type FactoryContextInit<TContext, TRaw> =
|
|
7
11
|
| TContext
|
|
8
12
|
| ((raw: TRaw) => TContext | Promise<TContext>)
|
|
13
|
+
| Array<FactoryContextInit<TContext, TRaw>>
|
|
9
14
|
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Compose multiple context initializers (values or functions) into one.
|
|
17
|
+
* Later initializers shallow-merge over earlier results (object case).
|
|
18
|
+
* Functions are resolved in order against the raw + the accumulating context.
|
|
19
|
+
*/
|
|
20
|
+
export function composeContextInits<TContext, TRaw>(
|
|
21
|
+
...inits: FactoryContextInit<TContext, TRaw>[]
|
|
22
|
+
): FactoryContextInit<TContext, TRaw> {
|
|
23
|
+
const flat = inits.flat() as FactoryContextInit<TContext, TRaw>[]
|
|
24
|
+
if (flat.length === 0) return {} as TContext
|
|
25
|
+
if (flat.length === 1) return flat[0]!
|
|
26
|
+
|
|
27
|
+
return async (raw: TRaw): Promise<TContext> => {
|
|
28
|
+
let acc: any = {}
|
|
29
|
+
for (const init of flat) {
|
|
30
|
+
const part = typeof init === 'function'
|
|
31
|
+
? await (init as (r: TRaw) => TContext | Promise<TContext>)(raw)
|
|
32
|
+
: init
|
|
33
|
+
if (part && typeof part === 'object') {
|
|
34
|
+
acc = { ...acc, ...part }
|
|
35
|
+
} else if (part !== undefined) {
|
|
36
|
+
// Non-object last-wins (rare, but supported)
|
|
37
|
+
acc = part
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return acc as TContext
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolves a {@link FactoryContextInit} (or array) for one request. */
|
|
11
45
|
export async function resolveFactoryContext<TContext, TRaw>(
|
|
12
46
|
init: FactoryContextInit<TContext, TRaw>,
|
|
13
47
|
raw: TRaw,
|
|
14
48
|
): Promise<TContext> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
:
|
|
49
|
+
if (Array.isArray(init)) {
|
|
50
|
+
// Use the same merge loop here to avoid calling a potentially non-fn value.
|
|
51
|
+
let acc: any = {}
|
|
52
|
+
for (const i of init) {
|
|
53
|
+
const part = typeof i === 'function'
|
|
54
|
+
? await (i as (r: TRaw) => TContext | Promise<TContext>)(raw)
|
|
55
|
+
: i
|
|
56
|
+
if (part && typeof part === 'object') {
|
|
57
|
+
acc = { ...acc, ...part }
|
|
58
|
+
} else if (part !== undefined) {
|
|
59
|
+
acc = part
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return acc as TContext
|
|
63
|
+
}
|
|
64
|
+
if (typeof init === 'function') {
|
|
65
|
+
return await (init as (raw: TRaw) => TContext | Promise<TContext>)(raw)
|
|
66
|
+
}
|
|
67
|
+
return init
|
|
18
68
|
}
|