@congruent-stack/congruent-api 0.3.3 → 0.6.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.mjs CHANGED
@@ -1,5 +1,684 @@
1
- function getHelloWorldMessage(name) {
2
- return `congruent-api: Hello, ${name}!`;
1
+ function endpoint(definition) {
2
+ return new HttpMethodEndpoint(definition);
3
+ }
4
+ class HttpMethodEndpoint {
5
+ _definition;
6
+ get definition() {
7
+ return this._definition;
8
+ }
9
+ _pathSegments = [];
10
+ get pathSegments() {
11
+ return this._pathSegments;
12
+ }
13
+ _cachedGenericPath = null;
14
+ get genericPath() {
15
+ if (!this._cachedGenericPath) {
16
+ this._cachedGenericPath = `/${this._pathSegments.join("/")}`;
17
+ }
18
+ return this._cachedGenericPath;
19
+ }
20
+ _method = null;
21
+ get method() {
22
+ return this._method;
23
+ }
24
+ get lowerCasedMethod() {
25
+ return this._method.toLowerCase();
26
+ }
27
+ constructor(definition) {
28
+ this._definition = definition;
29
+ }
30
+ /** @internal */
31
+ _cloneWith(path, method) {
32
+ const result = new HttpMethodEndpoint({
33
+ headers: !!this._definition.headers ? this._definition.headers.clone() : void 0,
34
+ query: !!this._definition.query ? this._definition.query.clone() : void 0,
35
+ body: !!this._definition.body ? this._definition.body.clone() : void 0,
36
+ responses: Object.fromEntries(
37
+ Object.entries(this._definition.responses).map(([status, response]) => [
38
+ status,
39
+ response._clone()
40
+ ])
41
+ )
42
+ });
43
+ result._pathSegments = path;
44
+ result._method = method;
45
+ return result;
46
+ }
47
+ }
48
+
49
+ function apiContract(definition) {
50
+ return new ApiContract(definition);
51
+ }
52
+ function isApiContractDefinition(obj) {
53
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj) && Object.values(obj).every(
54
+ (value) => value instanceof HttpMethodEndpoint || isApiContractDefinition(value)
55
+ );
56
+ }
57
+ class ApiContract {
58
+ definition;
59
+ constructor(definition) {
60
+ this.definition = definition;
61
+ }
62
+ cloneInitDef() {
63
+ return ApiContract._deepCloneInitDef(this.definition, []);
64
+ }
65
+ static _deepCloneInitDef(definition, path) {
66
+ const result = {};
67
+ for (const key in definition) {
68
+ const value = definition[key];
69
+ if (value instanceof HttpMethodEndpoint) {
70
+ result[key] = value._cloneWith(path, key);
71
+ } else if (isApiContractDefinition(value)) {
72
+ result[key] = ApiContract._deepCloneInitDef(value, [...path, key]);
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ }
78
+
79
+ function response(definition) {
80
+ return new HttpMethodEndpointResponse(definition);
81
+ }
82
+ class HttpMethodEndpointResponse {
83
+ _definition;
84
+ get definition() {
85
+ return this._definition;
86
+ }
87
+ constructor(definition) {
88
+ this._definition = definition;
89
+ }
90
+ /** @internal */
91
+ _clone() {
92
+ return new HttpMethodEndpointResponse(this._definition);
93
+ }
94
+ }
95
+
96
+ var HttpStatusCode = /* @__PURE__ */ ((HttpStatusCode2) => {
97
+ HttpStatusCode2[HttpStatusCode2["Continue_100"] = 100] = "Continue_100";
98
+ HttpStatusCode2[HttpStatusCode2["SwitchingProtocols_101"] = 101] = "SwitchingProtocols_101";
99
+ HttpStatusCode2[HttpStatusCode2["OK_200"] = 200] = "OK_200";
100
+ HttpStatusCode2[HttpStatusCode2["Created_201"] = 201] = "Created_201";
101
+ HttpStatusCode2[HttpStatusCode2["Accepted_202"] = 202] = "Accepted_202";
102
+ HttpStatusCode2[HttpStatusCode2["NoContent_204"] = 204] = "NoContent_204";
103
+ HttpStatusCode2[HttpStatusCode2["MultipleChoices_300"] = 300] = "MultipleChoices_300";
104
+ HttpStatusCode2[HttpStatusCode2["MovedPermanently_301"] = 301] = "MovedPermanently_301";
105
+ HttpStatusCode2[HttpStatusCode2["Found_302"] = 302] = "Found_302";
106
+ HttpStatusCode2[HttpStatusCode2["SeeOther_303"] = 303] = "SeeOther_303";
107
+ HttpStatusCode2[HttpStatusCode2["NotModified_304"] = 304] = "NotModified_304";
108
+ HttpStatusCode2[HttpStatusCode2["BadRequest_400"] = 400] = "BadRequest_400";
109
+ HttpStatusCode2[HttpStatusCode2["Unauthorized_401"] = 401] = "Unauthorized_401";
110
+ HttpStatusCode2[HttpStatusCode2["Forbidden_403"] = 403] = "Forbidden_403";
111
+ HttpStatusCode2[HttpStatusCode2["NotFound_404"] = 404] = "NotFound_404";
112
+ HttpStatusCode2[HttpStatusCode2["Conflict_409"] = 409] = "Conflict_409";
113
+ HttpStatusCode2[HttpStatusCode2["InternalServerError_500"] = 500] = "InternalServerError_500";
114
+ HttpStatusCode2[HttpStatusCode2["NotImplemented_501"] = 501] = "NotImplemented_501";
115
+ HttpStatusCode2[HttpStatusCode2["BadGateway_502"] = 502] = "BadGateway_502";
116
+ HttpStatusCode2[HttpStatusCode2["ServiceUnavailable_503"] = 503] = "ServiceUnavailable_503";
117
+ HttpStatusCode2[HttpStatusCode2["GatewayTimeout_504"] = 504] = "GatewayTimeout_504";
118
+ return HttpStatusCode2;
119
+ })(HttpStatusCode || {});
120
+ function isHttpStatusCode(value) {
121
+ return value in HttpStatusCode;
122
+ }
123
+
124
+ function isHttpResponseObject(obj) {
125
+ return obj !== null && typeof obj === "object" && "code" in obj && isHttpStatusCode(obj.code);
126
+ }
127
+
128
+ class MethodEndpointHandlerRegistryEntry {
129
+ _methodEndpoint;
130
+ get methodEndpoint() {
131
+ return this._methodEndpoint;
132
+ }
133
+ _dicontainer;
134
+ get dicontainer() {
135
+ return this._dicontainer;
136
+ }
137
+ constructor(methodEndpoint, dicontainer) {
138
+ this._methodEndpoint = methodEndpoint;
139
+ this._dicontainer = dicontainer;
140
+ }
141
+ _handler = null;
142
+ get handler() {
143
+ return this._handler;
144
+ }
145
+ register(handler) {
146
+ this._handler = handler;
147
+ if (this._onHandlerRegisteredCallback) {
148
+ this._onHandlerRegisteredCallback(this);
149
+ }
150
+ }
151
+ _onHandlerRegisteredCallback = null;
152
+ _onHandlerRegistered(callback) {
153
+ this._onHandlerRegisteredCallback = callback;
154
+ }
155
+ prepare(callback) {
156
+ callback(this);
157
+ return this;
158
+ }
159
+ _injection = (_dicontainer) => ({});
160
+ get injection() {
161
+ return this._injection;
162
+ }
163
+ inject(injection) {
164
+ this._injection = injection;
165
+ return this;
166
+ }
167
+ async trigger(data) {
168
+ if (!this._handler) {
169
+ throw new Error("Handler not set for this endpoint");
170
+ }
171
+ let badRequestResponse = null;
172
+ const headers = parseRequestDefinitionField(this._methodEndpoint.definition, "headers", data);
173
+ if (isHttpResponseObject(headers)) {
174
+ badRequestResponse = headers;
175
+ return badRequestResponse;
176
+ }
177
+ const query = parseRequestDefinitionField(this._methodEndpoint.definition, "query", data);
178
+ if (isHttpResponseObject(query)) {
179
+ badRequestResponse = query;
180
+ return badRequestResponse;
181
+ }
182
+ const body = parseRequestDefinitionField(this._methodEndpoint.definition, "body", data);
183
+ if (isHttpResponseObject(body)) {
184
+ badRequestResponse = body;
185
+ return badRequestResponse;
186
+ }
187
+ const path = `/${this._methodEndpoint.pathSegments.map(
188
+ (segment) => segment.startsWith(":") ? data.pathParams[segment.slice(1)] ?? "?" : segment
189
+ ).join("/")}`;
190
+ return await this._handler({
191
+ method: this._methodEndpoint.method,
192
+ path,
193
+ genericPath: this._methodEndpoint.genericPath,
194
+ pathSegments: this._methodEndpoint.pathSegments,
195
+ headers,
196
+ pathParams: data.pathParams,
197
+ query,
198
+ body,
199
+ injected: this._injection(this._dicontainer.createScope())
200
+ });
201
+ }
202
+ }
203
+ function parseRequestDefinitionField(definition, key, data) {
204
+ if (definition[key]) {
205
+ if (!(key in data) || data[key] === null || data[key] === void 0) {
206
+ if (!definition[key].safeParse(data[key]).success) {
207
+ return {
208
+ code: HttpStatusCode.BadRequest_400,
209
+ body: `'${key}' is required for this endpoint` + (key === "body" ? ", { 'Content-Type': 'application/json' } header might be missing" : "")
210
+ };
211
+ }
212
+ return null;
213
+ }
214
+ const result = definition[key].safeParse(data[key]);
215
+ if (!result.success) {
216
+ return {
217
+ code: HttpStatusCode.BadRequest_400,
218
+ body: result.error.issues
219
+ };
220
+ }
221
+ return result.data ?? null;
222
+ }
223
+ return null;
224
+ }
225
+
226
+ function middleware(apiReg, path) {
227
+ const reg = apiReg._middlewareRegistry;
228
+ const entry = new MiddlewareHandlersRegistryEntry(
229
+ reg,
230
+ path
231
+ );
232
+ return entry;
233
+ }
234
+ class MiddlewareHandlersRegistryEntryInternal {
235
+ _dicontainer;
236
+ _middlewareGenericPath;
237
+ get genericPath() {
238
+ return this._middlewareGenericPath;
239
+ }
240
+ _splitMiddlewarePath() {
241
+ const splitResult = this._middlewareGenericPath.split(" ");
242
+ let method = "";
243
+ let pathSegments = [];
244
+ if (splitResult.length === 2) {
245
+ method = splitResult[0].trim();
246
+ if (method === "") {
247
+ throw new Error(`Invalid middleware path format: "${this._middlewareGenericPath}". HTTP method is empty.`);
248
+ }
249
+ pathSegments = splitResult[1].split("/").map((segment) => segment.trim()).filter((segment) => segment !== "");
250
+ }
251
+ return {
252
+ method,
253
+ pathSegments
254
+ };
255
+ }
256
+ _inputSchemas;
257
+ _handler;
258
+ constructor(diContainer, middlewarePath, inputSchemas, injection, handler) {
259
+ this._dicontainer = diContainer;
260
+ this._middlewareGenericPath = middlewarePath;
261
+ this._inputSchemas = inputSchemas;
262
+ this._injection = injection;
263
+ this._handler = handler;
264
+ }
265
+ _injection = (_dicontainer) => ({});
266
+ async trigger(data, next) {
267
+ let badRequestResponse = null;
268
+ const headers = middlewareParseRequestDefinitionField(this._inputSchemas, "headers", data);
269
+ if (isHttpResponseObject(headers)) {
270
+ badRequestResponse = headers;
271
+ return badRequestResponse;
272
+ }
273
+ const query = middlewareParseRequestDefinitionField(this._inputSchemas, "query", data);
274
+ if (isHttpResponseObject(query)) {
275
+ badRequestResponse = query;
276
+ return badRequestResponse;
277
+ }
278
+ const body = middlewareParseRequestDefinitionField(this._inputSchemas, "body", data);
279
+ if (isHttpResponseObject(body)) {
280
+ badRequestResponse = body;
281
+ return badRequestResponse;
282
+ }
283
+ const { method, pathSegments } = this._splitMiddlewarePath();
284
+ const path = `/${pathSegments.map(
285
+ (segment) => segment.startsWith(":") ? data.pathParams[segment.slice(1)] ?? "?" : segment
286
+ ).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);
299
+ }
300
+ }
301
+ class MiddlewareHandlersRegistryEntry {
302
+ _registry;
303
+ _path;
304
+ constructor(registry, path) {
305
+ this._registry = registry;
306
+ this._path = path;
307
+ }
308
+ _injection = (_dicontainer) => ({});
309
+ get injection() {
310
+ return this._injection;
311
+ }
312
+ inject(injection) {
313
+ this._injection = injection;
314
+ return this;
315
+ }
316
+ register(inputSchemas, handler) {
317
+ const internalEntry = new MiddlewareHandlersRegistryEntryInternal(
318
+ this._registry.dicontainer,
319
+ this._path,
320
+ inputSchemas,
321
+ this._injection,
322
+ handler
323
+ );
324
+ this._registry.register(internalEntry);
325
+ }
326
+ }
327
+ class MiddlewareHandlersRegistry {
328
+ dicontainer;
329
+ constructor(dicontainer, callback) {
330
+ this.dicontainer = dicontainer;
331
+ this._onHandlerRegisteredCallback = callback;
332
+ }
333
+ register(entry) {
334
+ if (this._onHandlerRegisteredCallback) {
335
+ this._onHandlerRegisteredCallback(entry);
336
+ }
337
+ }
338
+ _onHandlerRegisteredCallback = null;
339
+ _onHandlerRegistered(callback) {
340
+ this._onHandlerRegisteredCallback = callback;
341
+ }
342
+ }
343
+ function middlewareParseRequestDefinitionField(inputSchemas, key, data) {
344
+ if (inputSchemas[key]) {
345
+ if (!(key in data) || data[key] === null || data[key] === void 0) {
346
+ if (!inputSchemas[key].safeParse(data[key]).success) {
347
+ return {
348
+ code: HttpStatusCode.BadRequest_400,
349
+ body: `'${key}' is required for this endpoint` + (key === "body" ? ", { 'Content-Type': 'application/json' } header might be missing" : "")
350
+ };
351
+ }
352
+ return null;
353
+ }
354
+ const result = inputSchemas[key].safeParse(data[key]);
355
+ if (!result.success) {
356
+ return {
357
+ code: HttpStatusCode.BadRequest_400,
358
+ body: result.error.issues
359
+ };
360
+ }
361
+ return result.data ?? null;
362
+ }
363
+ return null;
364
+ }
365
+
366
+ function flatListAllRegistryEntries(registry) {
367
+ const entries = [];
368
+ for (const key of Object.keys(registry)) {
369
+ if (key === "_middlewareRegistry") {
370
+ continue;
371
+ }
372
+ const value = registry[key];
373
+ if (value instanceof MethodEndpointHandlerRegistryEntry) {
374
+ entries.push(value);
375
+ } else if (typeof value === "object" && value !== null) {
376
+ const innerEntries = flatListAllRegistryEntries(value);
377
+ for (const entry of innerEntries) {
378
+ entries.push(entry);
379
+ }
380
+ }
381
+ }
382
+ return entries;
383
+ }
384
+ function createRegistry(diContainer, contract, settings) {
385
+ return new ApiHandlersRegistry(diContainer, contract, settings);
386
+ }
387
+ class InnerApiHandlersRegistry {
388
+ /** @internal */
389
+ _middlewareRegistry;
390
+ constructor(dicontainer, contract, settings) {
391
+ const initializedDefinition = contract.cloneInitDef();
392
+ const proto = { ...InnerApiHandlersRegistry.prototype };
393
+ Object.assign(proto, Object.getPrototypeOf(initializedDefinition));
394
+ Object.setPrototypeOf(this, proto);
395
+ Object.assign(this, initializedDefinition);
396
+ InnerApiHandlersRegistry._initialize(this, settings.handlerRegisteredCallback, dicontainer);
397
+ this._middlewareRegistry = new MiddlewareHandlersRegistry(dicontainer, settings.middlewareHandlerRegisteredCallback);
398
+ }
399
+ static _initialize(currObj, callback, dicontainer) {
400
+ for (const key of Object.keys(currObj)) {
401
+ if (key === "_middlewareRegistry") {
402
+ continue;
403
+ }
404
+ const value = currObj[key];
405
+ if (value instanceof HttpMethodEndpoint) {
406
+ const entry = new MethodEndpointHandlerRegistryEntry(value, dicontainer);
407
+ entry._onHandlerRegistered(callback);
408
+ currObj[key] = entry;
409
+ } else if (typeof value === "object" && value !== null) {
410
+ InnerApiHandlersRegistry._initialize(value, callback, dicontainer);
411
+ }
412
+ }
413
+ }
414
+ }
415
+ const ApiHandlersRegistry = InnerApiHandlersRegistry;
416
+
417
+ function partial(apiReg, path) {
418
+ const pathSegments = path.split("/").filter((segment) => segment.length > 0);
419
+ let current = apiReg;
420
+ for (const segment of pathSegments) {
421
+ if (current[segment] instanceof MethodEndpointHandlerRegistryEntry) {
422
+ throw new Error(`Path "${path}" is not partial`);
423
+ } else if (typeof current[segment] === "object") {
424
+ current = current[segment];
425
+ } else {
426
+ throw new Error(`Path segment "${segment}" not found in API handlers registry`);
427
+ }
428
+ }
429
+ return current;
430
+ }
431
+
432
+ function route(apiReg, path) {
433
+ const pathStr = path;
434
+ const spaceIndex = pathStr.indexOf(" ");
435
+ if (spaceIndex === -1) {
436
+ throw new Error(`Invalid path format: ${pathStr}. Expected format: "HTTP_METHOD /path"`);
437
+ }
438
+ const method = pathStr.substring(0, spaceIndex);
439
+ const urlPath = pathStr.substring(spaceIndex + 1);
440
+ const pathSegments = urlPath.split("/").filter((segment) => segment.length > 0);
441
+ let current = apiReg;
442
+ for (const segment of pathSegments) {
443
+ if (current[segment] instanceof MethodEndpointHandlerRegistryEntry) {
444
+ current = current[segment];
445
+ } else if (typeof current[segment] === "object") {
446
+ current = current[segment];
447
+ } else {
448
+ throw new Error(`Path segment "${segment}" not found in API handlers registry`);
449
+ }
450
+ }
451
+ if (!(method in current)) {
452
+ throw new Error(`Method "${method}" not found for path "${pathSegments.join("/")}"`);
453
+ }
454
+ if (!(current[method] instanceof MethodEndpointHandlerRegistryEntry)) {
455
+ throw new Error(`Method "${method}" is not a valid endpoint handler for path "${pathSegments.join("/")}"`);
456
+ }
457
+ current = current[method];
458
+ return current;
459
+ }
460
+
461
+ function register(apiRegOrEndpoint, pathOrHandler, handler) {
462
+ if (arguments.length === 3 && handler !== void 0) {
463
+ registerMethodPathHandler(
464
+ apiRegOrEndpoint,
465
+ pathOrHandler,
466
+ handler
467
+ );
468
+ } else if (arguments.length === 2) {
469
+ registerEntryHandler(
470
+ apiRegOrEndpoint,
471
+ pathOrHandler
472
+ );
473
+ } else {
474
+ throw new Error("Invalid number of arguments provided to register function");
475
+ }
476
+ }
477
+ function registerMethodPathHandler(apiReg, path, handler) {
478
+ const endpointEntry = route(apiReg, path);
479
+ return registerEntryHandler(endpointEntry, handler);
480
+ }
481
+ function registerEntryHandler(endpointEntry, handler) {
482
+ endpointEntry.register(handler);
483
+ }
484
+
485
+ function partialPathString(_apiReg, path) {
486
+ return path;
487
+ }
488
+
489
+ function createClient(contract, clientGenericHandler) {
490
+ const apiClient = new ApiClient(contract, clientGenericHandler);
491
+ return apiClient;
492
+ }
493
+ class InnerApiClient {
494
+ // TODO: if making __CONTEXT__ private member, the following compilation error occurs:
495
+ // "Property '__CONTEXT__' of exported anonymous class type may not be private or protected.ts(4094)"
496
+ // Reason: exported anonymous classes can't have private or protected members if declaration emit is enabled
497
+ // Source: https://stackoverflow.com/questions/55242196/typescript-allows-to-use-proper-multiple-inheritance-with-mixins-but-fails-to-c
498
+ /** @internal */
499
+ __CONTEXT__;
500
+ constructor(contract, clientGenericHandler) {
501
+ const initializedDefinition = contract.cloneInitDef();
502
+ const proto = { ...InnerApiClient.prototype };
503
+ Object.assign(proto, Object.getPrototypeOf(initializedDefinition));
504
+ Object.setPrototypeOf(this, proto);
505
+ Object.assign(this, initializedDefinition);
506
+ InnerApiClient._initialize(this, this, clientGenericHandler);
507
+ this.__CONTEXT__ = InnerApiClient._initNewContext();
508
+ }
509
+ static _initNewContext() {
510
+ return {
511
+ pathParameters: {}
512
+ };
513
+ }
514
+ static _initialize(client, currObj, clientGenericHandler) {
515
+ for (const key of Object.keys(currObj)) {
516
+ if (key === "__CONTEXT__") {
517
+ continue;
518
+ }
519
+ const val = currObj[key];
520
+ if (key.startsWith(":")) {
521
+ const paramName = key.slice(1);
522
+ currObj[paramName] = ((value) => {
523
+ client.__CONTEXT__.pathParameters[paramName] = value.toString();
524
+ return val;
525
+ });
526
+ delete currObj[key];
527
+ InnerApiClient._initialize(client, val, clientGenericHandler);
528
+ } else if (val instanceof HttpMethodEndpoint) {
529
+ currObj[key] = (req) => {
530
+ const pathParams = { ...client.__CONTEXT__.pathParameters };
531
+ 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);
535
+ const path = `/${val.pathSegments.map(
536
+ (segment) => segment.startsWith(":") ? pathParams[segment.slice(1)] ?? "?" : segment
537
+ ).join("/")}`;
538
+ return clientGenericHandler({
539
+ method: val.method,
540
+ pathSegments: val.pathSegments,
541
+ genericPath: val.genericPath,
542
+ path,
543
+ headers,
544
+ pathParams,
545
+ query,
546
+ body
547
+ });
548
+ };
549
+ } else if (typeof val === "object" && val !== null) {
550
+ InnerApiClient._initialize(client, val, clientGenericHandler);
551
+ }
552
+ }
553
+ }
554
+ }
555
+ const ApiClient = InnerApiClient;
556
+ function clientParseRequestDefinitionField(definition, key, data) {
557
+ 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`);
561
+ }
562
+ return null;
563
+ }
564
+ const result = definition[key].safeParse(data[key]);
565
+ if (!result.success) {
566
+ throw new Error(`Validation for '${key}' failed`, { cause: result.error });
567
+ }
568
+ return result.data ?? null;
569
+ }
570
+ return null;
571
+ }
572
+
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);
589
+ } else {
590
+ throw new Error(`Service not registered: ${serviceName}`);
591
+ }
592
+ }
593
+ return Reflect.get(target, prop, receiver);
594
+ }
595
+ });
596
+ return this.proxy;
597
+ }
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 });
603
+ return this;
604
+ }
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);
621
+ }
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
+ }
649
+ });
650
+ }
651
+ createTestClone() {
652
+ const clone = new DIContainer();
653
+ return clone;
654
+ }
655
+ }
656
+
657
+ function createInProcApiClient(contract, testContainer, registry) {
658
+ const testApiReg = createRegistry(testContainer, contract, {
659
+ handlerRegisteredCallback: (_entry) => {
660
+ },
661
+ middlewareHandlerRegisteredCallback: (_entry) => {
662
+ }
663
+ });
664
+ flatListAllRegistryEntries(registry).forEach((entry) => {
665
+ if (!entry.handler) {
666
+ return;
667
+ }
668
+ const rt = route(testApiReg, `${entry.methodEndpoint.method} ${entry.methodEndpoint.genericPath}`);
669
+ rt.inject(entry.injection).register(entry.handler);
670
+ });
671
+ const client = createClient(contract, async (input) => {
672
+ const rt = route(testApiReg, `${input.method} ${input.genericPath}`);
673
+ const result = rt.trigger({
674
+ headers: input.headers,
675
+ pathParams: input.pathParams,
676
+ body: input.body ?? {},
677
+ query: input.query ?? {}
678
+ });
679
+ return result;
680
+ });
681
+ return client;
3
682
  }
4
683
 
5
- export { getHelloWorldMessage };
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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@congruent-stack/congruent-api",
3
3
  "type": "module",
4
- "version": "0.3.3",
4
+ "version": "0.6.0",
5
5
  "description": "Typescript schema-first tooling for agnostic REST APIs.",
6
6
  "keywords": [],
7
7
  "author": "congruent-stack",
@@ -19,6 +19,9 @@
19
19
  "default": "./dist/index.mjs"
20
20
  }
21
21
  },
22
+ "peerDependencies": {
23
+ "zod": "^4.0.10"
24
+ },
22
25
  "devDependencies": {
23
26
  "pkgroll": "2.14.5",
24
27
  "typescript": "^5.9.2"