@congruent-stack/congruent-api 0.6.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;