@congruent-stack/congruent-api 0.7.0 → 0.9.0-rc.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/dist/index.cjs CHANGED
@@ -158,7 +158,7 @@ class MethodEndpointHandlerRegistryEntry {
158
158
  callback(this);
159
159
  return this;
160
160
  }
161
- _injection = (_dicontainer) => ({});
161
+ _injection = (_diScope) => ({});
162
162
  get injection() {
163
163
  return this._injection;
164
164
  }
@@ -166,28 +166,28 @@ class MethodEndpointHandlerRegistryEntry {
166
166
  this._injection = injection;
167
167
  return this;
168
168
  }
169
- async trigger(data) {
169
+ async trigger(diScope, requestObject) {
170
170
  if (!this._handler) {
171
171
  throw new Error("Handler not set for this endpoint");
172
172
  }
173
173
  let badRequestResponse = null;
174
- const headers = parseRequestDefinitionField(this._methodEndpoint.definition, "headers", data);
174
+ const headers = parseRequestDefinitionField(this._methodEndpoint.definition, "headers", requestObject);
175
175
  if (isHttpResponseObject(headers)) {
176
176
  badRequestResponse = headers;
177
177
  return badRequestResponse;
178
178
  }
179
- const query = parseRequestDefinitionField(this._methodEndpoint.definition, "query", data);
179
+ const query = parseRequestDefinitionField(this._methodEndpoint.definition, "query", requestObject);
180
180
  if (isHttpResponseObject(query)) {
181
181
  badRequestResponse = query;
182
182
  return badRequestResponse;
183
183
  }
184
- const body = parseRequestDefinitionField(this._methodEndpoint.definition, "body", data);
184
+ const body = parseRequestDefinitionField(this._methodEndpoint.definition, "body", requestObject);
185
185
  if (isHttpResponseObject(body)) {
186
186
  badRequestResponse = body;
187
187
  return badRequestResponse;
188
188
  }
189
189
  const path = `/${this._methodEndpoint.pathSegments.map(
190
- (segment) => segment.startsWith(":") ? data.pathParams[segment.slice(1)] ?? "?" : segment
190
+ (segment) => segment.startsWith(":") ? requestObject.pathParams[segment.slice(1)] ?? "?" : segment
191
191
  ).join("/")}`;
192
192
  return await this._handler({
193
193
  method: this._methodEndpoint.method,
@@ -195,32 +195,45 @@ class MethodEndpointHandlerRegistryEntry {
195
195
  genericPath: this._methodEndpoint.genericPath,
196
196
  pathSegments: this._methodEndpoint.pathSegments,
197
197
  headers,
198
- pathParams: data.pathParams,
198
+ pathParams: requestObject.pathParams,
199
199
  query,
200
200
  body,
201
- injected: this._injection(this._dicontainer.createScope())
201
+ injected: this._injection(diScope)
202
202
  });
203
203
  }
204
204
  }
205
- function parseRequestDefinitionField(definition, key, data) {
205
+ function parseRequestDefinitionField(definition, key, requestObject) {
206
206
  if (definition[key]) {
207
- if (!(key in data) || data[key] === null || data[key] === void 0) {
208
- if (!definition[key].safeParse(data[key]).success) {
207
+ if (!(key in requestObject) || requestObject[key] === null || requestObject[key] === void 0) {
208
+ const result2 = definition[key].safeParse(requestObject[key]);
209
+ if (!result2.success) {
210
+ switch (definition[key].type) {
211
+ case "optional":
212
+ if (requestObject[key] === null) {
213
+ return void 0;
214
+ }
215
+ break;
216
+ case "nullable":
217
+ if (requestObject[key] === void 0) {
218
+ return null;
219
+ }
220
+ break;
221
+ }
209
222
  return {
210
223
  code: HttpStatusCode.BadRequest_400,
211
224
  body: `'${key}' is required for this endpoint` + (key === "body" ? ", { 'Content-Type': 'application/json' } header might be missing" : "")
212
225
  };
213
226
  }
214
- return null;
227
+ return result2.data;
215
228
  }
216
- const result = definition[key].safeParse(data[key]);
229
+ const result = definition[key].safeParse(requestObject[key]);
217
230
  if (!result.success) {
218
231
  return {
219
232
  code: HttpStatusCode.BadRequest_400,
220
233
  body: result.error.issues
221
234
  };
222
235
  }
223
- return result.data ?? null;
236
+ return result.data;
224
237
  }
225
238
  return null;
226
239
  }
@@ -235,6 +248,9 @@ function middleware(apiReg, path) {
235
248
  }
236
249
  class MiddlewareHandlersRegistryEntryInternal {
237
250
  _dicontainer;
251
+ get dicontainer() {
252
+ return this._dicontainer;
253
+ }
238
254
  _middlewareGenericPath;
239
255
  get genericPath() {
240
256
  return this._middlewareGenericPath;
@@ -265,39 +281,42 @@ class MiddlewareHandlersRegistryEntryInternal {
265
281
  this._handler = handler;
266
282
  }
267
283
  _injection = (_dicontainer) => ({});
268
- async trigger(data, next) {
284
+ async trigger(diScope, requestObject, next) {
269
285
  let badRequestResponse = null;
270
- const headers = middlewareParseRequestDefinitionField(this._inputSchemas, "headers", data);
286
+ const headers = middlewareParseRequestDefinitionField(this._inputSchemas, "headers", requestObject);
271
287
  if (isHttpResponseObject(headers)) {
272
288
  badRequestResponse = headers;
273
289
  return badRequestResponse;
274
290
  }
275
- const query = middlewareParseRequestDefinitionField(this._inputSchemas, "query", data);
291
+ const query = middlewareParseRequestDefinitionField(this._inputSchemas, "query", requestObject);
276
292
  if (isHttpResponseObject(query)) {
277
293
  badRequestResponse = query;
278
294
  return badRequestResponse;
279
295
  }
280
- const body = middlewareParseRequestDefinitionField(this._inputSchemas, "body", data);
296
+ const body = middlewareParseRequestDefinitionField(this._inputSchemas, "body", requestObject);
281
297
  if (isHttpResponseObject(body)) {
282
298
  badRequestResponse = body;
283
299
  return badRequestResponse;
284
300
  }
285
301
  const { method, pathSegments } = this._splitMiddlewarePath();
286
302
  const path = `/${pathSegments.map(
287
- (segment) => segment.startsWith(":") ? data.pathParams[segment.slice(1)] ?? "?" : segment
303
+ (segment) => segment.startsWith(":") ? requestObject.pathParams[segment.slice(1)] ?? "?" : segment
288
304
  ).join("/")}`;
289
- return await this._handler({
290
- method,
291
- // TODO: might be empty, as middleware can be registered with path only, without method, possible fix: take it from express.request.method
292
- path,
293
- genericPath: this.genericPath,
294
- pathSegments,
295
- headers,
296
- pathParams: data.pathParams,
297
- query,
298
- body,
299
- injected: this._injection(this._dicontainer.createScope())
300
- }, next);
305
+ return await this._handler(
306
+ {
307
+ method,
308
+ // TODO: might be empty, as middleware can be registered with path only, without method, possible fix: take it from express.request.method
309
+ path,
310
+ genericPath: this.genericPath,
311
+ pathSegments,
312
+ headers,
313
+ pathParams: requestObject.pathParams,
314
+ query,
315
+ body,
316
+ injected: this._injection(diScope)
317
+ },
318
+ next
319
+ );
301
320
  }
302
321
  }
303
322
  class MiddlewareHandlersRegistryEntry {
@@ -307,7 +326,7 @@ class MiddlewareHandlersRegistryEntry {
307
326
  this._registry = registry;
308
327
  this._path = path;
309
328
  }
310
- _injection = (_dicontainer) => ({});
329
+ _injection = (_diScope) => ({});
311
330
  get injection() {
312
331
  return this._injection;
313
332
  }
@@ -332,8 +351,13 @@ class MiddlewareHandlersRegistry {
332
351
  this.dicontainer = dicontainer;
333
352
  this._onHandlerRegisteredCallback = callback;
334
353
  }
354
+ _list = [];
355
+ get list() {
356
+ return this._list;
357
+ }
335
358
  register(entry) {
336
359
  if (this._onHandlerRegisteredCallback) {
360
+ this._list.push(entry);
337
361
  this._onHandlerRegisteredCallback(entry);
338
362
  }
339
363
  }
@@ -342,25 +366,38 @@ class MiddlewareHandlersRegistry {
342
366
  this._onHandlerRegisteredCallback = callback;
343
367
  }
344
368
  }
345
- function middlewareParseRequestDefinitionField(inputSchemas, key, data) {
369
+ function middlewareParseRequestDefinitionField(inputSchemas, key, requestObject) {
346
370
  if (inputSchemas[key]) {
347
- if (!(key in data) || data[key] === null || data[key] === void 0) {
348
- if (!inputSchemas[key].safeParse(data[key]).success) {
371
+ if (!(key in requestObject) || requestObject[key] === null || requestObject[key] === void 0) {
372
+ const result2 = inputSchemas[key].safeParse(requestObject[key]);
373
+ if (!result2.success) {
374
+ switch (inputSchemas[key].type) {
375
+ case "optional":
376
+ if (requestObject[key] === null) {
377
+ return void 0;
378
+ }
379
+ break;
380
+ case "nullable":
381
+ if (requestObject[key] === void 0) {
382
+ return null;
383
+ }
384
+ break;
385
+ }
349
386
  return {
350
387
  code: HttpStatusCode.BadRequest_400,
351
388
  body: `'${key}' is required for this endpoint` + (key === "body" ? ", { 'Content-Type': 'application/json' } header might be missing" : "")
352
389
  };
353
390
  }
354
- return null;
391
+ return result2.data;
355
392
  }
356
- const result = inputSchemas[key].safeParse(data[key]);
393
+ const result = inputSchemas[key].safeParse(requestObject[key]);
357
394
  if (!result.success) {
358
395
  return {
359
396
  code: HttpStatusCode.BadRequest_400,
360
397
  body: result.error.issues
361
398
  };
362
399
  }
363
- return result.data ?? null;
400
+ return result.data;
364
401
  }
365
402
  return null;
366
403
  }
@@ -528,12 +565,12 @@ class InnerApiClient {
528
565
  delete currObj[key];
529
566
  InnerApiClient._initialize(client, val, clientGenericHandler);
530
567
  } else if (val instanceof HttpMethodEndpoint) {
531
- currObj[key] = (req) => {
568
+ currObj[key] = (requestObject) => {
532
569
  const pathParams = { ...client.__CONTEXT__.pathParameters };
533
570
  client.__CONTEXT__ = InnerApiClient._initNewContext();
534
- const headers = clientParseRequestDefinitionField(val.definition, "headers", req);
535
- const query = clientParseRequestDefinitionField(val.definition, "query", req);
536
- const body = clientParseRequestDefinitionField(val.definition, "body", req);
571
+ const headers = clientParseRequestDefinitionField(val.definition, "headers", requestObject);
572
+ const query = clientParseRequestDefinitionField(val.definition, "query", requestObject);
573
+ const body = clientParseRequestDefinitionField(val.definition, "body", requestObject);
537
574
  const path = `/${val.pathSegments.map(
538
575
  (segment) => segment.startsWith(":") ? pathParams[segment.slice(1)] ?? "?" : segment
539
576
  ).join("/")}`;
@@ -555,107 +592,153 @@ class InnerApiClient {
555
592
  }
556
593
  }
557
594
  const ApiClient = InnerApiClient;
558
- function clientParseRequestDefinitionField(definition, key, data) {
595
+ function clientParseRequestDefinitionField(definition, key, requestObject) {
559
596
  if (definition[key]) {
560
- if (!(key in data) || data[key] === null || data[key] === void 0) {
561
- if (!definition[key].safeParse(data[key]).success) {
562
- throw new Error(`${key} are required for this endpoint`);
597
+ if (!(key in requestObject) || requestObject[key] === null || requestObject[key] === void 0) {
598
+ const result2 = definition[key].safeParse(requestObject[key]);
599
+ if (!result2.success) {
600
+ switch (definition[key].type) {
601
+ case "optional":
602
+ if (requestObject[key] === null) {
603
+ return void 0;
604
+ }
605
+ break;
606
+ case "nullable":
607
+ if (requestObject[key] === void 0) {
608
+ return null;
609
+ }
610
+ break;
611
+ }
612
+ throw new Error(`'${key}' is required for this endpoint`);
563
613
  }
564
- return null;
614
+ return result2.data;
565
615
  }
566
- const result = definition[key].safeParse(data[key]);
616
+ const result = definition[key].safeParse(requestObject[key]);
567
617
  if (!result.success) {
568
618
  throw new Error(`Validation for '${key}' failed`, { cause: result.error });
569
619
  }
570
- return result.data ?? null;
620
+ return result.data;
571
621
  }
572
622
  return null;
573
623
  }
574
624
 
575
- class DIContainer {
576
- registry = /* @__PURE__ */ new Map();
577
- singletons = /* @__PURE__ */ new Map();
578
- proxy;
579
- /**
580
- * The constructor returns a Proxy. This is the runtime magic that intercepts
581
- * calls to methods like `getLoggerService()`. It parses the method name,
582
- * finds the corresponding service class in the registry, and resolves it.
583
- */
584
- constructor() {
585
- this.proxy = new Proxy(this, {
586
- get: (target, prop, receiver) => {
587
- if (typeof prop === "string" && prop.startsWith("get")) {
588
- const serviceName = prop.substring(3);
589
- if (target.registry.has(serviceName)) {
590
- return () => target.resolveByName(serviceName);
625
+ class DIContainerBase {
626
+ _map = /* @__PURE__ */ new Map();
627
+ _singletonInstances = /* @__PURE__ */ new Map();
628
+ createScope() {
629
+ const proxy = new Proxy({
630
+ _map: this._map,
631
+ _singletonInstances: this._singletonInstances,
632
+ _scopedInstances: /* @__PURE__ */ new Map(),
633
+ _isBuildingSingleton: false
634
+ }, {
635
+ get: (target, prop) => {
636
+ if (prop.startsWith("get")) {
637
+ const serviceName = prop.slice(3);
638
+ if (target._map.has(serviceName)) {
639
+ const entry = target._map.get(serviceName);
640
+ switch (entry.lifetime) {
641
+ case "transient":
642
+ return () => {
643
+ if (target._isBuildingSingleton) {
644
+ throw new Error(`Cannot resolve transient service '${serviceName}' while building a singleton`);
645
+ }
646
+ return entry.factory(proxy);
647
+ };
648
+ case "scoped":
649
+ return () => {
650
+ if (target._isBuildingSingleton) {
651
+ throw new Error(`Cannot resolve scoped service '${serviceName}' while building a singleton`);
652
+ }
653
+ if (!target._scopedInstances.has(serviceName)) {
654
+ const instance = entry.factory(proxy);
655
+ target._scopedInstances.set(serviceName, instance);
656
+ }
657
+ return target._scopedInstances.get(serviceName);
658
+ };
659
+ case "singleton":
660
+ return () => {
661
+ if (!target._singletonInstances.has(serviceName)) {
662
+ target._isBuildingSingleton = true;
663
+ try {
664
+ const instance = entry.factory(proxy);
665
+ target._singletonInstances.set(serviceName, instance);
666
+ } finally {
667
+ target._isBuildingSingleton = false;
668
+ }
669
+ }
670
+ return target._singletonInstances.get(serviceName);
671
+ };
672
+ default:
673
+ throw new Error(`Unsupported lifetime: ${entry.lifetime}`);
674
+ }
591
675
  } else {
592
676
  throw new Error(`Service not registered: ${serviceName}`);
593
677
  }
594
678
  }
595
- return Reflect.get(target, prop, receiver);
679
+ throw new Error(`Property access denied by Proxy: ${String(prop)}`);
596
680
  }
597
681
  });
598
- return this.proxy;
682
+ return proxy;
599
683
  }
600
- /**
601
- * Registers a service with explicit service name (fully type-safe).
602
- */
603
- register(serviceNameLiteral, factory, lifetime = "transient") {
604
- this.registry.set(serviceNameLiteral, { factory, lifetime });
684
+ }
685
+ class DIContainer extends DIContainerBase {
686
+ register(serviceNameCapitalizedLiteral, factory, lifetime) {
687
+ const entry = { factory, lifetime };
688
+ this._map.set(serviceNameCapitalizedLiteral, entry);
605
689
  return this;
606
690
  }
607
- /**
608
- * Resolves a service by service name.
609
- */
610
- resolveByName(serviceName) {
611
- const registration = this.registry.get(serviceName);
612
- if (!registration) {
613
- throw new Error(`Service not registered: ${serviceName}`);
614
- }
615
- if (registration.lifetime === "singleton") {
616
- if (!this.singletons.has(serviceName)) {
617
- const instance = registration.factory(this.proxy);
618
- this.singletons.set(serviceName, instance);
619
- }
620
- return this.singletons.get(serviceName);
621
- }
622
- return registration.factory(this.proxy);
691
+ createTestClone() {
692
+ return new DIContainerTestClone(this);
623
693
  }
624
- /**
625
- * Creates a scoped container with typed method access to services.
626
- */
627
- createScope() {
628
- const scope = {};
629
- return new Proxy(scope, {
630
- get: (target, prop) => {
631
- if (typeof prop === "string" && prop.startsWith("get")) {
632
- const serviceName = prop.substring(3);
633
- if (this.registry.has(serviceName)) {
634
- const registration = this.registry.get(serviceName);
635
- return () => {
636
- if (registration?.lifetime === "transient") {
637
- return this.resolveByName(serviceName);
638
- }
639
- const cacheKey = `_cached_${serviceName}`;
640
- if (!target[cacheKey]) {
641
- target[cacheKey] = this.resolveByName(serviceName);
642
- }
643
- return target[cacheKey];
644
- };
645
- } else {
646
- throw new Error(`Service not registered: ${serviceName}`);
647
- }
648
- }
649
- return Reflect.get(target, prop);
650
- }
694
+ }
695
+ class DIContainerTestClone extends DIContainerBase {
696
+ constructor(original) {
697
+ super();
698
+ original["_map"].forEach((value, key) => {
699
+ this._map.set(key, {
700
+ factory: (_scope) => {
701
+ throw new Error(`Service registration not overridden: ${key}`);
702
+ },
703
+ lifetime: value.lifetime
704
+ });
651
705
  });
652
706
  }
653
- createTestClone() {
654
- const clone = new DIContainer();
655
- return clone;
707
+ override(serviceNameLiteral, factory) {
708
+ const registration = this._map.get(serviceNameLiteral);
709
+ if (!registration) {
710
+ throw new Error(`Service not registered: ${serviceNameLiteral}`);
711
+ }
712
+ this._map.set(serviceNameLiteral, { factory, lifetime: registration.lifetime });
713
+ return this;
656
714
  }
657
715
  }
658
716
 
717
+ function execMiddleware(diScope, list, input) {
718
+ const queue = [...list];
719
+ const next = async () => {
720
+ const current = queue.shift();
721
+ if (!current) {
722
+ return;
723
+ }
724
+ const result = await current.trigger(
725
+ diScope,
726
+ {
727
+ headers: input.headers,
728
+ pathParams: input.pathParams,
729
+ body: input.body,
730
+ query: input.query
731
+ },
732
+ next
733
+ );
734
+ if (result) {
735
+ return result;
736
+ }
737
+ return next();
738
+ };
739
+ return next();
740
+ }
741
+
659
742
  function createInProcApiClient(contract, testContainer, registry) {
660
743
  const testApiReg = createRegistry(testContainer, contract, {
661
744
  handlerRegisteredCallback: (_entry) => {
@@ -663,6 +746,10 @@ function createInProcApiClient(contract, testContainer, registry) {
663
746
  middlewareHandlerRegisteredCallback: (_entry) => {
664
747
  }
665
748
  });
749
+ registry._middlewareRegistry.list.forEach((mwEntry) => {
750
+ testApiReg._middlewareRegistry.register(mwEntry);
751
+ });
752
+ const mwReg = testApiReg._middlewareRegistry;
666
753
  flatListAllRegistryEntries(registry).forEach((entry) => {
667
754
  if (!entry.handler) {
668
755
  return;
@@ -671,12 +758,17 @@ function createInProcApiClient(contract, testContainer, registry) {
671
758
  rt.inject(entry.injection).register(entry.handler);
672
759
  });
673
760
  const client = createClient(contract, async (input) => {
761
+ const diScope = testContainer.createScope();
762
+ const haltExecResponse = await execMiddleware(diScope, mwReg.list, input);
763
+ if (haltExecResponse) {
764
+ return haltExecResponse;
765
+ }
674
766
  const rt = route(testApiReg, `${input.method} ${input.genericPath}`);
675
- const result = rt.trigger({
767
+ const result = await rt.trigger(diScope, {
676
768
  headers: input.headers,
677
769
  pathParams: input.pathParams,
678
- body: input.body ?? {},
679
- query: input.query ?? {}
770
+ body: input.body,
771
+ query: input.query
680
772
  });
681
773
  return result;
682
774
  });
@@ -687,6 +779,8 @@ exports.ApiClient = ApiClient;
687
779
  exports.ApiContract = ApiContract;
688
780
  exports.ApiHandlersRegistry = ApiHandlersRegistry;
689
781
  exports.DIContainer = DIContainer;
782
+ exports.DIContainerBase = DIContainerBase;
783
+ exports.DIContainerTestClone = DIContainerTestClone;
690
784
  exports.HttpMethodEndpoint = HttpMethodEndpoint;
691
785
  exports.HttpMethodEndpointResponse = HttpMethodEndpointResponse;
692
786
  exports.HttpStatusCode = HttpStatusCode;
package/dist/index.d.cts CHANGED
@@ -171,53 +171,29 @@ declare function isHttpResponseObject(obj: any): obj is HttpResponseObject;
171
171
  type HttpMethodEndpointHandler<TDef extends IHttpMethodEndpointDefinition, TPathParams extends string, TInjected> = (input: HttpMethodEndpointHandlerInput<TDef, TPathParams, TInjected>) => Promise<HttpMethodEndpointHandlerOutput<TDef>>;
172
172
  type ClientHttpMethodEndpointHandler = (input: ClientHttpMethodEndpointHandlerInput) => Promise<ClientHttpMethodEndpointHandlerOutput>;
173
173
 
174
- type StringLiteral<S> = S extends string ? string extends S ? never : S : never;
175
- type DIClass<T> = new (...args: any[]) => T;
174
+ type StringLiteral<T extends string> = string extends T ? never : T;
175
+ type CapitalizedStringLiteral<T extends string> = string extends T ? never : T extends `${Uppercase<infer F>}${infer _}` ? F extends Lowercase<F> ? never : T : `❌ ERROR: Must start with uppercase letter`;
176
176
  type DILifetime = 'singleton' | 'transient' | 'scoped';
177
- type DIServiceRegistry = Record<string, any>;
178
- /**
179
- * A scoped container that provides typed method access to services.
180
- */
181
- type DIScope<R extends DIServiceRegistry> = {
182
- [K in keyof R as `get${string & K}`]: () => R[K];
177
+ type DIRegistryEntry<T> = {
178
+ factory: (scope: any) => T;
179
+ lifetime: DILifetime;
183
180
  };
184
- /**
185
- * This is the core of the static typing. It's a "mapped type" that takes a
186
- * ServiceRegistry `R` and creates a new type. For each key `K` in the registry,
187
- * it adds a method named `getK` that returns an instance of the corresponding service type.
188
- *
189
- * For example, if R is `{ LoggerService: LoggerService }`, this type will be:
190
- * { getLoggerService: () => LoggerService }
191
- */
192
- type DITypedContainer<R extends DIServiceRegistry> = DIContainer<R> & {
193
- [K in keyof R as `get${string & K}`]: () => R[K];
181
+ type DIRegistry = Record<string, DIRegistryEntry<any>>;
182
+ type DIScope<R extends DIRegistry> = {
183
+ [K in keyof R as `get${string & K}`]: () => R[K] extends DIRegistryEntry<infer T> ? T : never;
194
184
  };
195
- /**
196
- * A Dependency Injection (DI) Container that provides static typing for resolved services.
197
- */
198
- declare class DIContainer<R extends DIServiceRegistry = {}> {
199
- private registry;
200
- private singletons;
201
- private proxy;
202
- /**
203
- * The constructor returns a Proxy. This is the runtime magic that intercepts
204
- * calls to methods like `getLoggerService()`. It parses the method name,
205
- * finds the corresponding service class in the registry, and resolves it.
206
- */
207
- constructor();
208
- /**
209
- * Registers a service with explicit service name (fully type-safe).
210
- */
211
- register<T, N extends string>(serviceNameLiteral: StringLiteral<N>, factory: (container: DITypedContainer<R>) => T, lifetime?: DILifetime): DITypedContainer<R & Record<N, T>>;
212
- /**
213
- * Resolves a service by service name.
214
- */
215
- private resolveByName;
216
- /**
217
- * Creates a scoped container with typed method access to services.
218
- */
185
+ declare class DIContainerBase<R extends DIRegistry> {
186
+ protected _map: Map<string, DIRegistryEntry<any>>;
187
+ protected _singletonInstances: Map<string, any>;
219
188
  createScope(): DIScope<R>;
220
- createTestClone(): this;
189
+ }
190
+ declare class DIContainer<R extends DIRegistry = {}> extends DIContainerBase<R> {
191
+ register<K extends string, T>(serviceNameCapitalizedLiteral: CapitalizedStringLiteral<K>, factory: (scope: DIScope<R>) => T, lifetime: DILifetime): DIContainer<R & Record<K, DIRegistryEntry<T>>>;
192
+ createTestClone(): DIContainerTestClone<R, this>;
193
+ }
194
+ declare class DIContainerTestClone<R extends DIRegistry, TDIContainer extends DIContainer<R>> extends DIContainerBase<R> {
195
+ constructor(original: TDIContainer);
196
+ override<K extends keyof R & string>(serviceNameLiteral: K, factory: (scope: DIScope<R>) => R[K] extends DIRegistryEntry<infer T> ? T : never): this;
221
197
  }
222
198
 
223
199
  type PrepareRegistryEntryCallback<TDef extends IHttpMethodEndpointDefinition & ValidateHttpMethodEndpointDefinition<TDef>, TDIContainer extends DIContainer, TPathParams extends string> = (entry: MethodEndpointHandlerRegistryEntry<TDef, TDIContainer, TPathParams, any>) => void;
@@ -236,8 +212,8 @@ declare class MethodEndpointHandlerRegistryEntry<TDef extends IHttpMethodEndpoin
236
212
  prepare(callback: PrepareRegistryEntryCallback<TDef, TDIContainer, TPathParams>): this;
237
213
  private _injection;
238
214
  get injection(): any;
239
- inject<TNewInjected>(injection: (dicontainer: TDIContainer) => TNewInjected): MethodEndpointHandlerRegistryEntry<TDef, TDIContainer, TPathParams, TNewInjected>;
240
- trigger(data: {
215
+ inject<TNewInjected>(injection: (diScope: ReturnType<TDIContainer['createScope']>) => TNewInjected): MethodEndpointHandlerRegistryEntry<TDef, TDIContainer, TPathParams, TNewInjected>;
216
+ trigger(diScope: DIScope<any>, requestObject: {
241
217
  headers: Record<string, string>;
242
218
  pathParams: Record<string, string>;
243
219
  query: object;
@@ -280,6 +256,7 @@ type MiddlewarePath<TDef, BasePath extends string = ""> = (BasePath extends "" ?
280
256
  }[keyof TDef & string];
281
257
  declare class MiddlewareHandlersRegistryEntryInternal<TDIContainer extends DIContainer, TInjected> {
282
258
  private readonly _dicontainer;
259
+ get dicontainer(): TDIContainer;
283
260
  private readonly _middlewareGenericPath;
284
261
  get genericPath(): string;
285
262
  private _splitMiddlewarePath;
@@ -287,7 +264,7 @@ declare class MiddlewareHandlersRegistryEntryInternal<TDIContainer extends DICon
287
264
  private readonly _handler;
288
265
  constructor(diContainer: TDIContainer, middlewarePath: string, inputSchemas: MiddlewareHandlerInputSchemas, injection: (dicontainer: TDIContainer) => TInjected, handler: MiddlewareHandlerInternal<TInjected>);
289
266
  private _injection;
290
- trigger(data: {
267
+ trigger(diScope: DIScope<any>, requestObject: {
291
268
  headers: Record<string, string>;
292
269
  pathParams: Record<string, string>;
293
270
  query: object;
@@ -300,13 +277,15 @@ declare class MiddlewareHandlersRegistryEntry<TApiDef extends IApiContractDefini
300
277
  constructor(registry: MiddlewareHandlersRegistry<TDIContainer>, path: TPath);
301
278
  private _injection;
302
279
  get injection(): any;
303
- inject<TNewInjected>(injection: (dicontainer: TDIContainer) => TNewInjected): MiddlewareHandlersRegistryEntry<TApiDef, TDIContainer, TPathParams, TPath, TNewInjected>;
280
+ inject<TNewInjected>(injection: (diScope: ReturnType<TDIContainer['createScope']>) => TNewInjected): MiddlewareHandlersRegistryEntry<TApiDef, TDIContainer, TPathParams, TPath, TNewInjected>;
304
281
  register<const InputSchemas extends MiddlewareHandlerInputSchemas>(inputSchemas: InputSchemas, handler: MiddlewareHandler<`${TPathParams}${ExtractConcatenatedParamNamesFromPath<TPath>}`, InputSchemas, TInjected>): void;
305
282
  }
306
283
  type OnMiddlewareHandlerRegisteredCallback<TDIContainer extends DIContainer, TInjected> = (entry: MiddlewareHandlersRegistryEntryInternal<TDIContainer, TInjected>) => void;
307
284
  declare class MiddlewareHandlersRegistry<TDIContainer extends DIContainer> {
308
285
  readonly dicontainer: TDIContainer;
309
286
  constructor(dicontainer: TDIContainer, callback: OnMiddlewareHandlerRegisteredCallback<TDIContainer, unknown>);
287
+ private readonly _list;
288
+ get list(): Readonly<MiddlewareHandlersRegistryEntryInternal<TDIContainer, unknown>[]>;
310
289
  register<TInjected>(entry: MiddlewareHandlersRegistryEntryInternal<TDIContainer, TInjected>): void;
311
290
  private _onHandlerRegisteredCallback;
312
291
  _onHandlerRegistered(callback: OnMiddlewareHandlerRegisteredCallback<TDIContainer, unknown>): void;
@@ -379,6 +358,6 @@ type ApiClientDef<ObjType extends object> = {
379
358
  type ApiClient<TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>> = Omit<ApiClientDef<InnerApiClient<TDef> & TDef>, "__CONTEXT__">;
380
359
  declare const ApiClient: new <TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>>(contract: ApiContract<TDef>, clientGenericHandler: ClientHttpMethodEndpointHandler) => ApiClient<TDef>;
381
360
 
382
- declare function createInProcApiClient<TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>, TDIContainer extends DIContainer>(contract: ApiContract<TDef>, testContainer: TDIContainer, registry: ApiHandlersRegistry<TDef, TDIContainer>): ApiClient<TDef>;
361
+ declare function createInProcApiClient<TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>, TDIContainer extends DIContainer, TDIContainerTestClone extends DIContainerTestClone<any, TDIContainer>>(contract: ApiContract<TDef>, testContainer: TDIContainerTestClone, registry: ApiHandlersRegistry<TDef, TDIContainer>): ApiClient<TDef>;
383
362
 
384
- export { ApiClient, type ApiClientDef, ApiContract, ApiHandlersRegistry, type ApiHandlersRegistryDef, type ClientHttpMethodEndpointHandler, type ClientHttpMethodEndpointHandlerInput, type ClientHttpMethodEndpointHandlerOutput, type DIClass, DIContainer, type DILifetime, type DIScope, type ExtractConcatenatedParamNamesFromMethodFirstPath, type ExtractConcatenatedParamNamesFromPath, type ExtractConcatenatedParamNamesFromPathSegments, type ExtractEndpointFromPath, type GenericOnHandlerRegisteredCallback, type HttpMethod, type HttpMethodCallFunc, type HttpMethodCallInput, HttpMethodEndpoint, type HttpMethodEndpointHandler, type HttpMethodEndpointHandlerInput, type HttpMethodEndpointHandlerOutput, HttpMethodEndpointResponse, type HttpMethodEndpointResponses, type HttpResponseObject, HttpStatusCode, type IApiContractDefinition, type IClientContext, type IHttpMethodEndpointDefinition, type IHttpMethodEndpointResponseDefinition, type IRegistrySettings, type LowerCasedHttpMethod, MethodEndpointHandlerRegistryEntry, type MethodFirstPath, type MiddlewareHandler, type MiddlewareHandlerInput, type MiddlewareHandlerInputInternal, type MiddlewareHandlerInputSchemas, type MiddlewareHandlerInternal, MiddlewareHandlersRegistry, MiddlewareHandlersRegistryEntry, MiddlewareHandlersRegistryEntryInternal, type MiddlewarePath, type OnHandlerRegisteredCallback, type OnMiddlewareHandlerRegisteredCallback, type PartialPath, type PartialPathResult, type PathParamFunc, type PrepareRegistryEntryCallback, type TypedPathParams, type ValidateApiContractDefinition, type ValidateHttpMethodEndpointDefinition, apiContract, createClient, createInProcApiClient, createRegistry, endpoint, flatListAllRegistryEntries, isHttpResponseObject, isHttpStatusCode, middleware, partial, partialPathString, register, response, route };
363
+ export { ApiClient, type ApiClientDef, ApiContract, ApiHandlersRegistry, type ApiHandlersRegistryDef, type CapitalizedStringLiteral, type ClientHttpMethodEndpointHandler, type ClientHttpMethodEndpointHandlerInput, type ClientHttpMethodEndpointHandlerOutput, DIContainer, DIContainerBase, DIContainerTestClone, type DILifetime, type DIRegistry, type DIRegistryEntry, type DIScope, type ExtractConcatenatedParamNamesFromMethodFirstPath, type ExtractConcatenatedParamNamesFromPath, type ExtractConcatenatedParamNamesFromPathSegments, type ExtractEndpointFromPath, type GenericOnHandlerRegisteredCallback, type HttpMethod, type HttpMethodCallFunc, type HttpMethodCallInput, HttpMethodEndpoint, type HttpMethodEndpointHandler, type HttpMethodEndpointHandlerInput, type HttpMethodEndpointHandlerOutput, HttpMethodEndpointResponse, type HttpMethodEndpointResponses, type HttpResponseObject, HttpStatusCode, type IApiContractDefinition, type IClientContext, type IHttpMethodEndpointDefinition, type IHttpMethodEndpointResponseDefinition, type IRegistrySettings, type LowerCasedHttpMethod, MethodEndpointHandlerRegistryEntry, type MethodFirstPath, type MiddlewareHandler, type MiddlewareHandlerInput, type MiddlewareHandlerInputInternal, type MiddlewareHandlerInputSchemas, type MiddlewareHandlerInternal, MiddlewareHandlersRegistry, MiddlewareHandlersRegistryEntry, MiddlewareHandlersRegistryEntryInternal, type MiddlewarePath, type OnHandlerRegisteredCallback, type OnMiddlewareHandlerRegisteredCallback, type PartialPath, type PartialPathResult, type PathParamFunc, type PrepareRegistryEntryCallback, type StringLiteral, type TypedPathParams, type ValidateApiContractDefinition, type ValidateHttpMethodEndpointDefinition, apiContract, createClient, createInProcApiClient, createRegistry, endpoint, flatListAllRegistryEntries, isHttpResponseObject, isHttpStatusCode, middleware, partial, partialPathString, register, response, route };
package/dist/index.d.mts CHANGED
@@ -171,53 +171,29 @@ declare function isHttpResponseObject(obj: any): obj is HttpResponseObject;
171
171
  type HttpMethodEndpointHandler<TDef extends IHttpMethodEndpointDefinition, TPathParams extends string, TInjected> = (input: HttpMethodEndpointHandlerInput<TDef, TPathParams, TInjected>) => Promise<HttpMethodEndpointHandlerOutput<TDef>>;
172
172
  type ClientHttpMethodEndpointHandler = (input: ClientHttpMethodEndpointHandlerInput) => Promise<ClientHttpMethodEndpointHandlerOutput>;
173
173
 
174
- type StringLiteral<S> = S extends string ? string extends S ? never : S : never;
175
- type DIClass<T> = new (...args: any[]) => T;
174
+ type StringLiteral<T extends string> = string extends T ? never : T;
175
+ type CapitalizedStringLiteral<T extends string> = string extends T ? never : T extends `${Uppercase<infer F>}${infer _}` ? F extends Lowercase<F> ? never : T : `❌ ERROR: Must start with uppercase letter`;
176
176
  type DILifetime = 'singleton' | 'transient' | 'scoped';
177
- type DIServiceRegistry = Record<string, any>;
178
- /**
179
- * A scoped container that provides typed method access to services.
180
- */
181
- type DIScope<R extends DIServiceRegistry> = {
182
- [K in keyof R as `get${string & K}`]: () => R[K];
177
+ type DIRegistryEntry<T> = {
178
+ factory: (scope: any) => T;
179
+ lifetime: DILifetime;
183
180
  };
184
- /**
185
- * This is the core of the static typing. It's a "mapped type" that takes a
186
- * ServiceRegistry `R` and creates a new type. For each key `K` in the registry,
187
- * it adds a method named `getK` that returns an instance of the corresponding service type.
188
- *
189
- * For example, if R is `{ LoggerService: LoggerService }`, this type will be:
190
- * { getLoggerService: () => LoggerService }
191
- */
192
- type DITypedContainer<R extends DIServiceRegistry> = DIContainer<R> & {
193
- [K in keyof R as `get${string & K}`]: () => R[K];
181
+ type DIRegistry = Record<string, DIRegistryEntry<any>>;
182
+ type DIScope<R extends DIRegistry> = {
183
+ [K in keyof R as `get${string & K}`]: () => R[K] extends DIRegistryEntry<infer T> ? T : never;
194
184
  };
195
- /**
196
- * A Dependency Injection (DI) Container that provides static typing for resolved services.
197
- */
198
- declare class DIContainer<R extends DIServiceRegistry = {}> {
199
- private registry;
200
- private singletons;
201
- private proxy;
202
- /**
203
- * The constructor returns a Proxy. This is the runtime magic that intercepts
204
- * calls to methods like `getLoggerService()`. It parses the method name,
205
- * finds the corresponding service class in the registry, and resolves it.
206
- */
207
- constructor();
208
- /**
209
- * Registers a service with explicit service name (fully type-safe).
210
- */
211
- register<T, N extends string>(serviceNameLiteral: StringLiteral<N>, factory: (container: DITypedContainer<R>) => T, lifetime?: DILifetime): DITypedContainer<R & Record<N, T>>;
212
- /**
213
- * Resolves a service by service name.
214
- */
215
- private resolveByName;
216
- /**
217
- * Creates a scoped container with typed method access to services.
218
- */
185
+ declare class DIContainerBase<R extends DIRegistry> {
186
+ protected _map: Map<string, DIRegistryEntry<any>>;
187
+ protected _singletonInstances: Map<string, any>;
219
188
  createScope(): DIScope<R>;
220
- createTestClone(): this;
189
+ }
190
+ declare class DIContainer<R extends DIRegistry = {}> extends DIContainerBase<R> {
191
+ register<K extends string, T>(serviceNameCapitalizedLiteral: CapitalizedStringLiteral<K>, factory: (scope: DIScope<R>) => T, lifetime: DILifetime): DIContainer<R & Record<K, DIRegistryEntry<T>>>;
192
+ createTestClone(): DIContainerTestClone<R, this>;
193
+ }
194
+ declare class DIContainerTestClone<R extends DIRegistry, TDIContainer extends DIContainer<R>> extends DIContainerBase<R> {
195
+ constructor(original: TDIContainer);
196
+ override<K extends keyof R & string>(serviceNameLiteral: K, factory: (scope: DIScope<R>) => R[K] extends DIRegistryEntry<infer T> ? T : never): this;
221
197
  }
222
198
 
223
199
  type PrepareRegistryEntryCallback<TDef extends IHttpMethodEndpointDefinition & ValidateHttpMethodEndpointDefinition<TDef>, TDIContainer extends DIContainer, TPathParams extends string> = (entry: MethodEndpointHandlerRegistryEntry<TDef, TDIContainer, TPathParams, any>) => void;
@@ -236,8 +212,8 @@ declare class MethodEndpointHandlerRegistryEntry<TDef extends IHttpMethodEndpoin
236
212
  prepare(callback: PrepareRegistryEntryCallback<TDef, TDIContainer, TPathParams>): this;
237
213
  private _injection;
238
214
  get injection(): any;
239
- inject<TNewInjected>(injection: (dicontainer: TDIContainer) => TNewInjected): MethodEndpointHandlerRegistryEntry<TDef, TDIContainer, TPathParams, TNewInjected>;
240
- trigger(data: {
215
+ inject<TNewInjected>(injection: (diScope: ReturnType<TDIContainer['createScope']>) => TNewInjected): MethodEndpointHandlerRegistryEntry<TDef, TDIContainer, TPathParams, TNewInjected>;
216
+ trigger(diScope: DIScope<any>, requestObject: {
241
217
  headers: Record<string, string>;
242
218
  pathParams: Record<string, string>;
243
219
  query: object;
@@ -280,6 +256,7 @@ type MiddlewarePath<TDef, BasePath extends string = ""> = (BasePath extends "" ?
280
256
  }[keyof TDef & string];
281
257
  declare class MiddlewareHandlersRegistryEntryInternal<TDIContainer extends DIContainer, TInjected> {
282
258
  private readonly _dicontainer;
259
+ get dicontainer(): TDIContainer;
283
260
  private readonly _middlewareGenericPath;
284
261
  get genericPath(): string;
285
262
  private _splitMiddlewarePath;
@@ -287,7 +264,7 @@ declare class MiddlewareHandlersRegistryEntryInternal<TDIContainer extends DICon
287
264
  private readonly _handler;
288
265
  constructor(diContainer: TDIContainer, middlewarePath: string, inputSchemas: MiddlewareHandlerInputSchemas, injection: (dicontainer: TDIContainer) => TInjected, handler: MiddlewareHandlerInternal<TInjected>);
289
266
  private _injection;
290
- trigger(data: {
267
+ trigger(diScope: DIScope<any>, requestObject: {
291
268
  headers: Record<string, string>;
292
269
  pathParams: Record<string, string>;
293
270
  query: object;
@@ -300,13 +277,15 @@ declare class MiddlewareHandlersRegistryEntry<TApiDef extends IApiContractDefini
300
277
  constructor(registry: MiddlewareHandlersRegistry<TDIContainer>, path: TPath);
301
278
  private _injection;
302
279
  get injection(): any;
303
- inject<TNewInjected>(injection: (dicontainer: TDIContainer) => TNewInjected): MiddlewareHandlersRegistryEntry<TApiDef, TDIContainer, TPathParams, TPath, TNewInjected>;
280
+ inject<TNewInjected>(injection: (diScope: ReturnType<TDIContainer['createScope']>) => TNewInjected): MiddlewareHandlersRegistryEntry<TApiDef, TDIContainer, TPathParams, TPath, TNewInjected>;
304
281
  register<const InputSchemas extends MiddlewareHandlerInputSchemas>(inputSchemas: InputSchemas, handler: MiddlewareHandler<`${TPathParams}${ExtractConcatenatedParamNamesFromPath<TPath>}`, InputSchemas, TInjected>): void;
305
282
  }
306
283
  type OnMiddlewareHandlerRegisteredCallback<TDIContainer extends DIContainer, TInjected> = (entry: MiddlewareHandlersRegistryEntryInternal<TDIContainer, TInjected>) => void;
307
284
  declare class MiddlewareHandlersRegistry<TDIContainer extends DIContainer> {
308
285
  readonly dicontainer: TDIContainer;
309
286
  constructor(dicontainer: TDIContainer, callback: OnMiddlewareHandlerRegisteredCallback<TDIContainer, unknown>);
287
+ private readonly _list;
288
+ get list(): Readonly<MiddlewareHandlersRegistryEntryInternal<TDIContainer, unknown>[]>;
310
289
  register<TInjected>(entry: MiddlewareHandlersRegistryEntryInternal<TDIContainer, TInjected>): void;
311
290
  private _onHandlerRegisteredCallback;
312
291
  _onHandlerRegistered(callback: OnMiddlewareHandlerRegisteredCallback<TDIContainer, unknown>): void;
@@ -379,6 +358,6 @@ type ApiClientDef<ObjType extends object> = {
379
358
  type ApiClient<TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>> = Omit<ApiClientDef<InnerApiClient<TDef> & TDef>, "__CONTEXT__">;
380
359
  declare const ApiClient: new <TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>>(contract: ApiContract<TDef>, clientGenericHandler: ClientHttpMethodEndpointHandler) => ApiClient<TDef>;
381
360
 
382
- declare function createInProcApiClient<TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>, TDIContainer extends DIContainer>(contract: ApiContract<TDef>, testContainer: TDIContainer, registry: ApiHandlersRegistry<TDef, TDIContainer>): ApiClient<TDef>;
361
+ declare function createInProcApiClient<TDef extends IApiContractDefinition & ValidateApiContractDefinition<TDef>, TDIContainer extends DIContainer, TDIContainerTestClone extends DIContainerTestClone<any, TDIContainer>>(contract: ApiContract<TDef>, testContainer: TDIContainerTestClone, registry: ApiHandlersRegistry<TDef, TDIContainer>): ApiClient<TDef>;
383
362
 
384
- export { ApiClient, type ApiClientDef, ApiContract, ApiHandlersRegistry, type ApiHandlersRegistryDef, type ClientHttpMethodEndpointHandler, type ClientHttpMethodEndpointHandlerInput, type ClientHttpMethodEndpointHandlerOutput, type DIClass, DIContainer, type DILifetime, type DIScope, type ExtractConcatenatedParamNamesFromMethodFirstPath, type ExtractConcatenatedParamNamesFromPath, type ExtractConcatenatedParamNamesFromPathSegments, type ExtractEndpointFromPath, type GenericOnHandlerRegisteredCallback, type HttpMethod, type HttpMethodCallFunc, type HttpMethodCallInput, HttpMethodEndpoint, type HttpMethodEndpointHandler, type HttpMethodEndpointHandlerInput, type HttpMethodEndpointHandlerOutput, HttpMethodEndpointResponse, type HttpMethodEndpointResponses, type HttpResponseObject, HttpStatusCode, type IApiContractDefinition, type IClientContext, type IHttpMethodEndpointDefinition, type IHttpMethodEndpointResponseDefinition, type IRegistrySettings, type LowerCasedHttpMethod, MethodEndpointHandlerRegistryEntry, type MethodFirstPath, type MiddlewareHandler, type MiddlewareHandlerInput, type MiddlewareHandlerInputInternal, type MiddlewareHandlerInputSchemas, type MiddlewareHandlerInternal, MiddlewareHandlersRegistry, MiddlewareHandlersRegistryEntry, MiddlewareHandlersRegistryEntryInternal, type MiddlewarePath, type OnHandlerRegisteredCallback, type OnMiddlewareHandlerRegisteredCallback, type PartialPath, type PartialPathResult, type PathParamFunc, type PrepareRegistryEntryCallback, type TypedPathParams, type ValidateApiContractDefinition, type ValidateHttpMethodEndpointDefinition, apiContract, createClient, createInProcApiClient, createRegistry, endpoint, flatListAllRegistryEntries, isHttpResponseObject, isHttpStatusCode, middleware, partial, partialPathString, register, response, route };
363
+ export { ApiClient, type ApiClientDef, ApiContract, ApiHandlersRegistry, type ApiHandlersRegistryDef, type CapitalizedStringLiteral, type ClientHttpMethodEndpointHandler, type ClientHttpMethodEndpointHandlerInput, type ClientHttpMethodEndpointHandlerOutput, DIContainer, DIContainerBase, DIContainerTestClone, type DILifetime, type DIRegistry, type DIRegistryEntry, type DIScope, type ExtractConcatenatedParamNamesFromMethodFirstPath, type ExtractConcatenatedParamNamesFromPath, type ExtractConcatenatedParamNamesFromPathSegments, type ExtractEndpointFromPath, type GenericOnHandlerRegisteredCallback, type HttpMethod, type HttpMethodCallFunc, type HttpMethodCallInput, HttpMethodEndpoint, type HttpMethodEndpointHandler, type HttpMethodEndpointHandlerInput, type HttpMethodEndpointHandlerOutput, HttpMethodEndpointResponse, type HttpMethodEndpointResponses, type HttpResponseObject, HttpStatusCode, type IApiContractDefinition, type IClientContext, type IHttpMethodEndpointDefinition, type IHttpMethodEndpointResponseDefinition, type IRegistrySettings, type LowerCasedHttpMethod, MethodEndpointHandlerRegistryEntry, type MethodFirstPath, type MiddlewareHandler, type MiddlewareHandlerInput, type MiddlewareHandlerInputInternal, type MiddlewareHandlerInputSchemas, type MiddlewareHandlerInternal, MiddlewareHandlersRegistry, MiddlewareHandlersRegistryEntry, MiddlewareHandlersRegistryEntryInternal, type MiddlewarePath, type OnHandlerRegisteredCallback, type OnMiddlewareHandlerRegisteredCallback, type PartialPath, type PartialPathResult, type PathParamFunc, type PrepareRegistryEntryCallback, type StringLiteral, type TypedPathParams, type ValidateApiContractDefinition, type ValidateHttpMethodEndpointDefinition, apiContract, createClient, createInProcApiClient, createRegistry, endpoint, flatListAllRegistryEntries, isHttpResponseObject, isHttpStatusCode, middleware, partial, partialPathString, register, response, route };
package/dist/index.mjs CHANGED
@@ -156,7 +156,7 @@ class MethodEndpointHandlerRegistryEntry {
156
156
  callback(this);
157
157
  return this;
158
158
  }
159
- _injection = (_dicontainer) => ({});
159
+ _injection = (_diScope) => ({});
160
160
  get injection() {
161
161
  return this._injection;
162
162
  }
@@ -164,28 +164,28 @@ class MethodEndpointHandlerRegistryEntry {
164
164
  this._injection = injection;
165
165
  return this;
166
166
  }
167
- async trigger(data) {
167
+ async trigger(diScope, requestObject) {
168
168
  if (!this._handler) {
169
169
  throw new Error("Handler not set for this endpoint");
170
170
  }
171
171
  let badRequestResponse = null;
172
- const headers = parseRequestDefinitionField(this._methodEndpoint.definition, "headers", data);
172
+ const headers = parseRequestDefinitionField(this._methodEndpoint.definition, "headers", requestObject);
173
173
  if (isHttpResponseObject(headers)) {
174
174
  badRequestResponse = headers;
175
175
  return badRequestResponse;
176
176
  }
177
- const query = parseRequestDefinitionField(this._methodEndpoint.definition, "query", data);
177
+ const query = parseRequestDefinitionField(this._methodEndpoint.definition, "query", requestObject);
178
178
  if (isHttpResponseObject(query)) {
179
179
  badRequestResponse = query;
180
180
  return badRequestResponse;
181
181
  }
182
- const body = parseRequestDefinitionField(this._methodEndpoint.definition, "body", data);
182
+ const body = parseRequestDefinitionField(this._methodEndpoint.definition, "body", requestObject);
183
183
  if (isHttpResponseObject(body)) {
184
184
  badRequestResponse = body;
185
185
  return badRequestResponse;
186
186
  }
187
187
  const path = `/${this._methodEndpoint.pathSegments.map(
188
- (segment) => segment.startsWith(":") ? data.pathParams[segment.slice(1)] ?? "?" : segment
188
+ (segment) => segment.startsWith(":") ? requestObject.pathParams[segment.slice(1)] ?? "?" : segment
189
189
  ).join("/")}`;
190
190
  return await this._handler({
191
191
  method: this._methodEndpoint.method,
@@ -193,32 +193,45 @@ class MethodEndpointHandlerRegistryEntry {
193
193
  genericPath: this._methodEndpoint.genericPath,
194
194
  pathSegments: this._methodEndpoint.pathSegments,
195
195
  headers,
196
- pathParams: data.pathParams,
196
+ pathParams: requestObject.pathParams,
197
197
  query,
198
198
  body,
199
- injected: this._injection(this._dicontainer.createScope())
199
+ injected: this._injection(diScope)
200
200
  });
201
201
  }
202
202
  }
203
- function parseRequestDefinitionField(definition, key, data) {
203
+ function parseRequestDefinitionField(definition, key, requestObject) {
204
204
  if (definition[key]) {
205
- if (!(key in data) || data[key] === null || data[key] === void 0) {
206
- if (!definition[key].safeParse(data[key]).success) {
205
+ if (!(key in requestObject) || requestObject[key] === null || requestObject[key] === void 0) {
206
+ const result2 = definition[key].safeParse(requestObject[key]);
207
+ if (!result2.success) {
208
+ switch (definition[key].type) {
209
+ case "optional":
210
+ if (requestObject[key] === null) {
211
+ return void 0;
212
+ }
213
+ break;
214
+ case "nullable":
215
+ if (requestObject[key] === void 0) {
216
+ return null;
217
+ }
218
+ break;
219
+ }
207
220
  return {
208
221
  code: HttpStatusCode.BadRequest_400,
209
222
  body: `'${key}' is required for this endpoint` + (key === "body" ? ", { 'Content-Type': 'application/json' } header might be missing" : "")
210
223
  };
211
224
  }
212
- return null;
225
+ return result2.data;
213
226
  }
214
- const result = definition[key].safeParse(data[key]);
227
+ const result = definition[key].safeParse(requestObject[key]);
215
228
  if (!result.success) {
216
229
  return {
217
230
  code: HttpStatusCode.BadRequest_400,
218
231
  body: result.error.issues
219
232
  };
220
233
  }
221
- return result.data ?? null;
234
+ return result.data;
222
235
  }
223
236
  return null;
224
237
  }
@@ -233,6 +246,9 @@ function middleware(apiReg, path) {
233
246
  }
234
247
  class MiddlewareHandlersRegistryEntryInternal {
235
248
  _dicontainer;
249
+ get dicontainer() {
250
+ return this._dicontainer;
251
+ }
236
252
  _middlewareGenericPath;
237
253
  get genericPath() {
238
254
  return this._middlewareGenericPath;
@@ -263,39 +279,42 @@ class MiddlewareHandlersRegistryEntryInternal {
263
279
  this._handler = handler;
264
280
  }
265
281
  _injection = (_dicontainer) => ({});
266
- async trigger(data, next) {
282
+ async trigger(diScope, requestObject, next) {
267
283
  let badRequestResponse = null;
268
- const headers = middlewareParseRequestDefinitionField(this._inputSchemas, "headers", data);
284
+ const headers = middlewareParseRequestDefinitionField(this._inputSchemas, "headers", requestObject);
269
285
  if (isHttpResponseObject(headers)) {
270
286
  badRequestResponse = headers;
271
287
  return badRequestResponse;
272
288
  }
273
- const query = middlewareParseRequestDefinitionField(this._inputSchemas, "query", data);
289
+ const query = middlewareParseRequestDefinitionField(this._inputSchemas, "query", requestObject);
274
290
  if (isHttpResponseObject(query)) {
275
291
  badRequestResponse = query;
276
292
  return badRequestResponse;
277
293
  }
278
- const body = middlewareParseRequestDefinitionField(this._inputSchemas, "body", data);
294
+ const body = middlewareParseRequestDefinitionField(this._inputSchemas, "body", requestObject);
279
295
  if (isHttpResponseObject(body)) {
280
296
  badRequestResponse = body;
281
297
  return badRequestResponse;
282
298
  }
283
299
  const { method, pathSegments } = this._splitMiddlewarePath();
284
300
  const path = `/${pathSegments.map(
285
- (segment) => segment.startsWith(":") ? data.pathParams[segment.slice(1)] ?? "?" : segment
301
+ (segment) => segment.startsWith(":") ? requestObject.pathParams[segment.slice(1)] ?? "?" : segment
286
302
  ).join("/")}`;
287
- return await this._handler({
288
- method,
289
- // TODO: might be empty, as middleware can be registered with path only, without method, possible fix: take it from express.request.method
290
- path,
291
- genericPath: this.genericPath,
292
- pathSegments,
293
- headers,
294
- pathParams: data.pathParams,
295
- query,
296
- body,
297
- injected: this._injection(this._dicontainer.createScope())
298
- }, next);
303
+ return await this._handler(
304
+ {
305
+ method,
306
+ // TODO: might be empty, as middleware can be registered with path only, without method, possible fix: take it from express.request.method
307
+ path,
308
+ genericPath: this.genericPath,
309
+ pathSegments,
310
+ headers,
311
+ pathParams: requestObject.pathParams,
312
+ query,
313
+ body,
314
+ injected: this._injection(diScope)
315
+ },
316
+ next
317
+ );
299
318
  }
300
319
  }
301
320
  class MiddlewareHandlersRegistryEntry {
@@ -305,7 +324,7 @@ class MiddlewareHandlersRegistryEntry {
305
324
  this._registry = registry;
306
325
  this._path = path;
307
326
  }
308
- _injection = (_dicontainer) => ({});
327
+ _injection = (_diScope) => ({});
309
328
  get injection() {
310
329
  return this._injection;
311
330
  }
@@ -330,8 +349,13 @@ class MiddlewareHandlersRegistry {
330
349
  this.dicontainer = dicontainer;
331
350
  this._onHandlerRegisteredCallback = callback;
332
351
  }
352
+ _list = [];
353
+ get list() {
354
+ return this._list;
355
+ }
333
356
  register(entry) {
334
357
  if (this._onHandlerRegisteredCallback) {
358
+ this._list.push(entry);
335
359
  this._onHandlerRegisteredCallback(entry);
336
360
  }
337
361
  }
@@ -340,25 +364,38 @@ class MiddlewareHandlersRegistry {
340
364
  this._onHandlerRegisteredCallback = callback;
341
365
  }
342
366
  }
343
- function middlewareParseRequestDefinitionField(inputSchemas, key, data) {
367
+ function middlewareParseRequestDefinitionField(inputSchemas, key, requestObject) {
344
368
  if (inputSchemas[key]) {
345
- if (!(key in data) || data[key] === null || data[key] === void 0) {
346
- if (!inputSchemas[key].safeParse(data[key]).success) {
369
+ if (!(key in requestObject) || requestObject[key] === null || requestObject[key] === void 0) {
370
+ const result2 = inputSchemas[key].safeParse(requestObject[key]);
371
+ if (!result2.success) {
372
+ switch (inputSchemas[key].type) {
373
+ case "optional":
374
+ if (requestObject[key] === null) {
375
+ return void 0;
376
+ }
377
+ break;
378
+ case "nullable":
379
+ if (requestObject[key] === void 0) {
380
+ return null;
381
+ }
382
+ break;
383
+ }
347
384
  return {
348
385
  code: HttpStatusCode.BadRequest_400,
349
386
  body: `'${key}' is required for this endpoint` + (key === "body" ? ", { 'Content-Type': 'application/json' } header might be missing" : "")
350
387
  };
351
388
  }
352
- return null;
389
+ return result2.data;
353
390
  }
354
- const result = inputSchemas[key].safeParse(data[key]);
391
+ const result = inputSchemas[key].safeParse(requestObject[key]);
355
392
  if (!result.success) {
356
393
  return {
357
394
  code: HttpStatusCode.BadRequest_400,
358
395
  body: result.error.issues
359
396
  };
360
397
  }
361
- return result.data ?? null;
398
+ return result.data;
362
399
  }
363
400
  return null;
364
401
  }
@@ -526,12 +563,12 @@ class InnerApiClient {
526
563
  delete currObj[key];
527
564
  InnerApiClient._initialize(client, val, clientGenericHandler);
528
565
  } else if (val instanceof HttpMethodEndpoint) {
529
- currObj[key] = (req) => {
566
+ currObj[key] = (requestObject) => {
530
567
  const pathParams = { ...client.__CONTEXT__.pathParameters };
531
568
  client.__CONTEXT__ = InnerApiClient._initNewContext();
532
- const headers = clientParseRequestDefinitionField(val.definition, "headers", req);
533
- const query = clientParseRequestDefinitionField(val.definition, "query", req);
534
- const body = clientParseRequestDefinitionField(val.definition, "body", req);
569
+ const headers = clientParseRequestDefinitionField(val.definition, "headers", requestObject);
570
+ const query = clientParseRequestDefinitionField(val.definition, "query", requestObject);
571
+ const body = clientParseRequestDefinitionField(val.definition, "body", requestObject);
535
572
  const path = `/${val.pathSegments.map(
536
573
  (segment) => segment.startsWith(":") ? pathParams[segment.slice(1)] ?? "?" : segment
537
574
  ).join("/")}`;
@@ -553,107 +590,153 @@ class InnerApiClient {
553
590
  }
554
591
  }
555
592
  const ApiClient = InnerApiClient;
556
- function clientParseRequestDefinitionField(definition, key, data) {
593
+ function clientParseRequestDefinitionField(definition, key, requestObject) {
557
594
  if (definition[key]) {
558
- if (!(key in data) || data[key] === null || data[key] === void 0) {
559
- if (!definition[key].safeParse(data[key]).success) {
560
- throw new Error(`${key} are required for this endpoint`);
595
+ if (!(key in requestObject) || requestObject[key] === null || requestObject[key] === void 0) {
596
+ const result2 = definition[key].safeParse(requestObject[key]);
597
+ if (!result2.success) {
598
+ switch (definition[key].type) {
599
+ case "optional":
600
+ if (requestObject[key] === null) {
601
+ return void 0;
602
+ }
603
+ break;
604
+ case "nullable":
605
+ if (requestObject[key] === void 0) {
606
+ return null;
607
+ }
608
+ break;
609
+ }
610
+ throw new Error(`'${key}' is required for this endpoint`);
561
611
  }
562
- return null;
612
+ return result2.data;
563
613
  }
564
- const result = definition[key].safeParse(data[key]);
614
+ const result = definition[key].safeParse(requestObject[key]);
565
615
  if (!result.success) {
566
616
  throw new Error(`Validation for '${key}' failed`, { cause: result.error });
567
617
  }
568
- return result.data ?? null;
618
+ return result.data;
569
619
  }
570
620
  return null;
571
621
  }
572
622
 
573
- class DIContainer {
574
- registry = /* @__PURE__ */ new Map();
575
- singletons = /* @__PURE__ */ new Map();
576
- proxy;
577
- /**
578
- * The constructor returns a Proxy. This is the runtime magic that intercepts
579
- * calls to methods like `getLoggerService()`. It parses the method name,
580
- * finds the corresponding service class in the registry, and resolves it.
581
- */
582
- constructor() {
583
- this.proxy = new Proxy(this, {
584
- get: (target, prop, receiver) => {
585
- if (typeof prop === "string" && prop.startsWith("get")) {
586
- const serviceName = prop.substring(3);
587
- if (target.registry.has(serviceName)) {
588
- return () => target.resolveByName(serviceName);
623
+ class DIContainerBase {
624
+ _map = /* @__PURE__ */ new Map();
625
+ _singletonInstances = /* @__PURE__ */ new Map();
626
+ createScope() {
627
+ const proxy = new Proxy({
628
+ _map: this._map,
629
+ _singletonInstances: this._singletonInstances,
630
+ _scopedInstances: /* @__PURE__ */ new Map(),
631
+ _isBuildingSingleton: false
632
+ }, {
633
+ get: (target, prop) => {
634
+ if (prop.startsWith("get")) {
635
+ const serviceName = prop.slice(3);
636
+ if (target._map.has(serviceName)) {
637
+ const entry = target._map.get(serviceName);
638
+ switch (entry.lifetime) {
639
+ case "transient":
640
+ return () => {
641
+ if (target._isBuildingSingleton) {
642
+ throw new Error(`Cannot resolve transient service '${serviceName}' while building a singleton`);
643
+ }
644
+ return entry.factory(proxy);
645
+ };
646
+ case "scoped":
647
+ return () => {
648
+ if (target._isBuildingSingleton) {
649
+ throw new Error(`Cannot resolve scoped service '${serviceName}' while building a singleton`);
650
+ }
651
+ if (!target._scopedInstances.has(serviceName)) {
652
+ const instance = entry.factory(proxy);
653
+ target._scopedInstances.set(serviceName, instance);
654
+ }
655
+ return target._scopedInstances.get(serviceName);
656
+ };
657
+ case "singleton":
658
+ return () => {
659
+ if (!target._singletonInstances.has(serviceName)) {
660
+ target._isBuildingSingleton = true;
661
+ try {
662
+ const instance = entry.factory(proxy);
663
+ target._singletonInstances.set(serviceName, instance);
664
+ } finally {
665
+ target._isBuildingSingleton = false;
666
+ }
667
+ }
668
+ return target._singletonInstances.get(serviceName);
669
+ };
670
+ default:
671
+ throw new Error(`Unsupported lifetime: ${entry.lifetime}`);
672
+ }
589
673
  } else {
590
674
  throw new Error(`Service not registered: ${serviceName}`);
591
675
  }
592
676
  }
593
- return Reflect.get(target, prop, receiver);
677
+ throw new Error(`Property access denied by Proxy: ${String(prop)}`);
594
678
  }
595
679
  });
596
- return this.proxy;
680
+ return proxy;
597
681
  }
598
- /**
599
- * Registers a service with explicit service name (fully type-safe).
600
- */
601
- register(serviceNameLiteral, factory, lifetime = "transient") {
602
- this.registry.set(serviceNameLiteral, { factory, lifetime });
682
+ }
683
+ class DIContainer extends DIContainerBase {
684
+ register(serviceNameCapitalizedLiteral, factory, lifetime) {
685
+ const entry = { factory, lifetime };
686
+ this._map.set(serviceNameCapitalizedLiteral, entry);
603
687
  return this;
604
688
  }
605
- /**
606
- * Resolves a service by service name.
607
- */
608
- resolveByName(serviceName) {
609
- const registration = this.registry.get(serviceName);
610
- if (!registration) {
611
- throw new Error(`Service not registered: ${serviceName}`);
612
- }
613
- if (registration.lifetime === "singleton") {
614
- if (!this.singletons.has(serviceName)) {
615
- const instance = registration.factory(this.proxy);
616
- this.singletons.set(serviceName, instance);
617
- }
618
- return this.singletons.get(serviceName);
619
- }
620
- return registration.factory(this.proxy);
689
+ createTestClone() {
690
+ return new DIContainerTestClone(this);
621
691
  }
622
- /**
623
- * Creates a scoped container with typed method access to services.
624
- */
625
- createScope() {
626
- const scope = {};
627
- return new Proxy(scope, {
628
- get: (target, prop) => {
629
- if (typeof prop === "string" && prop.startsWith("get")) {
630
- const serviceName = prop.substring(3);
631
- if (this.registry.has(serviceName)) {
632
- const registration = this.registry.get(serviceName);
633
- return () => {
634
- if (registration?.lifetime === "transient") {
635
- return this.resolveByName(serviceName);
636
- }
637
- const cacheKey = `_cached_${serviceName}`;
638
- if (!target[cacheKey]) {
639
- target[cacheKey] = this.resolveByName(serviceName);
640
- }
641
- return target[cacheKey];
642
- };
643
- } else {
644
- throw new Error(`Service not registered: ${serviceName}`);
645
- }
646
- }
647
- return Reflect.get(target, prop);
648
- }
692
+ }
693
+ class DIContainerTestClone extends DIContainerBase {
694
+ constructor(original) {
695
+ super();
696
+ original["_map"].forEach((value, key) => {
697
+ this._map.set(key, {
698
+ factory: (_scope) => {
699
+ throw new Error(`Service registration not overridden: ${key}`);
700
+ },
701
+ lifetime: value.lifetime
702
+ });
649
703
  });
650
704
  }
651
- createTestClone() {
652
- const clone = new DIContainer();
653
- return clone;
705
+ override(serviceNameLiteral, factory) {
706
+ const registration = this._map.get(serviceNameLiteral);
707
+ if (!registration) {
708
+ throw new Error(`Service not registered: ${serviceNameLiteral}`);
709
+ }
710
+ this._map.set(serviceNameLiteral, { factory, lifetime: registration.lifetime });
711
+ return this;
654
712
  }
655
713
  }
656
714
 
715
+ function execMiddleware(diScope, list, input) {
716
+ const queue = [...list];
717
+ const next = async () => {
718
+ const current = queue.shift();
719
+ if (!current) {
720
+ return;
721
+ }
722
+ const result = await current.trigger(
723
+ diScope,
724
+ {
725
+ headers: input.headers,
726
+ pathParams: input.pathParams,
727
+ body: input.body,
728
+ query: input.query
729
+ },
730
+ next
731
+ );
732
+ if (result) {
733
+ return result;
734
+ }
735
+ return next();
736
+ };
737
+ return next();
738
+ }
739
+
657
740
  function createInProcApiClient(contract, testContainer, registry) {
658
741
  const testApiReg = createRegistry(testContainer, contract, {
659
742
  handlerRegisteredCallback: (_entry) => {
@@ -661,6 +744,10 @@ function createInProcApiClient(contract, testContainer, registry) {
661
744
  middlewareHandlerRegisteredCallback: (_entry) => {
662
745
  }
663
746
  });
747
+ registry._middlewareRegistry.list.forEach((mwEntry) => {
748
+ testApiReg._middlewareRegistry.register(mwEntry);
749
+ });
750
+ const mwReg = testApiReg._middlewareRegistry;
664
751
  flatListAllRegistryEntries(registry).forEach((entry) => {
665
752
  if (!entry.handler) {
666
753
  return;
@@ -669,16 +756,21 @@ function createInProcApiClient(contract, testContainer, registry) {
669
756
  rt.inject(entry.injection).register(entry.handler);
670
757
  });
671
758
  const client = createClient(contract, async (input) => {
759
+ const diScope = testContainer.createScope();
760
+ const haltExecResponse = await execMiddleware(diScope, mwReg.list, input);
761
+ if (haltExecResponse) {
762
+ return haltExecResponse;
763
+ }
672
764
  const rt = route(testApiReg, `${input.method} ${input.genericPath}`);
673
- const result = rt.trigger({
765
+ const result = await rt.trigger(diScope, {
674
766
  headers: input.headers,
675
767
  pathParams: input.pathParams,
676
- body: input.body ?? {},
677
- query: input.query ?? {}
768
+ body: input.body,
769
+ query: input.query
678
770
  });
679
771
  return result;
680
772
  });
681
773
  return client;
682
774
  }
683
775
 
684
- export { ApiClient, ApiContract, ApiHandlersRegistry, DIContainer, HttpMethodEndpoint, HttpMethodEndpointResponse, HttpStatusCode, MethodEndpointHandlerRegistryEntry, MiddlewareHandlersRegistry, MiddlewareHandlersRegistryEntry, MiddlewareHandlersRegistryEntryInternal, apiContract, createClient, createInProcApiClient, createRegistry, endpoint, flatListAllRegistryEntries, isHttpResponseObject, isHttpStatusCode, middleware, partial, partialPathString, register, response, route };
776
+ export { ApiClient, ApiContract, ApiHandlersRegistry, DIContainer, DIContainerBase, DIContainerTestClone, HttpMethodEndpoint, HttpMethodEndpointResponse, HttpStatusCode, MethodEndpointHandlerRegistryEntry, MiddlewareHandlersRegistry, MiddlewareHandlersRegistryEntry, MiddlewareHandlersRegistryEntryInternal, apiContract, createClient, createInProcApiClient, createRegistry, endpoint, flatListAllRegistryEntries, isHttpResponseObject, isHttpStatusCode, middleware, partial, partialPathString, register, response, route };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@congruent-stack/congruent-api",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.9.0-rc.0",
5
5
  "description": "Typescript schema-first tooling for agnostic REST APIs.",
6
6
  "keywords": [],
7
7
  "author": "congruent-stack",