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