@geekmidas/services 1.0.3 → 1.1.0
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/README.md +24 -0
- package/dist/{ServiceDiscovery-ykolgkIj.d.mts → ServiceDiscovery-BO8W-RY8.d.cts} +2 -2
- package/dist/{ServiceDiscovery-ykolgkIj.d.mts.map → ServiceDiscovery-BO8W-RY8.d.cts.map} +1 -1
- package/dist/{ServiceDiscovery-Dvqa-Q1_.d.cts → ServiceDiscovery-CRq_bGbi.d.mts} +2 -2
- package/dist/{ServiceDiscovery-Dvqa-Q1_.d.cts.map → ServiceDiscovery-CRq_bGbi.d.mts.map} +1 -1
- package/dist/{ServiceDiscovery-tZ6VKHIZ.mjs → ServiceDiscovery-DO_uDfuG.mjs} +2 -2
- package/dist/{ServiceDiscovery-tZ6VKHIZ.mjs.map → ServiceDiscovery-DO_uDfuG.mjs.map} +1 -1
- package/dist/{ServiceDiscovery-C5x1wcN1.cjs → ServiceDiscovery-m3XkNh32.cjs} +2 -2
- package/dist/{ServiceDiscovery-C5x1wcN1.cjs.map → ServiceDiscovery-m3XkNh32.cjs.map} +1 -1
- package/dist/{context-BpYagzpr.mjs → context-C9DCrFWA.mjs} +64 -4
- package/dist/context-C9DCrFWA.mjs.map +1 -0
- package/dist/{context-CaeISj3o.d.cts → context-CU-87vsM.d.mts} +2 -2
- package/dist/context-CU-87vsM.d.mts.map +1 -0
- package/dist/{context-CoyHq8lH.cjs → context-Cf2Ig5qr.cjs} +70 -4
- package/dist/context-Cf2Ig5qr.cjs.map +1 -0
- package/dist/{context-HGC2PJzv.d.mts → context-D2owD3ac.d.cts} +2 -2
- package/dist/context-D2owD3ac.d.cts.map +1 -0
- package/dist/context.cjs +1 -1
- package/dist/context.d.cts +2 -2
- package/dist/context.d.mts +2 -2
- package/dist/context.mjs +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/middy.cjs +120 -0
- package/dist/middy.cjs.map +1 -0
- package/dist/middy.d.cts +146 -0
- package/dist/middy.d.cts.map +1 -0
- package/dist/middy.d.mts +146 -0
- package/dist/middy.d.mts.map +1 -0
- package/dist/middy.mjs +118 -0
- package/dist/middy.mjs.map +1 -0
- package/dist/trpc.cjs +66 -0
- package/dist/trpc.cjs.map +1 -0
- package/dist/trpc.d.cts +86 -0
- package/dist/trpc.d.cts.map +1 -0
- package/dist/trpc.d.mts +86 -0
- package/dist/trpc.d.mts.map +1 -0
- package/dist/trpc.mjs +65 -0
- package/dist/trpc.mjs.map +1 -0
- package/dist/{types-CcHmCx_U.d.mts → types-Bgk-0NOO.d.cts} +9 -2
- package/dist/types-Bgk-0NOO.d.cts.map +1 -0
- package/dist/{types-D7d_yeU5.d.cts → types-BoyOCa57.d.mts} +9 -2
- package/dist/types-BoyOCa57.d.mts.map +1 -0
- package/docs/request-scoped-logging.md +167 -0
- package/package.json +37 -1
- package/src/__tests__/context.spec.ts +180 -4
- package/src/__tests__/middy.spec.ts +218 -0
- package/src/__tests__/trpc.spec.ts +218 -0
- package/src/context.ts +104 -8
- package/src/middy.ts +226 -0
- package/src/trpc.ts +190 -0
- package/src/types.ts +7 -0
- package/tsdown.config.ts +11 -1
- package/dist/ServiceDiscovery.cjs +0 -4
- package/dist/ServiceDiscovery.d.cts +0 -3
- package/dist/ServiceDiscovery.d.mts +0 -3
- package/dist/ServiceDiscovery.mjs +0 -4
- package/dist/context-BpYagzpr.mjs.map +0 -1
- package/dist/context-CaeISj3o.d.cts.map +0 -1
- package/dist/context-CoyHq8lH.cjs.map +0 -1
- package/dist/context-HGC2PJzv.d.mts.map +0 -1
- package/dist/types-CcHmCx_U.d.mts.map +0 -1
- package/dist/types-D7d_yeU5.d.cts.map +0 -1
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +0 -2
- package/dist/types.d.mts +0 -2
- package/dist/types.mjs +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trpc.cjs","names":["mw: CreateMiddleware<TContext, TMeta>","envParser?: EnvironmentParser<{}>","services: [...T]"],"sources":["../src/trpc.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport type {\n\tTRPCMiddlewareBuilder,\n\tTRPCMiddlewareFunction,\n} from '@trpc/server';\nimport { runWithRequestContext } from './context';\nimport { ServiceDiscovery, type ServiceRecord } from './ServiceDiscovery';\nimport type { Service } from './types';\n\n/**\n * Shape of `t.middleware` from `@trpc/server`. We accept this rather than the\n * initialized `t` object so callers retain ownership of their tRPC instance\n * (no double-initialization, no opinion on context/meta shape).\n */\ntype CreateMiddleware<TContext, TMeta> = <$ContextOverrides>(\n\tfn: TRPCMiddlewareFunction<\n\t\tTContext,\n\t\tTMeta,\n\t\tobject,\n\t\t$ContextOverrides,\n\t\tunknown\n\t>,\n) => TRPCMiddlewareBuilder<TContext, TMeta, $ContextOverrides, unknown>;\n\n/**\n * Result of `createServicesMiddleware`: a function that accepts a service\n * tuple and returns a tRPC middleware that merges resolved services onto the\n * context.\n */\nexport type ServicesMiddleware<\n\tTContext extends object,\n\tTMeta extends object,\n> = <const T extends Service[]>(\n\tservices: [...T],\n) => TRPCMiddlewareBuilder<TContext, TMeta, ServiceRecord<T>, unknown>;\n\n/**\n * Context shape required by `createServicesMiddleware` overload 2.\n * Procedures that pull services via context-stored discovery must expose it\n * under `serviceDiscovery`.\n */\nexport interface ContextWithServiceDiscovery {\n\tserviceDiscovery: ServiceDiscovery;\n}\n\n/**\n * Minimum context required for request-context propagation. `logger` must be\n * present so services can call `serviceContext.getLogger()`. `requestId` and\n * `startTime` are auto-generated if missing.\n */\nexport interface ContextWithLogger {\n\tlogger: Logger;\n\trequestId?: string;\n\tstartTime?: number;\n}\n\n/**\n * Create a tRPC middleware that:\n *\n * 1. Resolves the requested services via `ServiceDiscovery`.\n * 2. Wraps the downstream call in `runWithRequestContext` so any code reached\n * by the procedure (including service method implementations) can read the\n * current logger/request id via `serviceContext`.\n * 3. Merges the resolved services onto the tRPC context so handlers can access\n * them by service name (`ctx.database`, `ctx.cache`, ...).\n *\n * Two overloads:\n * - Pass an `envParser` to create a per-request `ServiceDiscovery` instance.\n * - Omit `envParser` to read `ctx.serviceDiscovery` from the tRPC context.\n *\n * @example\n * ```ts\n * import { initTRPC } from '@trpc/server';\n * import { createServicesMiddleware } from '@geekmidas/services/trpc';\n *\n * const t = initTRPC.context<Context>().create();\n * const withServices = createServicesMiddleware(t.middleware, envParser);\n *\n * export const authedProcedure = t.procedure.use(\n * withServices([databaseService, cacheService]),\n * );\n * ```\n */\nexport function createServicesMiddleware<\n\tTContext extends ContextWithLogger & object,\n\tTMeta extends object,\n>(\n\tmw: CreateMiddleware<TContext, TMeta>,\n\tenvParser: EnvironmentParser<{}>,\n): ServicesMiddleware<TContext, TMeta>;\nexport function createServicesMiddleware<\n\tTContext extends ContextWithLogger & ContextWithServiceDiscovery & object,\n\tTMeta extends object,\n>(mw: CreateMiddleware<TContext, TMeta>): ServicesMiddleware<TContext, TMeta>;\nexport function createServicesMiddleware<\n\tTContext extends ContextWithLogger & object,\n\tTMeta extends object,\n>(\n\tmw: CreateMiddleware<TContext, TMeta>,\n\tenvParser?: EnvironmentParser<{}>,\n): ServicesMiddleware<TContext, TMeta> {\n\treturn (<const T extends Service[]>(services: [...T]) => {\n\t\tconst builder = mw(async (opts) => {\n\t\t\tconst ctx = opts.ctx as TContext & Partial<ContextWithServiceDiscovery>;\n\n\t\t\tconst discovery =\n\t\t\t\tctx.serviceDiscovery ??\n\t\t\t\tServiceDiscovery.getInstance(\n\t\t\t\t\tenvParser ??\n\t\t\t\t\t\t(() => {\n\t\t\t\t\t\t\t// Hit only if overload 2 was selected but ctx.serviceDiscovery is\n\t\t\t\t\t\t\t// missing at runtime — surface the mistake immediately rather\n\t\t\t\t\t\t\t// than letting an undefined env parser fail deep inside register.\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t'createServicesMiddleware: no `envParser` provided and ' +\n\t\t\t\t\t\t\t\t\t'`ctx.serviceDiscovery` is missing. Pass an EnvironmentParser ' +\n\t\t\t\t\t\t\t\t\t'to createServicesMiddleware(), or attach a ServiceDiscovery ' +\n\t\t\t\t\t\t\t\t\t'instance to the tRPC context.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})(),\n\t\t\t\t);\n\n\t\t\tconst requestId = ctx.requestId ?? randomUUID();\n\t\t\tconst startTime = ctx.startTime ?? Date.now();\n\n\t\t\treturn runWithRequestContext(\n\t\t\t\t{ logger: ctx.logger, requestId, startTime },\n\t\t\t\tasync () => {\n\t\t\t\t\tconst resolved = await discovery.register(services);\n\t\t\t\t\treturn opts.next({\n\t\t\t\t\t\tctx: { ...opts.ctx, ...resolved } as typeof opts.ctx &\n\t\t\t\t\t\t\tServiceRecord<T>,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t);\n\t\t});\n\n\t\t// Tag the inner middleware function with the requested services so external\n\t\t// tooling (e.g. detect-procedures route generators) can introspect a\n\t\t// procedure's service dependencies without re-executing middleware.\n\t\tconst middlewares = (\n\t\t\tbuilder as unknown as { _middlewares?: Array<{ _services?: Service[] }> }\n\t\t)._middlewares;\n\t\tif (middlewares?.length) {\n\t\t\tconst last = middlewares[middlewares.length - 1];\n\t\t\tif (last) last._services = services as unknown as Service[];\n\t\t}\n\n\t\treturn builder as TRPCMiddlewareBuilder<\n\t\t\tTContext,\n\t\t\tTMeta,\n\t\t\tServiceRecord<T>,\n\t\t\tunknown\n\t\t>;\n\t}) as ServicesMiddleware<TContext, TMeta>;\n}\n\n/**\n * Create a tRPC middleware that establishes a request context for downstream\n * code without resolving any services. Useful when services aren't needed on\n * a procedure but the handler (or libraries it calls) still wants to read\n * `serviceContext.getLogger()` / `getRequestId()` / `getRequestStartTime()`.\n *\n * `requestId` and `startTime` are pulled from the tRPC context when present,\n * otherwise generated (`randomUUID()` and `Date.now()`).\n *\n * @example\n * ```ts\n * const withRequestContext = createRequestContextMiddleware(t.middleware);\n * export const baseProcedure = t.procedure.use(withRequestContext);\n * ```\n */\nexport function createRequestContextMiddleware<\n\tTContext extends ContextWithLogger & object,\n\tTMeta extends object,\n>(\n\tmw: CreateMiddleware<TContext, TMeta>,\n): TRPCMiddlewareBuilder<TContext, TMeta, object, unknown> {\n\treturn mw(async (opts) => {\n\t\tconst ctx = opts.ctx as TContext;\n\t\tconst requestId = ctx.requestId ?? randomUUID();\n\t\tconst startTime = ctx.startTime ?? Date.now();\n\t\treturn runWithRequestContext(\n\t\t\t{ logger: ctx.logger, requestId, startTime },\n\t\t\t() => opts.next(),\n\t\t);\n\t});\n}\n"],"mappings":";;;;;AAgGA,SAAgB,yBAIfA,IACAC,WACsC;AACtC,QAAQ,CAA4BC,aAAqB;EACxD,MAAM,UAAU,GAAG,OAAO,SAAS;GAClC,MAAM,MAAM,KAAK;GAEjB,MAAM,YACL,IAAI,oBACJ,0CAAiB,YAChB,aACC,CAAC,MAAM;AAIN,UAAM,IAAI,MACT;GAKD,IAAG,CACL;GAEF,MAAM,YAAY,IAAI,aAAa,6BAAY;GAC/C,MAAM,YAAY,IAAI,aAAa,KAAK,KAAK;AAE7C,UAAO,sCACN;IAAE,QAAQ,IAAI;IAAQ;IAAW;GAAW,GAC5C,YAAY;IACX,MAAM,WAAW,MAAM,UAAU,SAAS,SAAS;AACnD,WAAO,KAAK,KAAK,EAChB,KAAK;KAAE,GAAG,KAAK;KAAK,GAAG;IAAU,EAEjC,EAAC;GACF,EACD;EACD,EAAC;EAKF,MAAM,cACL,QACC;AACF,MAAI,aAAa,QAAQ;GACxB,MAAM,OAAO,YAAY,YAAY,SAAS;AAC9C,OAAI,KAAM,MAAK,YAAY;EAC3B;AAED,SAAO;CAMP;AACD;;;;;;;;;;;;;;;;AAiBD,SAAgB,+BAIfF,IAC0D;AAC1D,QAAO,GAAG,OAAO,SAAS;EACzB,MAAM,MAAM,KAAK;EACjB,MAAM,YAAY,IAAI,aAAa,6BAAY;EAC/C,MAAM,YAAY,IAAI,aAAa,KAAK,KAAK;AAC7C,SAAO,sCACN;GAAE,QAAQ,IAAI;GAAQ;GAAW;EAAW,GAC5C,MAAM,KAAK,MAAM,CACjB;CACD,EAAC;AACF"}
|
package/dist/trpc.d.cts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Service } from "./types-Bgk-0NOO.cjs";
|
|
2
|
+
import { ServiceDiscovery, ServiceRecord } from "./ServiceDiscovery-BO8W-RY8.cjs";
|
|
3
|
+
import { Logger } from "@geekmidas/logger";
|
|
4
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
5
|
+
import { TRPCMiddlewareBuilder, TRPCMiddlewareFunction } from "@trpc/server";
|
|
6
|
+
|
|
7
|
+
//#region src/trpc.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shape of `t.middleware` from `@trpc/server`. We accept this rather than the
|
|
11
|
+
* initialized `t` object so callers retain ownership of their tRPC instance
|
|
12
|
+
* (no double-initialization, no opinion on context/meta shape).
|
|
13
|
+
*/
|
|
14
|
+
type CreateMiddleware<TContext, TMeta> = <$ContextOverrides>(fn: TRPCMiddlewareFunction<TContext, TMeta, object, $ContextOverrides, unknown>) => TRPCMiddlewareBuilder<TContext, TMeta, $ContextOverrides, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Result of `createServicesMiddleware`: a function that accepts a service
|
|
17
|
+
* tuple and returns a tRPC middleware that merges resolved services onto the
|
|
18
|
+
* context.
|
|
19
|
+
*/
|
|
20
|
+
type ServicesMiddleware<TContext extends object, TMeta extends object> = <const T extends Service[]>(services: [...T]) => TRPCMiddlewareBuilder<TContext, TMeta, ServiceRecord<T>, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Context shape required by `createServicesMiddleware` overload 2.
|
|
23
|
+
* Procedures that pull services via context-stored discovery must expose it
|
|
24
|
+
* under `serviceDiscovery`.
|
|
25
|
+
*/
|
|
26
|
+
interface ContextWithServiceDiscovery {
|
|
27
|
+
serviceDiscovery: ServiceDiscovery;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Minimum context required for request-context propagation. `logger` must be
|
|
31
|
+
* present so services can call `serviceContext.getLogger()`. `requestId` and
|
|
32
|
+
* `startTime` are auto-generated if missing.
|
|
33
|
+
*/
|
|
34
|
+
interface ContextWithLogger {
|
|
35
|
+
logger: Logger;
|
|
36
|
+
requestId?: string;
|
|
37
|
+
startTime?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a tRPC middleware that:
|
|
41
|
+
*
|
|
42
|
+
* 1. Resolves the requested services via `ServiceDiscovery`.
|
|
43
|
+
* 2. Wraps the downstream call in `runWithRequestContext` so any code reached
|
|
44
|
+
* by the procedure (including service method implementations) can read the
|
|
45
|
+
* current logger/request id via `serviceContext`.
|
|
46
|
+
* 3. Merges the resolved services onto the tRPC context so handlers can access
|
|
47
|
+
* them by service name (`ctx.database`, `ctx.cache`, ...).
|
|
48
|
+
*
|
|
49
|
+
* Two overloads:
|
|
50
|
+
* - Pass an `envParser` to create a per-request `ServiceDiscovery` instance.
|
|
51
|
+
* - Omit `envParser` to read `ctx.serviceDiscovery` from the tRPC context.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { initTRPC } from '@trpc/server';
|
|
56
|
+
* import { createServicesMiddleware } from '@geekmidas/services/trpc';
|
|
57
|
+
*
|
|
58
|
+
* const t = initTRPC.context<Context>().create();
|
|
59
|
+
* const withServices = createServicesMiddleware(t.middleware, envParser);
|
|
60
|
+
*
|
|
61
|
+
* export const authedProcedure = t.procedure.use(
|
|
62
|
+
* withServices([databaseService, cacheService]),
|
|
63
|
+
* );
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function createServicesMiddleware<TContext extends ContextWithLogger & object, TMeta extends object>(mw: CreateMiddleware<TContext, TMeta>, envParser: EnvironmentParser<{}>): ServicesMiddleware<TContext, TMeta>;
|
|
67
|
+
declare function createServicesMiddleware<TContext extends ContextWithLogger & ContextWithServiceDiscovery & object, TMeta extends object>(mw: CreateMiddleware<TContext, TMeta>): ServicesMiddleware<TContext, TMeta>;
|
|
68
|
+
/**
|
|
69
|
+
* Create a tRPC middleware that establishes a request context for downstream
|
|
70
|
+
* code without resolving any services. Useful when services aren't needed on
|
|
71
|
+
* a procedure but the handler (or libraries it calls) still wants to read
|
|
72
|
+
* `serviceContext.getLogger()` / `getRequestId()` / `getRequestStartTime()`.
|
|
73
|
+
*
|
|
74
|
+
* `requestId` and `startTime` are pulled from the tRPC context when present,
|
|
75
|
+
* otherwise generated (`randomUUID()` and `Date.now()`).
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
80
|
+
* export const baseProcedure = t.procedure.use(withRequestContext);
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
declare function createRequestContextMiddleware<TContext extends ContextWithLogger & object, TMeta extends object>(mw: CreateMiddleware<TContext, TMeta>): TRPCMiddlewareBuilder<TContext, TMeta, object, unknown>;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { ContextWithLogger, ContextWithServiceDiscovery, ServicesMiddleware, createRequestContextMiddleware, createServicesMiddleware };
|
|
86
|
+
//# sourceMappingURL=trpc.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trpc.d.cts","names":[],"sources":["../src/trpc.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AASuC;;;KAOlC,gBAGH,CAAA,QAAA,EAAA,KAAA,CAAA,GAAA,CAAA,iBAAA,CAAA,CAAA,EAAA,EAFG,sBAEH,CADA,QACA,EAAA,KAAA,EAAA,MAAA,EAEA,iBAFA,EAAA,OAAA,CAAA,EAAA,GAKG,qBALH,CAKyB,QALzB,EAKmC,KALnC,EAK0C,iBAL1C,EAAA,OAAA,CAAA;;;;;;AAKG,KAOO,kBAPP,CAAA,iBAAA,MAAA,EAAA,cAAA,MAAA,CAAA,GAAA,CAAA,gBAUgB,OAVhB,EAAA,CAAA,CAAA,QAAA,EAAA,CAAA,GAWU,CAXV,CAAA,EAAA,GAYA,qBAZA,CAYsB,QAZtB,EAYgC,KAZhC,EAYuC,aAZvC,CAYqD,CAZrD,CAAA,EAAA,OAAA,CAAA;AAAqB;AAO1B;;;;AAK2B,UAOV,2BAAA,CAPU;EAAQ,gBAAE,EAQlB,gBARkB;;;;AAAX;AAO1B;AASA;AAiCgB,UAjCC,iBAAA,CAiCuB;EAAA,MAAA,EAhC/B,MAgC+B;EAAA,SACtB,CAAA,EAAA,MAAA;EAAiB,SAGb,CAAA,EAAA,MAAA;;;;;;;AAED;AACrB;;;;;;;;;;AAG4D;AA+E5D;;;;;;;;;AAKwB;iBA9FR,0CACE,sDAGb,iBAAiB,UAAU,mBACpB,wBACT,mBAAmB,UAAU;iBAChB,0CACE,oBAAoB,gEAEhC,iBAAiB,UAAU,SAAS,mBAAmB,UAAU;;;;;;;;;;;;;;;;iBA+EvD,gDACE,sDAGb,iBAAiB,UAAU,SAC7B,sBAAsB,UAAU"}
|
package/dist/trpc.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Service } from "./types-BoyOCa57.mjs";
|
|
2
|
+
import { ServiceDiscovery, ServiceRecord } from "./ServiceDiscovery-CRq_bGbi.mjs";
|
|
3
|
+
import { Logger } from "@geekmidas/logger";
|
|
4
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
5
|
+
import { TRPCMiddlewareBuilder, TRPCMiddlewareFunction } from "@trpc/server";
|
|
6
|
+
|
|
7
|
+
//#region src/trpc.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shape of `t.middleware` from `@trpc/server`. We accept this rather than the
|
|
11
|
+
* initialized `t` object so callers retain ownership of their tRPC instance
|
|
12
|
+
* (no double-initialization, no opinion on context/meta shape).
|
|
13
|
+
*/
|
|
14
|
+
type CreateMiddleware<TContext, TMeta> = <$ContextOverrides>(fn: TRPCMiddlewareFunction<TContext, TMeta, object, $ContextOverrides, unknown>) => TRPCMiddlewareBuilder<TContext, TMeta, $ContextOverrides, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Result of `createServicesMiddleware`: a function that accepts a service
|
|
17
|
+
* tuple and returns a tRPC middleware that merges resolved services onto the
|
|
18
|
+
* context.
|
|
19
|
+
*/
|
|
20
|
+
type ServicesMiddleware<TContext extends object, TMeta extends object> = <const T extends Service[]>(services: [...T]) => TRPCMiddlewareBuilder<TContext, TMeta, ServiceRecord<T>, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Context shape required by `createServicesMiddleware` overload 2.
|
|
23
|
+
* Procedures that pull services via context-stored discovery must expose it
|
|
24
|
+
* under `serviceDiscovery`.
|
|
25
|
+
*/
|
|
26
|
+
interface ContextWithServiceDiscovery {
|
|
27
|
+
serviceDiscovery: ServiceDiscovery;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Minimum context required for request-context propagation. `logger` must be
|
|
31
|
+
* present so services can call `serviceContext.getLogger()`. `requestId` and
|
|
32
|
+
* `startTime` are auto-generated if missing.
|
|
33
|
+
*/
|
|
34
|
+
interface ContextWithLogger {
|
|
35
|
+
logger: Logger;
|
|
36
|
+
requestId?: string;
|
|
37
|
+
startTime?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a tRPC middleware that:
|
|
41
|
+
*
|
|
42
|
+
* 1. Resolves the requested services via `ServiceDiscovery`.
|
|
43
|
+
* 2. Wraps the downstream call in `runWithRequestContext` so any code reached
|
|
44
|
+
* by the procedure (including service method implementations) can read the
|
|
45
|
+
* current logger/request id via `serviceContext`.
|
|
46
|
+
* 3. Merges the resolved services onto the tRPC context so handlers can access
|
|
47
|
+
* them by service name (`ctx.database`, `ctx.cache`, ...).
|
|
48
|
+
*
|
|
49
|
+
* Two overloads:
|
|
50
|
+
* - Pass an `envParser` to create a per-request `ServiceDiscovery` instance.
|
|
51
|
+
* - Omit `envParser` to read `ctx.serviceDiscovery` from the tRPC context.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { initTRPC } from '@trpc/server';
|
|
56
|
+
* import { createServicesMiddleware } from '@geekmidas/services/trpc';
|
|
57
|
+
*
|
|
58
|
+
* const t = initTRPC.context<Context>().create();
|
|
59
|
+
* const withServices = createServicesMiddleware(t.middleware, envParser);
|
|
60
|
+
*
|
|
61
|
+
* export const authedProcedure = t.procedure.use(
|
|
62
|
+
* withServices([databaseService, cacheService]),
|
|
63
|
+
* );
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function createServicesMiddleware<TContext extends ContextWithLogger & object, TMeta extends object>(mw: CreateMiddleware<TContext, TMeta>, envParser: EnvironmentParser<{}>): ServicesMiddleware<TContext, TMeta>;
|
|
67
|
+
declare function createServicesMiddleware<TContext extends ContextWithLogger & ContextWithServiceDiscovery & object, TMeta extends object>(mw: CreateMiddleware<TContext, TMeta>): ServicesMiddleware<TContext, TMeta>;
|
|
68
|
+
/**
|
|
69
|
+
* Create a tRPC middleware that establishes a request context for downstream
|
|
70
|
+
* code without resolving any services. Useful when services aren't needed on
|
|
71
|
+
* a procedure but the handler (or libraries it calls) still wants to read
|
|
72
|
+
* `serviceContext.getLogger()` / `getRequestId()` / `getRequestStartTime()`.
|
|
73
|
+
*
|
|
74
|
+
* `requestId` and `startTime` are pulled from the tRPC context when present,
|
|
75
|
+
* otherwise generated (`randomUUID()` and `Date.now()`).
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
80
|
+
* export const baseProcedure = t.procedure.use(withRequestContext);
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
declare function createRequestContextMiddleware<TContext extends ContextWithLogger & object, TMeta extends object>(mw: CreateMiddleware<TContext, TMeta>): TRPCMiddlewareBuilder<TContext, TMeta, object, unknown>;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { ContextWithLogger, ContextWithServiceDiscovery, ServicesMiddleware, createRequestContextMiddleware, createServicesMiddleware };
|
|
86
|
+
//# sourceMappingURL=trpc.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trpc.d.mts","names":[],"sources":["../src/trpc.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AASuC;;;KAOlC,gBAGH,CAAA,QAAA,EAAA,KAAA,CAAA,GAAA,CAAA,iBAAA,CAAA,CAAA,EAAA,EAFG,sBAEH,CADA,QACA,EAAA,KAAA,EAAA,MAAA,EAEA,iBAFA,EAAA,OAAA,CAAA,EAAA,GAKG,qBALH,CAKyB,QALzB,EAKmC,KALnC,EAK0C,iBAL1C,EAAA,OAAA,CAAA;;;;;;AAKG,KAOO,kBAPP,CAAA,iBAAA,MAAA,EAAA,cAAA,MAAA,CAAA,GAAA,CAAA,gBAUgB,OAVhB,EAAA,CAAA,CAAA,QAAA,EAAA,CAAA,GAWU,CAXV,CAAA,EAAA,GAYA,qBAZA,CAYsB,QAZtB,EAYgC,KAZhC,EAYuC,aAZvC,CAYqD,CAZrD,CAAA,EAAA,OAAA,CAAA;AAAqB;AAO1B;;;;AAK2B,UAOV,2BAAA,CAPU;EAAQ,gBAAE,EAQlB,gBARkB;;;;AAAX;AAO1B;AASA;AAiCgB,UAjCC,iBAAA,CAiCuB;EAAA,MAAA,EAhC/B,MAgC+B;EAAA,SACtB,CAAA,EAAA,MAAA;EAAiB,SAGb,CAAA,EAAA,MAAA;;;;;;;AAED;AACrB;;;;;;;;;;AAG4D;AA+E5D;;;;;;;;;AAKwB;iBA9FR,0CACE,sDAGb,iBAAiB,UAAU,mBACpB,wBACT,mBAAmB,UAAU;iBAChB,0CACE,oBAAoB,gEAEhC,iBAAiB,UAAU,SAAS,mBAAmB,UAAU;;;;;;;;;;;;;;;;iBA+EvD,gDACE,sDAGb,iBAAiB,UAAU,SAC7B,sBAAsB,UAAU"}
|
package/dist/trpc.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { runWithRequestContext } from "./context-C9DCrFWA.mjs";
|
|
2
|
+
import { ServiceDiscovery } from "./ServiceDiscovery-DO_uDfuG.mjs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
//#region src/trpc.ts
|
|
6
|
+
function createServicesMiddleware(mw, envParser) {
|
|
7
|
+
return (services) => {
|
|
8
|
+
const builder = mw(async (opts) => {
|
|
9
|
+
const ctx = opts.ctx;
|
|
10
|
+
const discovery = ctx.serviceDiscovery ?? ServiceDiscovery.getInstance(envParser ?? (() => {
|
|
11
|
+
throw new Error("createServicesMiddleware: no `envParser` provided and `ctx.serviceDiscovery` is missing. Pass an EnvironmentParser to createServicesMiddleware(), or attach a ServiceDiscovery instance to the tRPC context.");
|
|
12
|
+
})());
|
|
13
|
+
const requestId = ctx.requestId ?? randomUUID();
|
|
14
|
+
const startTime = ctx.startTime ?? Date.now();
|
|
15
|
+
return runWithRequestContext({
|
|
16
|
+
logger: ctx.logger,
|
|
17
|
+
requestId,
|
|
18
|
+
startTime
|
|
19
|
+
}, async () => {
|
|
20
|
+
const resolved = await discovery.register(services);
|
|
21
|
+
return opts.next({ ctx: {
|
|
22
|
+
...opts.ctx,
|
|
23
|
+
...resolved
|
|
24
|
+
} });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
const middlewares = builder._middlewares;
|
|
28
|
+
if (middlewares?.length) {
|
|
29
|
+
const last = middlewares[middlewares.length - 1];
|
|
30
|
+
if (last) last._services = services;
|
|
31
|
+
}
|
|
32
|
+
return builder;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a tRPC middleware that establishes a request context for downstream
|
|
37
|
+
* code without resolving any services. Useful when services aren't needed on
|
|
38
|
+
* a procedure but the handler (or libraries it calls) still wants to read
|
|
39
|
+
* `serviceContext.getLogger()` / `getRequestId()` / `getRequestStartTime()`.
|
|
40
|
+
*
|
|
41
|
+
* `requestId` and `startTime` are pulled from the tRPC context when present,
|
|
42
|
+
* otherwise generated (`randomUUID()` and `Date.now()`).
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
47
|
+
* export const baseProcedure = t.procedure.use(withRequestContext);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
function createRequestContextMiddleware(mw) {
|
|
51
|
+
return mw(async (opts) => {
|
|
52
|
+
const ctx = opts.ctx;
|
|
53
|
+
const requestId = ctx.requestId ?? randomUUID();
|
|
54
|
+
const startTime = ctx.startTime ?? Date.now();
|
|
55
|
+
return runWithRequestContext({
|
|
56
|
+
logger: ctx.logger,
|
|
57
|
+
requestId,
|
|
58
|
+
startTime
|
|
59
|
+
}, () => opts.next());
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { createRequestContextMiddleware, createServicesMiddleware };
|
|
65
|
+
//# sourceMappingURL=trpc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trpc.mjs","names":["mw: CreateMiddleware<TContext, TMeta>","envParser?: EnvironmentParser<{}>","services: [...T]"],"sources":["../src/trpc.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport type {\n\tTRPCMiddlewareBuilder,\n\tTRPCMiddlewareFunction,\n} from '@trpc/server';\nimport { runWithRequestContext } from './context';\nimport { ServiceDiscovery, type ServiceRecord } from './ServiceDiscovery';\nimport type { Service } from './types';\n\n/**\n * Shape of `t.middleware` from `@trpc/server`. We accept this rather than the\n * initialized `t` object so callers retain ownership of their tRPC instance\n * (no double-initialization, no opinion on context/meta shape).\n */\ntype CreateMiddleware<TContext, TMeta> = <$ContextOverrides>(\n\tfn: TRPCMiddlewareFunction<\n\t\tTContext,\n\t\tTMeta,\n\t\tobject,\n\t\t$ContextOverrides,\n\t\tunknown\n\t>,\n) => TRPCMiddlewareBuilder<TContext, TMeta, $ContextOverrides, unknown>;\n\n/**\n * Result of `createServicesMiddleware`: a function that accepts a service\n * tuple and returns a tRPC middleware that merges resolved services onto the\n * context.\n */\nexport type ServicesMiddleware<\n\tTContext extends object,\n\tTMeta extends object,\n> = <const T extends Service[]>(\n\tservices: [...T],\n) => TRPCMiddlewareBuilder<TContext, TMeta, ServiceRecord<T>, unknown>;\n\n/**\n * Context shape required by `createServicesMiddleware` overload 2.\n * Procedures that pull services via context-stored discovery must expose it\n * under `serviceDiscovery`.\n */\nexport interface ContextWithServiceDiscovery {\n\tserviceDiscovery: ServiceDiscovery;\n}\n\n/**\n * Minimum context required for request-context propagation. `logger` must be\n * present so services can call `serviceContext.getLogger()`. `requestId` and\n * `startTime` are auto-generated if missing.\n */\nexport interface ContextWithLogger {\n\tlogger: Logger;\n\trequestId?: string;\n\tstartTime?: number;\n}\n\n/**\n * Create a tRPC middleware that:\n *\n * 1. Resolves the requested services via `ServiceDiscovery`.\n * 2. Wraps the downstream call in `runWithRequestContext` so any code reached\n * by the procedure (including service method implementations) can read the\n * current logger/request id via `serviceContext`.\n * 3. Merges the resolved services onto the tRPC context so handlers can access\n * them by service name (`ctx.database`, `ctx.cache`, ...).\n *\n * Two overloads:\n * - Pass an `envParser` to create a per-request `ServiceDiscovery` instance.\n * - Omit `envParser` to read `ctx.serviceDiscovery` from the tRPC context.\n *\n * @example\n * ```ts\n * import { initTRPC } from '@trpc/server';\n * import { createServicesMiddleware } from '@geekmidas/services/trpc';\n *\n * const t = initTRPC.context<Context>().create();\n * const withServices = createServicesMiddleware(t.middleware, envParser);\n *\n * export const authedProcedure = t.procedure.use(\n * withServices([databaseService, cacheService]),\n * );\n * ```\n */\nexport function createServicesMiddleware<\n\tTContext extends ContextWithLogger & object,\n\tTMeta extends object,\n>(\n\tmw: CreateMiddleware<TContext, TMeta>,\n\tenvParser: EnvironmentParser<{}>,\n): ServicesMiddleware<TContext, TMeta>;\nexport function createServicesMiddleware<\n\tTContext extends ContextWithLogger & ContextWithServiceDiscovery & object,\n\tTMeta extends object,\n>(mw: CreateMiddleware<TContext, TMeta>): ServicesMiddleware<TContext, TMeta>;\nexport function createServicesMiddleware<\n\tTContext extends ContextWithLogger & object,\n\tTMeta extends object,\n>(\n\tmw: CreateMiddleware<TContext, TMeta>,\n\tenvParser?: EnvironmentParser<{}>,\n): ServicesMiddleware<TContext, TMeta> {\n\treturn (<const T extends Service[]>(services: [...T]) => {\n\t\tconst builder = mw(async (opts) => {\n\t\t\tconst ctx = opts.ctx as TContext & Partial<ContextWithServiceDiscovery>;\n\n\t\t\tconst discovery =\n\t\t\t\tctx.serviceDiscovery ??\n\t\t\t\tServiceDiscovery.getInstance(\n\t\t\t\t\tenvParser ??\n\t\t\t\t\t\t(() => {\n\t\t\t\t\t\t\t// Hit only if overload 2 was selected but ctx.serviceDiscovery is\n\t\t\t\t\t\t\t// missing at runtime — surface the mistake immediately rather\n\t\t\t\t\t\t\t// than letting an undefined env parser fail deep inside register.\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t'createServicesMiddleware: no `envParser` provided and ' +\n\t\t\t\t\t\t\t\t\t'`ctx.serviceDiscovery` is missing. Pass an EnvironmentParser ' +\n\t\t\t\t\t\t\t\t\t'to createServicesMiddleware(), or attach a ServiceDiscovery ' +\n\t\t\t\t\t\t\t\t\t'instance to the tRPC context.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})(),\n\t\t\t\t);\n\n\t\t\tconst requestId = ctx.requestId ?? randomUUID();\n\t\t\tconst startTime = ctx.startTime ?? Date.now();\n\n\t\t\treturn runWithRequestContext(\n\t\t\t\t{ logger: ctx.logger, requestId, startTime },\n\t\t\t\tasync () => {\n\t\t\t\t\tconst resolved = await discovery.register(services);\n\t\t\t\t\treturn opts.next({\n\t\t\t\t\t\tctx: { ...opts.ctx, ...resolved } as typeof opts.ctx &\n\t\t\t\t\t\t\tServiceRecord<T>,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t);\n\t\t});\n\n\t\t// Tag the inner middleware function with the requested services so external\n\t\t// tooling (e.g. detect-procedures route generators) can introspect a\n\t\t// procedure's service dependencies without re-executing middleware.\n\t\tconst middlewares = (\n\t\t\tbuilder as unknown as { _middlewares?: Array<{ _services?: Service[] }> }\n\t\t)._middlewares;\n\t\tif (middlewares?.length) {\n\t\t\tconst last = middlewares[middlewares.length - 1];\n\t\t\tif (last) last._services = services as unknown as Service[];\n\t\t}\n\n\t\treturn builder as TRPCMiddlewareBuilder<\n\t\t\tTContext,\n\t\t\tTMeta,\n\t\t\tServiceRecord<T>,\n\t\t\tunknown\n\t\t>;\n\t}) as ServicesMiddleware<TContext, TMeta>;\n}\n\n/**\n * Create a tRPC middleware that establishes a request context for downstream\n * code without resolving any services. Useful when services aren't needed on\n * a procedure but the handler (or libraries it calls) still wants to read\n * `serviceContext.getLogger()` / `getRequestId()` / `getRequestStartTime()`.\n *\n * `requestId` and `startTime` are pulled from the tRPC context when present,\n * otherwise generated (`randomUUID()` and `Date.now()`).\n *\n * @example\n * ```ts\n * const withRequestContext = createRequestContextMiddleware(t.middleware);\n * export const baseProcedure = t.procedure.use(withRequestContext);\n * ```\n */\nexport function createRequestContextMiddleware<\n\tTContext extends ContextWithLogger & object,\n\tTMeta extends object,\n>(\n\tmw: CreateMiddleware<TContext, TMeta>,\n): TRPCMiddlewareBuilder<TContext, TMeta, object, unknown> {\n\treturn mw(async (opts) => {\n\t\tconst ctx = opts.ctx as TContext;\n\t\tconst requestId = ctx.requestId ?? randomUUID();\n\t\tconst startTime = ctx.startTime ?? Date.now();\n\t\treturn runWithRequestContext(\n\t\t\t{ logger: ctx.logger, requestId, startTime },\n\t\t\t() => opts.next(),\n\t\t);\n\t});\n}\n"],"mappings":";;;;;AAgGA,SAAgB,yBAIfA,IACAC,WACsC;AACtC,QAAQ,CAA4BC,aAAqB;EACxD,MAAM,UAAU,GAAG,OAAO,SAAS;GAClC,MAAM,MAAM,KAAK;GAEjB,MAAM,YACL,IAAI,oBACJ,iBAAiB,YAChB,aACC,CAAC,MAAM;AAIN,UAAM,IAAI,MACT;GAKD,IAAG,CACL;GAEF,MAAM,YAAY,IAAI,aAAa,YAAY;GAC/C,MAAM,YAAY,IAAI,aAAa,KAAK,KAAK;AAE7C,UAAO,sBACN;IAAE,QAAQ,IAAI;IAAQ;IAAW;GAAW,GAC5C,YAAY;IACX,MAAM,WAAW,MAAM,UAAU,SAAS,SAAS;AACnD,WAAO,KAAK,KAAK,EAChB,KAAK;KAAE,GAAG,KAAK;KAAK,GAAG;IAAU,EAEjC,EAAC;GACF,EACD;EACD,EAAC;EAKF,MAAM,cACL,QACC;AACF,MAAI,aAAa,QAAQ;GACxB,MAAM,OAAO,YAAY,YAAY,SAAS;AAC9C,OAAI,KAAM,MAAK,YAAY;EAC3B;AAED,SAAO;CAMP;AACD;;;;;;;;;;;;;;;;AAiBD,SAAgB,+BAIfF,IAC0D;AAC1D,QAAO,GAAG,OAAO,SAAS;EACzB,MAAM,MAAM,KAAK;EACjB,MAAM,YAAY,IAAI,aAAa,YAAY;EAC/C,MAAM,YAAY,IAAI,aAAa,KAAK,KAAK;AAC7C,SAAO,sBACN;GAAE,QAAQ,IAAI;GAAQ;GAAW;EAAW,GAC5C,MAAM,KAAK,MAAM,CACjB;CACD,EAAC;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
2
1
|
import { Logger } from "@geekmidas/logger";
|
|
2
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
|
|
@@ -11,6 +11,13 @@ import { Logger } from "@geekmidas/logger";
|
|
|
11
11
|
interface ServiceContext {
|
|
12
12
|
/**
|
|
13
13
|
* Get the current request's logger.
|
|
14
|
+
*
|
|
15
|
+
* Returns a **request-scoped proxy** that re-resolves the underlying logger
|
|
16
|
+
* from AsyncLocalStorage on every call. This makes it safe for a singleton
|
|
17
|
+
* service to capture the logger once (e.g. during `register()`) and reuse it
|
|
18
|
+
* across requests — each log call routes to the current request's logger
|
|
19
|
+
* instead of freezing the first request's logger.
|
|
20
|
+
*
|
|
14
21
|
* @throws Error if called outside a request context
|
|
15
22
|
*/
|
|
16
23
|
getLogger(): Logger;
|
|
@@ -83,4 +90,4 @@ interface Service<TName extends string = string, TInstance = unknown> {
|
|
|
83
90
|
//# sourceMappingURL=types.d.ts.map
|
|
84
91
|
//#endregion
|
|
85
92
|
export { Service, ServiceContext, ServiceRegisterOptions };
|
|
86
|
-
//# sourceMappingURL=types-
|
|
93
|
+
//# sourceMappingURL=types-Bgk-0NOO.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-Bgk-0NOO.d.cts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;;;AAQA;AAqCA;;AAEY,UAvCK,cAAA,CAuCL;EAAiB;AAEL;AA8BxB;;;;;;;AAW+D;;eAtEjD;;;;;;;;;;;;;;;;;;;;;UAyBG,sBAAA;;aAEL;;WAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BO;;;;eAIH;;;;;;;oBAOK,yBAAyB,YAAY,QAAQ"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
2
1
|
import { Logger } from "@geekmidas/logger";
|
|
2
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
|
|
@@ -11,6 +11,13 @@ import { Logger } from "@geekmidas/logger";
|
|
|
11
11
|
interface ServiceContext {
|
|
12
12
|
/**
|
|
13
13
|
* Get the current request's logger.
|
|
14
|
+
*
|
|
15
|
+
* Returns a **request-scoped proxy** that re-resolves the underlying logger
|
|
16
|
+
* from AsyncLocalStorage on every call. This makes it safe for a singleton
|
|
17
|
+
* service to capture the logger once (e.g. during `register()`) and reuse it
|
|
18
|
+
* across requests — each log call routes to the current request's logger
|
|
19
|
+
* instead of freezing the first request's logger.
|
|
20
|
+
*
|
|
14
21
|
* @throws Error if called outside a request context
|
|
15
22
|
*/
|
|
16
23
|
getLogger(): Logger;
|
|
@@ -83,4 +90,4 @@ interface Service<TName extends string = string, TInstance = unknown> {
|
|
|
83
90
|
//# sourceMappingURL=types.d.ts.map
|
|
84
91
|
//#endregion
|
|
85
92
|
export { Service, ServiceContext, ServiceRegisterOptions };
|
|
86
|
-
//# sourceMappingURL=types-
|
|
93
|
+
//# sourceMappingURL=types-BoyOCa57.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-BoyOCa57.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;;;AAQA;AAqCA;;AAEY,UAvCK,cAAA,CAuCL;EAAiB;AAEL;AA8BxB;;;;;;;AAW+D;;eAtEjD;;;;;;;;;;;;;;;;;;;;;UAyBG,sBAAA;;aAEL;;WAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BO;;;;eAIH;;;;;;;oBAOK,yBAAyB,YAAY,QAAQ"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Request-Scoped Logging in Singleton Services
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Services in `@geekmidas/services` are **singletons**. `ServiceDiscovery.register()`
|
|
6
|
+
(and `get()`) instantiates a service **once**, caches the instance in an internal
|
|
7
|
+
`Map`, and returns that same instance for every subsequent request:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// ServiceDiscovery.register()
|
|
11
|
+
if (this.instances.has(name)) {
|
|
12
|
+
return this.instances.get(name); // cached — register() does NOT run again
|
|
13
|
+
}
|
|
14
|
+
const instance = await service.register({ envParser, context: serviceContext });
|
|
15
|
+
this.instances.set(name, instance);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The per-request logger, on the other hand, is **not** a singleton. On every request
|
|
19
|
+
an adaptor builds a fresh child logger with request-specific bindings and stores it
|
|
20
|
+
in `AsyncLocalStorage` via `runWithRequestContext`:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// e.g. HonoEndpointAdaptor
|
|
24
|
+
const logger = endpoint.logger.child({
|
|
25
|
+
requestId, // unique per request
|
|
26
|
+
endpoint, route, host, method, path,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return runWithRequestContext({ logger, requestId, startTime }, async () => {
|
|
30
|
+
const services = await serviceDiscovery.register(endpoint.services);
|
|
31
|
+
// ...handle request...
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### The bug
|
|
36
|
+
|
|
37
|
+
`service.register()` runs **inside the first request's context**. If a service reads
|
|
38
|
+
the logger **at registration time** and stores the concrete reference:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const databaseService = {
|
|
42
|
+
serviceName: 'database' as const,
|
|
43
|
+
register({ context }) {
|
|
44
|
+
const logger = context.getLogger(); // ❌ resolved ONCE, during request #1
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
async query(sql: string) {
|
|
48
|
+
logger.debug({ sql }, 'Executing query'); // always request #1's logger
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
} satisfies Service<'database', Database>;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
…then `logger` is frozen to the **first** request's logger forever, because
|
|
56
|
+
`register()` never runs again. Every later request reuses the cached service
|
|
57
|
+
instance, so its logs carry the **first** request's `requestId` (and any user/session
|
|
58
|
+
bindings).
|
|
59
|
+
|
|
60
|
+
**Symptom:** logs make it look like the user who made the *first* request after a
|
|
61
|
+
cold start is responsible for actions actually performed by *other* users on later
|
|
62
|
+
requests. Request correlation, per-user log filtering, and audit trails are all
|
|
63
|
+
silently wrong.
|
|
64
|
+
|
|
65
|
+
This is an easy mistake to make because `register()` is handed a `context` object,
|
|
66
|
+
and "grab the logger once and reuse it" looks reasonable — but it is incompatible
|
|
67
|
+
with the singleton lifecycle.
|
|
68
|
+
|
|
69
|
+
## Solution
|
|
70
|
+
|
|
71
|
+
`serviceContext.getLogger()` returns a **stable, request-scoped proxy logger**
|
|
72
|
+
instead of the raw logger. The proxy holds no logger of its own — on **every** log
|
|
73
|
+
call it re-resolves the current request's logger from `AsyncLocalStorage`:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
proxy.info('x') → asyncLocalStorage.getStore().logger.info('x') // resolved at call time
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Because resolution happens per call (not at capture time), capturing the logger once
|
|
80
|
+
during `register()` is now **safe**: the single captured reference routes each call
|
|
81
|
+
to whichever request is currently executing.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
register({ context }) {
|
|
85
|
+
const logger = context.getLogger(); // ✅ now safe to capture — it's a live proxy
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
async query(sql: string) {
|
|
89
|
+
logger.debug({ sql }, 'Executing query'); // logs to the CURRENT request
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Child loggers compose correctly too
|
|
96
|
+
|
|
97
|
+
`proxy.child(bindings)` returns **another** proxy carrying the bindings, applied lazily
|
|
98
|
+
on top of the current request's logger at call time:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
register({ context }) {
|
|
102
|
+
// Captured once. `{ svc: 'db' }` is the static part; the per-request bindings
|
|
103
|
+
// (requestId, user, ...) come from whichever base logger is current.
|
|
104
|
+
const logger = context.getLogger().child({ svc: 'db' });
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
async query(sql: string) {
|
|
108
|
+
// request A → loggerA.child({ svc: 'db' }).debug(...)
|
|
109
|
+
// request B → loggerB.child({ svc: 'db' }).debug(...)
|
|
110
|
+
logger.debug({ sql }, 'Executing query');
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Implementation
|
|
117
|
+
|
|
118
|
+
See `createRequestScopedLogger` in
|
|
119
|
+
[`src/context.ts`](../src/context.ts):
|
|
120
|
+
|
|
121
|
+
- `getLogger()` still **throws eagerly** if called with no active request context,
|
|
122
|
+
preserving the "catch bugs early" contract.
|
|
123
|
+
- The returned object is a shared, process-wide proxy. It carries no request state,
|
|
124
|
+
so sharing it across requests is safe — `AsyncLocalStorage` provides correct
|
|
125
|
+
per-async-context isolation, and each resolve/log call is synchronous (no `await`
|
|
126
|
+
between resolving and using the logger), so it is concurrency-safe.
|
|
127
|
+
- Each `child()` call returns a new proxy that remembers its bindings and rebuilds
|
|
128
|
+
the child chain off the current base logger, memoised per underlying logger to
|
|
129
|
+
avoid rebuilding the chain on every log line.
|
|
130
|
+
|
|
131
|
+
## Guidance for service authors
|
|
132
|
+
|
|
133
|
+
- ✅ You **may** capture `context.getLogger()` (or a `.child()` of it) once in
|
|
134
|
+
`register()` and reuse it — it stays correct per request.
|
|
135
|
+
- ✅ You **may** also call `context.getLogger()` inside each method; behaviour is
|
|
136
|
+
identical.
|
|
137
|
+
- ⚠️ Do **not** wrap the proxy in something that snapshots a concrete logger, e.g.
|
|
138
|
+
`const real = someConcreteLogger; ...` outside the proxy. Resolution only stays
|
|
139
|
+
live while you go through the proxy returned by `getLogger()`/`.child()`.
|
|
140
|
+
- ⚠️ Calling a log method outside any request context throws
|
|
141
|
+
(`called outside request context`). Guard background work with
|
|
142
|
+
`serviceContext.hasContext()` if it may run detached from a request.
|
|
143
|
+
|
|
144
|
+
## Tests
|
|
145
|
+
|
|
146
|
+
Regression coverage lives in
|
|
147
|
+
[`src/__tests__/context.spec.ts`](../src/__tests__/context.spec.ts):
|
|
148
|
+
|
|
149
|
+
- `captured-once logger follows each request (singleton service fix)` — a logger
|
|
150
|
+
captured during the first request still logs to the second request's logger.
|
|
151
|
+
- `child loggers also follow the current request` — the same guarantee for
|
|
152
|
+
`.child()` proxies.
|
|
153
|
+
- `should delegate to the current request logger` — basic delegation.
|
|
154
|
+
|
|
155
|
+
## Establishing the context
|
|
156
|
+
|
|
157
|
+
The request context this relies on is established by the adaptors that run your
|
|
158
|
+
code:
|
|
159
|
+
|
|
160
|
+
- **Constructs** (`Endpoint`, `Function`, `Cron`, `Subscriber`) wrap each
|
|
161
|
+
invocation in `runWithRequestContext` automatically.
|
|
162
|
+
- **Standalone Middy handlers** can opt in with the
|
|
163
|
+
`@geekmidas/services/middy` middlewares (`requestContext`, `addServices`,
|
|
164
|
+
`withServices`), which establish the context (and optionally resolve services)
|
|
165
|
+
before the handler runs.
|
|
166
|
+
- **Tests** can use `@geekmidas/testkit`'s `runInRequestContext` /
|
|
167
|
+
`requestContextFixture`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/services",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,6 +23,26 @@
|
|
|
23
23
|
"types": "./dist/context.d.cts",
|
|
24
24
|
"default": "./dist/context.cjs"
|
|
25
25
|
}
|
|
26
|
+
},
|
|
27
|
+
"./trpc": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/trpc.d.mts",
|
|
30
|
+
"default": "./dist/trpc.mjs"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/trpc.d.cts",
|
|
34
|
+
"default": "./dist/trpc.cjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"./middy": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/middy.d.mts",
|
|
40
|
+
"default": "./dist/middy.mjs"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/middy.d.cts",
|
|
44
|
+
"default": "./dist/middy.cjs"
|
|
45
|
+
}
|
|
26
46
|
}
|
|
27
47
|
},
|
|
28
48
|
"repository": {
|
|
@@ -35,6 +55,9 @@
|
|
|
35
55
|
},
|
|
36
56
|
"dependencies": {},
|
|
37
57
|
"peerDependencies": {
|
|
58
|
+
"@middy/core": ">=6.3.1",
|
|
59
|
+
"@trpc/server": ">=11.0.0",
|
|
60
|
+
"@types/aws-lambda": ">=8.10.92",
|
|
38
61
|
"@geekmidas/envkit": "^1.0.7",
|
|
39
62
|
"@geekmidas/logger": "^1.0.2"
|
|
40
63
|
},
|
|
@@ -44,9 +67,22 @@
|
|
|
44
67
|
},
|
|
45
68
|
"@geekmidas/logger": {
|
|
46
69
|
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@middy/core": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@trpc/server": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@types/aws-lambda": {
|
|
78
|
+
"optional": true
|
|
47
79
|
}
|
|
48
80
|
},
|
|
49
81
|
"devDependencies": {
|
|
82
|
+
"@middy/core": "~6.3.2",
|
|
83
|
+
"@trpc/server": "~11.16.0",
|
|
84
|
+
"@types/aws-lambda": "~8.10.147",
|
|
85
|
+
"@types/node": "~24.9.1",
|
|
50
86
|
"@geekmidas/envkit": "^1.0.7",
|
|
51
87
|
"@geekmidas/logger": "^1.0.2"
|
|
52
88
|
},
|