opinionated-machine 6.15.0 → 6.16.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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/DIContext.d.ts +2 -0
- package/dist/lib/DIContext.js +34 -20
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/api-contracts/AbstractApiController.d.ts +19 -0
- package/dist/lib/api-contracts/AbstractApiController.js +18 -0
- package/dist/lib/api-contracts/AbstractApiController.js.map +1 -0
- package/dist/lib/api-contracts/apiHandlerTypes.d.ts +116 -0
- package/dist/lib/api-contracts/apiHandlerTypes.js +2 -0
- package/dist/lib/api-contracts/apiHandlerTypes.js.map +1 -0
- package/dist/lib/api-contracts/apiRouteBuilder.d.ts +9 -0
- package/dist/lib/api-contracts/apiRouteBuilder.js +352 -0
- package/dist/lib/api-contracts/apiRouteBuilder.js.map +1 -0
- package/dist/lib/api-contracts/asApiControllerClass.d.ts +24 -0
- package/dist/lib/api-contracts/asApiControllerClass.js +27 -0
- package/dist/lib/api-contracts/asApiControllerClass.js.map +1 -0
- package/dist/lib/api-contracts/index.d.ts +4 -0
- package/dist/lib/api-contracts/index.js +4 -0
- package/dist/lib/api-contracts/index.js.map +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { AbstractController, type BuildRoutesReturnType } from './lib/AbstractController.js';
|
|
2
2
|
export { AbstractModule, type InferModuleDependencies, type InferPublicModuleDependencies, type MandatoryNameAndRegistrationPair, type PublicDependencies, } from './lib/AbstractModule.js';
|
|
3
3
|
export { AbstractTestContextFactory, type CreateTestContextParams, } from './lib/AbstractTestContextFactory.js';
|
|
4
|
+
export * from './lib/api-contracts/index.js';
|
|
4
5
|
export type { NestedPartial } from './lib/configUtils.js';
|
|
5
6
|
export { type DependencyInjectionOptions, DIContext, type RegisterDependenciesParams, } from './lib/DIContext.js';
|
|
6
7
|
export { ENABLE_ALL, isAnyMessageQueueConsumerEnabled, isEnqueuedJobWorkersEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isPeriodicJobEnabled, resolveJobQueuesEnabled, } from './lib/diConfigUtils.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { AbstractController } from './lib/AbstractController.js';
|
|
2
2
|
export { AbstractModule, } from './lib/AbstractModule.js';
|
|
3
3
|
export { AbstractTestContextFactory, } from './lib/AbstractTestContextFactory.js';
|
|
4
|
+
export * from './lib/api-contracts/index.js';
|
|
4
5
|
export { DIContext, } from './lib/DIContext.js';
|
|
5
6
|
export { ENABLE_ALL, isAnyMessageQueueConsumerEnabled, isEnqueuedJobWorkersEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isPeriodicJobEnabled, resolveJobQueuesEnabled, } from './lib/diConfigUtils.js';
|
|
6
7
|
// Dual-mode (SSE + JSON)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EACL,cAAc,GAKf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,0BAA0B,GAE3B,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EAEL,SAAS,GAEV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,gCAAgC,EAChC,2BAA2B,EAC3B,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,yBAAyB;AACzB,cAAc,yBAAyB,CAAA;AACvC,cAAc,4BAA4B,CAAA;AAC1C,iCAAiC;AACjC,cAAc,uBAAuB,CAAA;AACrC,MAAM;AACN,cAAc,oBAAoB,CAAA;AAClC,wBAAwB;AACxB,cAAc,wBAAwB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EACL,cAAc,GAKf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,0BAA0B,GAE3B,MAAM,qCAAqC,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAE5C,OAAO,EAEL,SAAS,GAEV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,gCAAgC,EAChC,2BAA2B,EAC3B,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,yBAAyB;AACzB,cAAc,yBAAyB,CAAA;AACvC,cAAc,4BAA4B,CAAA;AAC1C,iCAAiC;AACjC,cAAc,uBAAuB,CAAA;AACrC,MAAM;AACN,cAAc,oBAAoB,CAAA;AAClC,wBAAwB;AACxB,cAAc,wBAAwB,CAAA"}
|
package/dist/lib/DIContext.d.ts
CHANGED
|
@@ -31,8 +31,10 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
31
31
|
private readonly controllerResolvers;
|
|
32
32
|
private readonly sseControllerNames;
|
|
33
33
|
private readonly dualModeControllerNames;
|
|
34
|
+
private readonly apiControllerNames;
|
|
34
35
|
private readonly appConfig;
|
|
35
36
|
constructor(diContainer: AwilixContainer<Dependencies>, options: DependencyInjectionOptions, appConfig: Config, awilixManager?: AwilixManager);
|
|
37
|
+
private registerControllers;
|
|
36
38
|
private registerModule;
|
|
37
39
|
registerDependencies(params: RegisterDependenciesParams<Dependencies, Config, ExternalDependencies>, externalDependencies: ExternalDependencies, resolveControllers?: boolean): void;
|
|
38
40
|
registerRoutes(app: FastifyInstance<any, any, any, any>): void;
|
package/dist/lib/DIContext.js
CHANGED
|
@@ -12,6 +12,8 @@ export class DIContext {
|
|
|
12
12
|
sseControllerNames;
|
|
13
13
|
// Dual-mode controller dependency names (resolved from container to preserve singletons)
|
|
14
14
|
dualModeControllerNames;
|
|
15
|
+
// ApiContract controller dependency names (resolved from container to preserve singletons)
|
|
16
|
+
apiControllerNames;
|
|
15
17
|
appConfig;
|
|
16
18
|
constructor(diContainer, options, appConfig, awilixManager) {
|
|
17
19
|
this.options = options;
|
|
@@ -29,6 +31,31 @@ export class DIContext {
|
|
|
29
31
|
this.controllerResolvers = [];
|
|
30
32
|
this.sseControllerNames = [];
|
|
31
33
|
this.dualModeControllerNames = [];
|
|
34
|
+
this.apiControllerNames = [];
|
|
35
|
+
}
|
|
36
|
+
registerControllers(
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: controller resolver properties are duck-typed
|
|
38
|
+
controllers, targetDiConfig) {
|
|
39
|
+
for (const [name, resolver] of Object.entries(controllers)) {
|
|
40
|
+
if (resolver.isDualModeController) {
|
|
41
|
+
this.dualModeControllerNames.push(name);
|
|
42
|
+
// @ts-expect-error we can't really ensure type-safety here
|
|
43
|
+
targetDiConfig[name] = resolver;
|
|
44
|
+
}
|
|
45
|
+
else if (resolver.isSSEController) {
|
|
46
|
+
this.sseControllerNames.push(name);
|
|
47
|
+
// @ts-expect-error we can't really ensure type-safety here
|
|
48
|
+
targetDiConfig[name] = resolver;
|
|
49
|
+
}
|
|
50
|
+
else if (resolver.isApiController) {
|
|
51
|
+
this.apiControllerNames.push(name);
|
|
52
|
+
// @ts-expect-error we can't really ensure type-safety here
|
|
53
|
+
targetDiConfig[name] = resolver;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.controllerResolvers.push(resolver);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
32
59
|
}
|
|
33
60
|
registerModule(module, targetDiConfig, externalDependencies, resolveControllers, isPrimaryModule) {
|
|
34
61
|
const resolvedDIConfig = module.resolveDependencies(this.options, externalDependencies);
|
|
@@ -41,26 +68,7 @@ export class DIContext {
|
|
|
41
68
|
}
|
|
42
69
|
if (isPrimaryModule && resolveControllers) {
|
|
43
70
|
const controllers = module.resolveControllers(this.options);
|
|
44
|
-
|
|
45
|
-
// @ts-expect-error isDualModeController is a custom property on the resolver
|
|
46
|
-
if (resolver.isDualModeController) {
|
|
47
|
-
// Dual-mode controller: register in DI container and track name for route registration
|
|
48
|
-
this.dualModeControllerNames.push(name);
|
|
49
|
-
// @ts-expect-error we can't really ensure type-safety here
|
|
50
|
-
targetDiConfig[name] = resolver;
|
|
51
|
-
// @ts-expect-error isSSEController is a custom property on the resolver
|
|
52
|
-
}
|
|
53
|
-
else if (resolver.isSSEController) {
|
|
54
|
-
// SSE controller: register in DI container and track name for route registration
|
|
55
|
-
this.sseControllerNames.push(name);
|
|
56
|
-
// @ts-expect-error we can't really ensure type-safety here
|
|
57
|
-
targetDiConfig[name] = resolver;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
// REST controller: add resolver for route registration
|
|
61
|
-
this.controllerResolvers.push(resolver);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
71
|
+
this.registerControllers(controllers, targetDiConfig);
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
registerDependencies(params, externalDependencies, resolveControllers = true) {
|
|
@@ -101,6 +109,12 @@ export class DIContext {
|
|
|
101
109
|
app.route(route);
|
|
102
110
|
}
|
|
103
111
|
}
|
|
112
|
+
for (const controllerName of this.apiControllerNames) {
|
|
113
|
+
const controller = this.diContainer.resolve(controllerName);
|
|
114
|
+
for (const route of controller.routes) {
|
|
115
|
+
app.route(route);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
104
118
|
}
|
|
105
119
|
/**
|
|
106
120
|
* Check if any SSE controllers are registered.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAIpC,OAAO,EAAE,iCAAiC,EAAsB,MAAM,kBAAkB,CAAA;AAGxF,OAAO,EACL,iBAAiB,GAGlB,MAAM,mBAAmB,CAAA;AAwB1B,MAAM,OAAO,SAAS;IAKH,OAAO,CAA4B;IACpC,aAAa,CAAe;IAC5B,WAAW,CAA+B;IAC1D,8EAA8E;IAC7D,mBAAmB,CAAiB;IACrD,mFAAmF;IAClE,kBAAkB,CAAU;IAC7C,yFAAyF;IACxE,uBAAuB,CAAU;IAClD,2FAA2F;IAC1E,kBAAkB,CAAU;IAC5B,SAAS,CAAQ;IAElC,YACE,WAA0C,EAC1C,OAAmC,EACnC,SAAiB,EACjB,aAA6B;QAE7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,aAAa;YAChB,aAAa;gBACb,IAAI,aAAa,CAAC;oBAChB,YAAY,EAAE,IAAI;oBAClB,SAAS,EAAE,IAAI;oBACf,WAAW;oBACX,WAAW,EAAE,IAAI;oBACjB,qBAAqB,EAAE,IAAI;iBAC5B,CAAC,CAAA;QACJ,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAA;QAC7B,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAA;QAC5B,IAAI,CAAC,uBAAuB,GAAG,EAAE,CAAA;QACjC,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAA;IAC9B,CAAC;IAEO,mBAAmB;IACzB,4FAA4F;IAC5F,WAAgC,EAChC,cAAqD;QAErD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAC3D,IAAI,QAAQ,CAAC,oBAAoB,EAAE,CAAC;gBAClC,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACvC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAA6B,CAAC,CAAA;YAC9D,CAAC;QACH,CAAC;IACH,CAAC;IAEO,cAAc,CACpB,MAAqD,EACrD,cAAqD,EACrD,oBAA0C,EAC1C,kBAA2B,EAC3B,eAAwB;QAExB,MAAM,gBAAgB,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAA;QAEvF,KAAK,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;YACnC,2DAA2D;YAC3D,IAAI,eAAe,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBACpD,2DAA2D;gBAC3D,cAAc,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA;YAC7C,CAAC;QACH,CAAC;QAED,IAAI,eAAe,IAAI,kBAAkB,EAAE,CAAC;YAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YAE3D,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED,oBAAoB,CAClB,MAA8E,EAC9E,oBAA0C,EAC1C,kBAAkB,GAAG,IAAI;QAEzB,MAAM,eAAe,GAAG,iCAAiC,CACvD,IAAI,CAAC,SAAS,EACd,MAAM,CAAC,kBAAkB,IAAI,QAAQ,EACrC,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,mBAAmB,IAAI,EAAE,CACjC,CAAA;QACD,MAAM,cAAc,GAA0C,EAAE,CAAA;QAEhE,KAAK,MAAM,aAAa,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC3C,IAAI,CAAC,cAAc,CACjB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,IAAI,CACL,CAAA;QACH,CAAC;QAED,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC5B,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;gBACtD,IAAI,CAAC,cAAc,CACjB,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,CACN,CAAA;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAmD,CAAC,CAAA;QAE9E,8BAA8B;QAC9B,0CAA0C;QAC1C,KAAK,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC;YAChF,MAAM,eAAe,GAAG,EAAE,GAAI,gBAAsC,EAAE,CAAA;YAEtE,2CAA2C;YAC3C,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,aAAa,CAAC,CAAA;YACxE,mBAAmB;YACnB,IAAI,eAAe,CAAC,QAAQ,KAAK,gBAAgB,CAAC,QAAQ,EAAE,CAAC;gBAC3D,mBAAmB;gBACnB,eAAe,CAAC,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAA;YACtD,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC;IAED,4FAA4F;IAC5F,cAAc,CAAC,GAAwC;QACrD,KAAK,MAAM,kBAAkB,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC1D,wEAAwE;YACxE,MAAM,UAAU,GAA4B,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACxF,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,EAAE,CAAA;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1C,mFAAmF;gBACnF,2EAA2E;gBAC3E,GAAG,CAAC,KAAK,CAAC,KAAkB,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACrD,MAAM,UAAU,GAA0B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAElF,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;gBACtC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,iBAAiB;QACf,OAAO,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,CAAA;IAC3C,CAAC;IAED;;;OAGG;IACH,sBAAsB;QACpB,OAAO,IAAI,CAAC,uBAAuB,CAAC,MAAM,GAAG,CAAC,CAAA;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB;IACf,iFAAiF;IACjF,GAAwC,EACxC,OAAkC;QAElC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC9B,OAAM;QACR,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACrD,uDAAuD;YACvD,MAAM,aAAa,GACjB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAC1C,MAAM,SAAS,GAAG,aAAa,CAAC,cAAc,EAAE,CAAA;YAEhD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;gBACnD,MAAM,KAAK,GAAG,iBAAiB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;gBAC3D,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBACzC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,sBAAsB;IACpB,iFAAiF;IACjF,GAAwC,EACxC,OAAuC;QAEvC,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,CAAC;YACnC,OAAM;QACR,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAC1D,uDAAuD;YACvD,MAAM,kBAAkB,GAEpB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAC5C,MAAM,cAAc,GAAG,kBAAkB,CAAC,mBAAmB,EAAE,CAAA;YAE/D,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;gBACxD,MAAM,KAAK,GAAG,iBAAiB,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAA;gBAChE,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBAC9C,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAEO,yBAAyB,CAC/B,KAAmB,EACnB,OAAuC;QAEvC,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;YACxB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClD,CAAC;QACD,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAC/C,CAAC;QACD,0EAA0E;QAC1E,IAAI,OAAO,EAAE,iBAAiB,KAAK,SAAS,IAAI,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;YAClF,2EAA2E;YAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;YAChE,eAAe,CAAC,MAAM,GAAG,KAAK,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,EAAE;gBAC3D,GAAG,EAAE;oBACH,GAAG,CAAC,OAAO,CAAC,iBAAiB,KAAK,SAAS,IAAI;wBAC7C,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;qBAC7C,CAAC;oBACF,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;iBAC5E;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,oBAAoB,CAAC,KAAmB,EAAE,OAAkC;QAClF,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;YACxB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClD,CAAC;QACD,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAC/C,CAAC;QACD,6DAA6D;QAC7D,IAAI,OAAO,EAAE,iBAAiB,KAAK,SAAS,IAAI,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;YAClF,2EAA2E;YAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;YAChE,eAAe,CAAC,MAAM,GAAG,KAAK,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,EAAE;gBAC3D,GAAG,EAAE;oBACH,GAAG,CAAC,OAAO,CAAC,iBAAiB,KAAK,SAAS,IAAI;wBAC7C,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;qBAC7C,CAAC;oBACF,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;iBAC5E;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,gBAAgB,CACtB,KAAmB,EACnB,gBAA4C;QAE5C,MAAM,kBAAkB,GAAG,KAAK,CAAC,UAAU,CAAA;QAC3C,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACxB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAA;YACnC,OAAM;QACR,CAAC;QACD,2EAA2E;QAC3E,MAAM,QAAQ,GAAU,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC;YACvD,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAA;QACxB,2EAA2E;QAC3E,MAAM,cAAc,GAAU,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;YAC3D,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAA;QACtB,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,cAAc,EAAE,GAAG,QAAQ,CAAC,CAAA;IACrD,CAAC;IAEO,cAAc,CACpB,KAAmB,EACnB,SAA6D;QAE7D,2EAA2E;QAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;QAChE,eAAe,CAAC,MAAM,GAAG;YACvB,GAAG,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,CAAC;YACjC,SAAS;SACV,CAAA;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,CAAA;QACzC,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAA;IACxC,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RouteOptions } from 'fastify';
|
|
2
|
+
/**
|
|
3
|
+
* Abstract base class for controllers that use the `ApiContract` API.
|
|
4
|
+
*
|
|
5
|
+
* Concrete controllers declare a `routes` property built with `buildApiRoute()`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* class UserController extends AbstractApiController {
|
|
10
|
+
* readonly routes = [
|
|
11
|
+
* buildApiRoute(getUser, async (req) => ({ status: 200, body: { id: req.params.id } })),
|
|
12
|
+
* buildApiRoute(streamUpdates, async (_req, sse) => { sse.start('keepAlive') }),
|
|
13
|
+
* ]
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class AbstractApiController {
|
|
18
|
+
abstract readonly routes: RouteOptions[];
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for controllers that use the `ApiContract` API.
|
|
3
|
+
*
|
|
4
|
+
* Concrete controllers declare a `routes` property built with `buildApiRoute()`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* class UserController extends AbstractApiController {
|
|
9
|
+
* readonly routes = [
|
|
10
|
+
* buildApiRoute(getUser, async (req) => ({ status: 200, body: { id: req.params.id } })),
|
|
11
|
+
* buildApiRoute(streamUpdates, async (_req, sse) => { sse.start('keepAlive') }),
|
|
12
|
+
* ]
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class AbstractApiController {
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=AbstractApiController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AbstractApiController.js","sourceRoot":"","sources":["../../../lib/api-contracts/AbstractApiController.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAgB,qBAAqB;CAE1C"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ApiContract, ContractNoBody, ContractResponseMode, InferSseSuccessResponses, PayloadApiContract } from '@lokalise/api-contracts';
|
|
2
|
+
import type { FastifyRequest, RouteOptions } from 'fastify';
|
|
3
|
+
import type { z } from 'zod/v4';
|
|
4
|
+
import type { DualModeType } from '../dualmode/dualModeTypes.ts';
|
|
5
|
+
import type { FastifySSERouteOptions, SSEContext, SSEHandlerResult, SyncModeReply } from '../routes/fastifyRouteTypes.ts';
|
|
6
|
+
type NonSseBodyEntry<T> = T extends undefined ? never : T extends {
|
|
7
|
+
_tag: 'SseResponse';
|
|
8
|
+
} ? never : T extends {
|
|
9
|
+
_tag: 'BlobResponse';
|
|
10
|
+
} ? Blob : T extends {
|
|
11
|
+
_tag: 'TextResponse';
|
|
12
|
+
} ? string : T extends {
|
|
13
|
+
_tag: 'AnyOfResponses';
|
|
14
|
+
responses: Array<infer R>;
|
|
15
|
+
} ? NonSseBodyEntry<R> : T extends z.ZodType ? z.output<T> : undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Discriminated union of `{ status, body }` pairs for all non-SSE responses in a contract.
|
|
18
|
+
*
|
|
19
|
+
* Allows non-SSE handlers to return a specific status code and body together without
|
|
20
|
+
* calling `reply.code()` separately.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* async (request) => {
|
|
25
|
+
* if (!valid) return { status: 400, body: { error: 'Bad Request' } }
|
|
26
|
+
* return { id: request.params.id }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export type InferApiStatusResponse<Contract extends ApiContract> = {
|
|
31
|
+
[K in keyof Contract['responsesByStatusCode']]: NonSseBodyEntry<Contract['responsesByStatusCode'][K]> extends never ? never : {
|
|
32
|
+
status: K;
|
|
33
|
+
body: NonSseBodyEntry<Contract['responsesByStatusCode'][K]>;
|
|
34
|
+
};
|
|
35
|
+
}[keyof Contract['responsesByStatusCode']];
|
|
36
|
+
type InferOptSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodType ? z.output<NonNullable<T>> : Fallback;
|
|
37
|
+
type InferApiBodyType<Contract extends ApiContract> = Contract extends PayloadApiContract ? Contract['requestBodySchema'] extends typeof ContractNoBody ? undefined : NonNullable<Contract['requestBodySchema']> extends z.ZodType ? z.output<NonNullable<Contract['requestBodySchema']>> : undefined : undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Infer the FastifyRequest type from an ApiContract.
|
|
40
|
+
*
|
|
41
|
+
* Provides properly typed params, querystring, headers, and body.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const handler = async (request: InferApiRequest<typeof myContract>) => {
|
|
46
|
+
* request.params.userId // typed
|
|
47
|
+
* request.body.name // typed
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export type InferApiRequest<Contract extends ApiContract> = FastifyRequest<{
|
|
52
|
+
Params: InferOptSchema<Contract['requestPathParamsSchema']>;
|
|
53
|
+
Querystring: InferOptSchema<Contract['requestQuerySchema']>;
|
|
54
|
+
Headers: InferOptSchema<Contract['requestHeaderSchema']>;
|
|
55
|
+
Body: InferApiBodyType<Contract>;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Handler for non-SSE responses from an ApiContract.
|
|
59
|
+
*
|
|
60
|
+
* Always return `{ status, body }` — the framework validates the body against the
|
|
61
|
+
* contract's schema for that status code and sends it.
|
|
62
|
+
*
|
|
63
|
+
* Use `reply.header()` to set response headers when needed.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* async (request) => ({ status: 200, body: { id: request.params.userId } })
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example With multiple status codes
|
|
71
|
+
* ```typescript
|
|
72
|
+
* async (request) => {
|
|
73
|
+
* if (!valid) return { status: 400, body: { error: 'Bad Request' } }
|
|
74
|
+
* return { status: 200, body: { id: request.params.userId } }
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export type ApiNonSseHandler<Contract extends ApiContract> = (request: InferApiRequest<Contract>, reply: SyncModeReply) => InferApiStatusResponse<Contract> | Promise<InferApiStatusResponse<Contract>>;
|
|
79
|
+
/**
|
|
80
|
+
* Handler for SSE responses from an ApiContract.
|
|
81
|
+
*
|
|
82
|
+
* Call `sse.start(mode)` to begin streaming or `sse.respond(code, body)` for
|
|
83
|
+
* early HTTP returns before streaming starts.
|
|
84
|
+
*/
|
|
85
|
+
export type ApiSseHandler<Contract extends ApiContract> = (request: InferApiRequest<Contract>, sse: SSEContext<InferSseSuccessResponses<Contract['responsesByStatusCode']>>) => SSEHandlerResult | Promise<SSEHandlerResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Infer the handler shape based on the contract's response mode:
|
|
88
|
+
* - `'non-sse'` — bare `ApiNonSseHandler` function
|
|
89
|
+
* - `'sse'` — bare `ApiSseHandler` function
|
|
90
|
+
* - `'dual'` — `{ nonSse, sse }` object, branched by `Accept` header
|
|
91
|
+
*/
|
|
92
|
+
export type InferApiHandler<Contract extends ApiContract> = [
|
|
93
|
+
ContractResponseMode<Contract['responsesByStatusCode']>
|
|
94
|
+
] extends ['dual'] ? {
|
|
95
|
+
nonSse: ApiNonSseHandler<Contract>;
|
|
96
|
+
sse: ApiSseHandler<Contract>;
|
|
97
|
+
} : [ContractResponseMode<Contract['responsesByStatusCode']>] extends ['sse'] ? ApiSseHandler<Contract> : ApiNonSseHandler<Contract>;
|
|
98
|
+
/**
|
|
99
|
+
* Options for configuring an ApiContract route.
|
|
100
|
+
*
|
|
101
|
+
* Extends Fastify's `RouteOptions` minus the fields the contract provides
|
|
102
|
+
* (`method`, `url`, `schema`, `handler`, `sse`), so any Fastify hook or config
|
|
103
|
+
* (`preHandler`, `onRequest`, `config`, `bodyLimit`, etc.) can be passed directly.
|
|
104
|
+
*
|
|
105
|
+
* SSE lifecycle options (`onConnect`, `onClose`, `onReconnect`) are only
|
|
106
|
+
* relevant for SSE and dual-mode contracts and are ignored for non-SSE routes.
|
|
107
|
+
*/
|
|
108
|
+
export type ApiRouteOptions = Omit<RouteOptions, 'method' | 'url' | 'schema' | 'handler' | 'sse'> & Omit<FastifySSERouteOptions, 'preHandler'> & {
|
|
109
|
+
/**
|
|
110
|
+
* Default response mode for dual-mode routes when the `Accept` header
|
|
111
|
+
* does not express a preference.
|
|
112
|
+
* @default 'json'
|
|
113
|
+
*/
|
|
114
|
+
defaultMode?: DualModeType;
|
|
115
|
+
};
|
|
116
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiHandlerTypes.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiHandlerTypes.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ApiContract } from '@lokalise/api-contracts';
|
|
2
|
+
import type { RouteOptions } from 'fastify';
|
|
3
|
+
import type { ApiRouteOptions, InferApiHandler } from './apiHandlerTypes.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
|
|
6
|
+
*
|
|
7
|
+
* @returns Fastify `RouteOptions` ready to pass to `app.route()`
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildApiRoute<Contract extends ApiContract>(contract: Contract, handler: InferApiHandler<Contract>, options?: ApiRouteOptions): RouteOptions;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ContractNoBody, getSseSchemaByEventName, hasAnySuccessSseResponse, isAnyOfResponses, isBlobResponse, isSseResponse, isTextResponse, mapApiContractToPath, SUCCESSFUL_HTTP_STATUS_CODES, } from '@lokalise/api-contracts';
|
|
3
|
+
import { InternalError } from '@lokalise/node-core';
|
|
4
|
+
import { determineMode, hasHttpStatusCode, isErrorLike } from "../routes/fastifyRouteUtils.js";
|
|
5
|
+
function isSuccessResponseDual(value) {
|
|
6
|
+
if (value === ContractNoBody || isTextResponse(value) || isBlobResponse(value))
|
|
7
|
+
return true;
|
|
8
|
+
if (!isSseResponse(value) && !isAnyOfResponses(value))
|
|
9
|
+
return true;
|
|
10
|
+
if (isAnyOfResponses(value)) {
|
|
11
|
+
return value.responses.some((response) => !isSseResponse(response));
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function getContractResponseMode(contract) {
|
|
16
|
+
if (!hasAnySuccessSseResponse(contract))
|
|
17
|
+
return 'non-sse';
|
|
18
|
+
for (const code of SUCCESSFUL_HTTP_STATUS_CODES) {
|
|
19
|
+
const value = contract.responsesByStatusCode[code];
|
|
20
|
+
if (value && isSuccessResponseDual(value))
|
|
21
|
+
return 'dual';
|
|
22
|
+
}
|
|
23
|
+
return 'sse';
|
|
24
|
+
}
|
|
25
|
+
function buildSSERouteConfig(options) {
|
|
26
|
+
if (!options?.serializer && options?.heartbeatInterval === undefined)
|
|
27
|
+
return true;
|
|
28
|
+
const sseConfig = {};
|
|
29
|
+
if (options.serializer)
|
|
30
|
+
sseConfig.serializer = options.serializer;
|
|
31
|
+
if (options.heartbeatInterval !== undefined)
|
|
32
|
+
sseConfig.heartbeatInterval = options.heartbeatInterval;
|
|
33
|
+
return sseConfig;
|
|
34
|
+
}
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Internal Helpers — Sync Route
|
|
37
|
+
// ============================================================================
|
|
38
|
+
function getSchemaForStatusCode(contract, status) {
|
|
39
|
+
const entry = contract.responsesByStatusCode[status];
|
|
40
|
+
if (!entry ||
|
|
41
|
+
entry === ContractNoBody ||
|
|
42
|
+
isSseResponse(entry) ||
|
|
43
|
+
isTextResponse(entry) ||
|
|
44
|
+
isBlobResponse(entry)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (isAnyOfResponses(entry)) {
|
|
48
|
+
for (const anyResponse of entry.responses) {
|
|
49
|
+
if (isSseResponse(anyResponse) ||
|
|
50
|
+
isTextResponse(anyResponse) ||
|
|
51
|
+
isBlobResponse(anyResponse)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return anyResponse;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function validateApiResponseHeaders(contract, reply) {
|
|
63
|
+
const schema = contract.responseHeaderSchema;
|
|
64
|
+
if (!schema) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const result = schema.safeParse(reply.getHeaders());
|
|
68
|
+
if (!result.success) {
|
|
69
|
+
throw new InternalError({
|
|
70
|
+
message: 'Internal Server Error',
|
|
71
|
+
errorCode: 'RESPONSE_HEADERS_VALIDATION_FAILED',
|
|
72
|
+
details: { validationError: result.error.message },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function handleApiSyncRoute(contract,
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: Handler types are validated by InferApiHandler at the call site
|
|
78
|
+
handler,
|
|
79
|
+
// biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
|
|
80
|
+
request, reply) {
|
|
81
|
+
const { status, body } = await handler(request, reply);
|
|
82
|
+
if (reply.sent) {
|
|
83
|
+
request.log.warn({
|
|
84
|
+
msg: 'Sync handler sent response directly, bypassing response validation',
|
|
85
|
+
tag: 'response_sent_directly',
|
|
86
|
+
method: request.method,
|
|
87
|
+
url: request.url,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const schema = getSchemaForStatusCode(contract, status);
|
|
93
|
+
if (schema) {
|
|
94
|
+
const result = schema.safeParse(body);
|
|
95
|
+
if (!result.success) {
|
|
96
|
+
throw new InternalError({
|
|
97
|
+
message: 'Internal Server Error',
|
|
98
|
+
errorCode: 'RESPONSE_VALIDATION_FAILED',
|
|
99
|
+
details: { validationError: result.error.message },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
reply.code(500);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
validateApiResponseHeaders(contract, reply);
|
|
109
|
+
if (!reply.hasHeader('content-type')) {
|
|
110
|
+
reply.type('application/json');
|
|
111
|
+
}
|
|
112
|
+
return reply.code(status).send(body);
|
|
113
|
+
}
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Internal Helpers — SSE Route (no controller, uses reply.sse directly)
|
|
116
|
+
// ============================================================================
|
|
117
|
+
function buildApiSSEContext(
|
|
118
|
+
// biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
|
|
119
|
+
request, reply, eventSchemas, options) {
|
|
120
|
+
let started = false;
|
|
121
|
+
let responseData;
|
|
122
|
+
const sseReply = reply;
|
|
123
|
+
const sseContext = {
|
|
124
|
+
start: (mode, startOptions) => {
|
|
125
|
+
started = true;
|
|
126
|
+
if (mode === 'keepAlive') {
|
|
127
|
+
sseReply.sse.keepAlive();
|
|
128
|
+
}
|
|
129
|
+
// sendHeaders() calls writeHead(200) but only queues headers in the buffer.
|
|
130
|
+
// flushHeaders() forces them onto the wire so the client's fetch() returns.
|
|
131
|
+
sseReply.sse.sendHeaders();
|
|
132
|
+
reply.raw.flushHeaders();
|
|
133
|
+
const connectionId = randomUUID();
|
|
134
|
+
const send = async (eventName, data, sendOptions) => {
|
|
135
|
+
const schema = eventSchemas[eventName];
|
|
136
|
+
if (schema) {
|
|
137
|
+
const result = schema.safeParse(data);
|
|
138
|
+
if (!result.success) {
|
|
139
|
+
throw new InternalError({
|
|
140
|
+
message: `SSE event validation failed for event "${eventName}": ${result.error.message}`,
|
|
141
|
+
errorCode: 'RESPONSE_VALIDATION_FAILED',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await sseReply.sse.send({
|
|
147
|
+
event: eventName,
|
|
148
|
+
data,
|
|
149
|
+
id: sendOptions?.id,
|
|
150
|
+
retry: sendOptions?.retry,
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const session = {
|
|
159
|
+
id: connectionId,
|
|
160
|
+
request,
|
|
161
|
+
reply,
|
|
162
|
+
context: (startOptions?.context ?? {}),
|
|
163
|
+
connectedAt: new Date(),
|
|
164
|
+
// biome-ignore lint/suspicious/noExplicitAny: SSEEventSender generic is satisfied at handler call site
|
|
165
|
+
send: send,
|
|
166
|
+
isConnected: () => sseReply.sse.isConnected,
|
|
167
|
+
getStream: () => sseReply.sse.stream(),
|
|
168
|
+
sendStream: async (messages) => {
|
|
169
|
+
for await (const message of messages) {
|
|
170
|
+
await send(message.event, message.data, { id: message.id, retry: message.retry });
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
rooms: { join: () => { }, leave: () => { } },
|
|
174
|
+
eventSchemas,
|
|
175
|
+
};
|
|
176
|
+
if (options?.onConnect) {
|
|
177
|
+
void Promise.resolve(options.onConnect(session)).catch(() => { });
|
|
178
|
+
}
|
|
179
|
+
if (options?.onClose) {
|
|
180
|
+
const onClose = options.onClose;
|
|
181
|
+
sseReply.sse.onClose(() => {
|
|
182
|
+
void Promise.resolve(onClose(session, 'client')).catch(() => { });
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (options?.onReconnect && sseReply.sse.lastEventId) {
|
|
186
|
+
const onReconnect = options.onReconnect;
|
|
187
|
+
const lastEventId = sseReply.sse.lastEventId;
|
|
188
|
+
void sseReply.sse.replay(async () => {
|
|
189
|
+
const replay = await onReconnect(session, lastEventId);
|
|
190
|
+
if (replay) {
|
|
191
|
+
for await (const msg of replay) {
|
|
192
|
+
await sseReply.sse.send(msg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return session;
|
|
198
|
+
},
|
|
199
|
+
respond: ((code, body) => {
|
|
200
|
+
if (started) {
|
|
201
|
+
throw new Error('Cannot call sse.respond() after sse.start() — the SSE stream is already open.');
|
|
202
|
+
}
|
|
203
|
+
responseData = { code, body };
|
|
204
|
+
return { _type: 'respond', code, body };
|
|
205
|
+
// biome-ignore lint/suspicious/noExplicitAny: respond typing is enforced by contract at call site
|
|
206
|
+
}),
|
|
207
|
+
sendHeaders: () => {
|
|
208
|
+
sseReply.sse.sendHeaders();
|
|
209
|
+
},
|
|
210
|
+
reply,
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
sseContext,
|
|
214
|
+
isStarted: () => started,
|
|
215
|
+
hasResponse: () => responseData !== undefined,
|
|
216
|
+
getResponseData: () => responseData,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Core SSE handler coordinates context, error handling, and lifecycle
|
|
220
|
+
async function handleApiSseRoute(
|
|
221
|
+
// biome-ignore lint/suspicious/noExplicitAny: SSE handler types are validated by InferApiHandler at call site
|
|
222
|
+
sseHandler, eventSchemas, options,
|
|
223
|
+
// biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
|
|
224
|
+
request, reply) {
|
|
225
|
+
const { sseContext, isStarted, hasResponse, getResponseData } = buildApiSSEContext(request, reply, eventSchemas, options);
|
|
226
|
+
try {
|
|
227
|
+
await sseHandler(request, sseContext);
|
|
228
|
+
if (!isStarted() && !hasResponse()) {
|
|
229
|
+
throw new Error('SSE handler must either send a response (sse.respond()) ' +
|
|
230
|
+
'or start streaming (sse.start()). Handler returned without doing either.');
|
|
231
|
+
}
|
|
232
|
+
const responseData = getResponseData();
|
|
233
|
+
if (responseData) {
|
|
234
|
+
// Early HTTP response (sse.respond() was called before streaming)
|
|
235
|
+
reply.removeHeader('cache-control');
|
|
236
|
+
reply.removeHeader('x-accel-buffering');
|
|
237
|
+
reply.type('application/json').code(responseData.code).send(responseData.body);
|
|
238
|
+
}
|
|
239
|
+
// If started, @fastify/sse manages the rest of the connection lifecycle
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
if (isStarted()) {
|
|
243
|
+
// Headers already sent — can't change status code; try to send error event
|
|
244
|
+
const sseReply = reply;
|
|
245
|
+
if (sseReply.sse.isConnected) {
|
|
246
|
+
try {
|
|
247
|
+
await sseReply.sse.send({
|
|
248
|
+
event: 'error',
|
|
249
|
+
data: { message: isErrorLike(err) ? err.message : 'Internal Server Error' },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Ignore send failures during error handling
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
// Streaming not started — send HTTP error response
|
|
259
|
+
const message = isErrorLike(err) ? err.message : 'Internal Server Error';
|
|
260
|
+
const statusCode = hasHttpStatusCode(err) ? err.httpStatusCode : 500;
|
|
261
|
+
const statusText = statusCode >= 500 ? 'Internal Server Error' : 'Error';
|
|
262
|
+
reply.code(statusCode).type('application/json').send({ statusCode, error: statusText, message });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Internal Helpers — Schema
|
|
267
|
+
// ============================================================================
|
|
268
|
+
function buildResponseSchemas(contract) {
|
|
269
|
+
return Object.keys(contract.responsesByStatusCode).reduce((acc, statusCode) => {
|
|
270
|
+
const schema = getSchemaForStatusCode(contract, Number(statusCode));
|
|
271
|
+
if (schema) {
|
|
272
|
+
acc[Number(statusCode)] = schema;
|
|
273
|
+
}
|
|
274
|
+
return acc;
|
|
275
|
+
}, {});
|
|
276
|
+
}
|
|
277
|
+
function buildBaseSchema(contract) {
|
|
278
|
+
const schema = {};
|
|
279
|
+
if (contract.requestPathParamsSchema)
|
|
280
|
+
schema.params = contract.requestPathParamsSchema;
|
|
281
|
+
if (contract.requestQuerySchema)
|
|
282
|
+
schema.querystring = contract.requestQuerySchema;
|
|
283
|
+
if (contract.requestHeaderSchema)
|
|
284
|
+
schema.headers = contract.requestHeaderSchema;
|
|
285
|
+
if (contract.requestBodySchema !== undefined && contract.requestBodySchema !== ContractNoBody) {
|
|
286
|
+
schema.body = contract.requestBodySchema;
|
|
287
|
+
}
|
|
288
|
+
schema.response = buildResponseSchemas(contract);
|
|
289
|
+
return schema;
|
|
290
|
+
}
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Public API
|
|
293
|
+
// ============================================================================
|
|
294
|
+
/**
|
|
295
|
+
* Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
|
|
296
|
+
*
|
|
297
|
+
* @returns Fastify `RouteOptions` ready to pass to `app.route()`
|
|
298
|
+
*/
|
|
299
|
+
export function buildApiRoute(contract, handler, options) {
|
|
300
|
+
// Separate SSE-specific options (not in Fastify RouteOptions) from passthrough options
|
|
301
|
+
const { defaultMode, contractMetadataToRouteMapper, serializer: _serializer, heartbeatInterval: _heartbeatInterval, onConnect: _onConnect, onClose: _onClose, onReconnect: _onReconnect, logger: _logger, ...fastifyOptions } = options ?? {};
|
|
302
|
+
const url = mapApiContractToPath(contract);
|
|
303
|
+
const mode = getContractResponseMode(contract);
|
|
304
|
+
const eventSchemas = getSseSchemaByEventName(contract) ?? {};
|
|
305
|
+
const baseSchema = buildBaseSchema(contract);
|
|
306
|
+
const contractMetadata = contractMetadataToRouteMapper?.(contract.metadata) ?? {};
|
|
307
|
+
if (mode === 'non-sse') {
|
|
308
|
+
// biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
|
|
309
|
+
const syncHandler = handler;
|
|
310
|
+
return {
|
|
311
|
+
...fastifyOptions,
|
|
312
|
+
...contractMetadata,
|
|
313
|
+
method: contract.method,
|
|
314
|
+
url,
|
|
315
|
+
schema: baseSchema,
|
|
316
|
+
handler: async (request, reply) => handleApiSyncRoute(contract, syncHandler, request, reply),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (mode === 'dual') {
|
|
320
|
+
const resolvedDefaultMode = defaultMode ?? 'json';
|
|
321
|
+
// biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
|
|
322
|
+
const dualHandlers = handler;
|
|
323
|
+
return {
|
|
324
|
+
...fastifyOptions,
|
|
325
|
+
...contractMetadata,
|
|
326
|
+
method: contract.method,
|
|
327
|
+
url,
|
|
328
|
+
sse: buildSSERouteConfig(options),
|
|
329
|
+
schema: baseSchema,
|
|
330
|
+
handler: (request, reply) => {
|
|
331
|
+
const responseMode = determineMode(request.headers.accept, resolvedDefaultMode);
|
|
332
|
+
if (responseMode === 'json') {
|
|
333
|
+
return handleApiSyncRoute(contract, dualHandlers.nonSse, request, reply);
|
|
334
|
+
}
|
|
335
|
+
return handleApiSseRoute(dualHandlers.sse, eventSchemas, options, request, reply);
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// SSE-only
|
|
340
|
+
// biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
|
|
341
|
+
const sseHandler = handler;
|
|
342
|
+
return {
|
|
343
|
+
...fastifyOptions,
|
|
344
|
+
...contractMetadata,
|
|
345
|
+
method: contract.method,
|
|
346
|
+
url,
|
|
347
|
+
sse: buildSSERouteConfig(options),
|
|
348
|
+
schema: baseSchema,
|
|
349
|
+
handler: async (request, reply) => handleApiSseRoute(sseHandler, eventSchemas, options, request, reply),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=apiRouteBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiRouteBuilder.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiRouteBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BuildResolver, BuildResolverOptions, DisposableResolver } from 'awilix';
|
|
2
|
+
declare module 'awilix' {
|
|
3
|
+
interface ResolverOptions<T> {
|
|
4
|
+
/** Marks a resolver as an api controller (new ApiContract-based). */
|
|
5
|
+
isApiController?: boolean;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Register an `AbstractApiController` subclass with the awilix DI container.
|
|
10
|
+
*
|
|
11
|
+
* The returned resolver does **not** set `isSSEController` or `isDualModeController`,
|
|
12
|
+
* so `DIContext` reads its `routes` property automatically during `registerRoutes()`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // In a module's resolveControllers():
|
|
17
|
+
* return {
|
|
18
|
+
* userController: asApiControllerClass(UserController),
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function asApiControllerClass<T = object>(Type: {
|
|
23
|
+
prototype: T;
|
|
24
|
+
}, opts?: BuildResolverOptions<T>): BuildResolver<T> & DisposableResolver<T>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { asFunction } from 'awilix';
|
|
2
|
+
/**
|
|
3
|
+
* Register an `AbstractApiController` subclass with the awilix DI container.
|
|
4
|
+
*
|
|
5
|
+
* The returned resolver does **not** set `isSSEController` or `isDualModeController`,
|
|
6
|
+
* so `DIContext` reads its `routes` property automatically during `registerRoutes()`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // In a module's resolveControllers():
|
|
11
|
+
* return {
|
|
12
|
+
* userController: asApiControllerClass(UserController),
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function asApiControllerClass(Type, opts) {
|
|
17
|
+
const Ctor = Type;
|
|
18
|
+
return asFunction(
|
|
19
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic constructor invocation with cradle proxy
|
|
20
|
+
(cradle) => new Ctor(cradle), {
|
|
21
|
+
public: false,
|
|
22
|
+
isApiController: true,
|
|
23
|
+
...opts,
|
|
24
|
+
lifetime: 'SINGLETON',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=asApiControllerClass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asApiControllerClass.js","sourceRoot":"","sources":["../../../lib/api-contracts/asApiControllerClass.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AASnC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAsB,EACtB,IAA8B;IAE9B,MAAM,IAAI,GAAG,IAAiC,CAAA;IAC9C,OAAO,UAAU;IACf,+FAA+F;IAC/F,CAAC,MAAW,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,EACjC;QACE,MAAM,EAAE,KAAK;QACb,eAAe,EAAE,IAAI;QACrB,GAAG,IAAI;QACP,QAAQ,EAAE,WAAW;KACtB,CACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AbstractApiController } from './AbstractApiController.ts';
|
|
2
|
+
export type { ApiNonSseHandler, ApiRouteOptions, ApiSseHandler, InferApiHandler, InferApiRequest, InferApiStatusResponse, } from './apiHandlerTypes.ts';
|
|
3
|
+
export { buildApiRoute } from './apiRouteBuilder.ts';
|
|
4
|
+
export { asApiControllerClass } from './asApiControllerClass.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/api-contracts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AASlE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA"}
|