@forklaunch/universal-sdk 0.2.7 → 0.3.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/lib/index.d.mts CHANGED
@@ -1,3 +1,37 @@
1
- declare const universalSdk: <TypedController>(host: string) => TypedController;
1
+ import { OpenAPIObject } from 'openapi3-ts/oas31';
2
+
3
+ type ResponseContentParserType = 'json' | 'file' | 'text' | 'stream';
4
+
5
+ /**
6
+ * @typedef {Object} RequestType
7
+ * @property {Record<string, string | number | boolean>} [params] - URL parameters.
8
+ * @property {Record<string, unknown>} [body] - Request body.
9
+ * @property {Record<string, string | number | boolean>} [query] - Query parameters.
10
+ * @property {Record<string, string>} [headers] - Request headers.
11
+ */
12
+
13
+ type RegistryOptions = {
14
+ path: string;
15
+ static?: boolean;
16
+ } | {
17
+ url: string;
18
+ static?: boolean;
19
+ } | {
20
+ raw: OpenAPIObject;
21
+ };
22
+
23
+ /**
24
+ * Initializes the Forklaunch SDK with HTTP methods proxied.
25
+ *
26
+ * @template TypedController
27
+ * @param {string} host - The host URL for the SDK.
28
+ * @returns {TypedController} - The SDK proxy with methods for HTTP requests.
29
+ */
30
+
31
+ declare const universalSdk: <TypedController>(options: {
32
+ host: string;
33
+ registryOptions: RegistryOptions;
34
+ contentTypeParserMap?: Record<string, ResponseContentParserType>;
35
+ }) => Promise<TypedController>;
2
36
 
3
37
  export { universalSdk };
package/lib/index.d.ts CHANGED
@@ -1,3 +1,37 @@
1
- declare const universalSdk: <TypedController>(host: string) => TypedController;
1
+ import { OpenAPIObject } from 'openapi3-ts/oas31';
2
+
3
+ type ResponseContentParserType = 'json' | 'file' | 'text' | 'stream';
4
+
5
+ /**
6
+ * @typedef {Object} RequestType
7
+ * @property {Record<string, string | number | boolean>} [params] - URL parameters.
8
+ * @property {Record<string, unknown>} [body] - Request body.
9
+ * @property {Record<string, string | number | boolean>} [query] - Query parameters.
10
+ * @property {Record<string, string>} [headers] - Request headers.
11
+ */
12
+
13
+ type RegistryOptions = {
14
+ path: string;
15
+ static?: boolean;
16
+ } | {
17
+ url: string;
18
+ static?: boolean;
19
+ } | {
20
+ raw: OpenAPIObject;
21
+ };
22
+
23
+ /**
24
+ * Initializes the Forklaunch SDK with HTTP methods proxied.
25
+ *
26
+ * @template TypedController
27
+ * @param {string} host - The host URL for the SDK.
28
+ * @returns {TypedController} - The SDK proxy with methods for HTTP requests.
29
+ */
30
+
31
+ declare const universalSdk: <TypedController>(options: {
32
+ host: string;
33
+ registryOptions: RegistryOptions;
34
+ contentTypeParserMap?: Record<string, ResponseContentParserType>;
35
+ }) => Promise<TypedController>;
2
36
 
3
37
  export { universalSdk };
package/lib/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // index.ts
@@ -24,7 +34,161 @@ __export(index_exports, {
24
34
  });
25
35
  module.exports = __toCommonJS(index_exports);
26
36
 
27
- // src/utils/regex.ts
37
+ // src/universalSdk.ts
38
+ var import_common2 = require("@forklaunch/common");
39
+ var import_ajv = __toESM(require("ajv"));
40
+ var import_ajv_formats = __toESM(require("ajv-formats"));
41
+
42
+ // src/core/coerceSpecialTypes.ts
43
+ function handleSpecialString(v, format) {
44
+ if (typeof v !== "string") return v;
45
+ if (format === "date-time") {
46
+ const d = new Date(v);
47
+ return isNaN(d.getTime()) ? v : d;
48
+ }
49
+ if (format === "binary") {
50
+ try {
51
+ return Buffer.from(v, "base64");
52
+ } catch {
53
+ throw new Error("Invalid base64 string");
54
+ }
55
+ }
56
+ return v;
57
+ }
58
+ function getType(type) {
59
+ if (Array.isArray(type)) return type[0];
60
+ return type;
61
+ }
62
+ function getFormatsFromSchema(def) {
63
+ const formats = /* @__PURE__ */ new Set();
64
+ if (!def) return formats;
65
+ if (def.format) formats.add(def.format);
66
+ for (const keyword of ["anyOf", "oneOf"]) {
67
+ if (Array.isArray(def[keyword])) {
68
+ for (const sub of def[keyword]) {
69
+ if (sub && typeof sub === "object") {
70
+ if (sub.format) formats.add(sub.format);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return formats;
76
+ }
77
+ function getTypeFromSchema(def) {
78
+ if (!def) return void 0;
79
+ const t = getType(def.type);
80
+ if (!t) {
81
+ for (const keyword of ["anyOf", "oneOf"]) {
82
+ if (Array.isArray(def[keyword])) {
83
+ for (const sub of def[keyword]) {
84
+ const subType = getType(sub.type);
85
+ if (subType) return subType;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return t;
91
+ }
92
+ function getSpecialFormat(def) {
93
+ const formats = getFormatsFromSchema(def);
94
+ if (formats.has("date-time")) return "date-time";
95
+ if (formats.has("binary")) return "binary";
96
+ return void 0;
97
+ }
98
+ function coerceSpecialTypes(input, schema) {
99
+ const props = schema.properties || {};
100
+ for (const [key, def] of Object.entries(props)) {
101
+ if (!def) continue;
102
+ const value = input[key];
103
+ const type = getTypeFromSchema(def);
104
+ if (type === "object" && def.properties && typeof value === "object" && value !== null) {
105
+ input[key] = coerceSpecialTypes(
106
+ value,
107
+ def
108
+ );
109
+ continue;
110
+ }
111
+ if (type === "array" && def.items && Array.isArray(value)) {
112
+ input[key] = value.map((item) => {
113
+ const itemDef = def.items;
114
+ const itemType = getTypeFromSchema(itemDef);
115
+ if (itemType === "object" && itemDef.properties && typeof item === "object" && item !== null) {
116
+ return coerceSpecialTypes(
117
+ item,
118
+ itemDef
119
+ );
120
+ }
121
+ if (itemType === "string") {
122
+ const format = getSpecialFormat(itemDef);
123
+ return handleSpecialString(item, format);
124
+ }
125
+ return item;
126
+ });
127
+ continue;
128
+ }
129
+ if (type === "string") {
130
+ const format = getSpecialFormat(def);
131
+ input[key] = handleSpecialString(value, format);
132
+ }
133
+ }
134
+ return input;
135
+ }
136
+
137
+ // src/core/mapContentType.ts
138
+ var import_common = require("@forklaunch/common");
139
+ function mapContentType(contentType) {
140
+ switch (contentType) {
141
+ case "json":
142
+ return "application/json";
143
+ case "file":
144
+ return "application/octet-stream";
145
+ case "text":
146
+ return "text/plain";
147
+ case "stream":
148
+ return "text/event-stream";
149
+ case void 0:
150
+ return "application/json";
151
+ default:
152
+ (0, import_common.isNever)(contentType);
153
+ return "application/json";
154
+ }
155
+ }
156
+
157
+ // src/core/refreshOpenApi.ts
158
+ async function refreshOpenApi(host, registryOptions, existingRegistryOpenApiHash) {
159
+ if (existingRegistryOpenApiHash === "static" || "static" in registryOptions && registryOptions.static) {
160
+ return {
161
+ updateRequired: false
162
+ };
163
+ }
164
+ if ("raw" in registryOptions) {
165
+ return {
166
+ updateRequired: true,
167
+ registryOpenApiJson: registryOptions.raw,
168
+ registryOpenApiHash: "static"
169
+ };
170
+ }
171
+ const registry = "path" in registryOptions ? `${host}/${registryOptions.path}` : "url" in registryOptions ? registryOptions.url : null;
172
+ if (registry == null) {
173
+ throw new Error("Raw OpenAPI JSON or registry information not provided");
174
+ }
175
+ const registryOpenApiHashFetch = await fetch(`${registry}-hash`);
176
+ const registryOpenApiHash = await registryOpenApiHashFetch.text();
177
+ if (existingRegistryOpenApiHash == null || existingRegistryOpenApiHash !== registryOpenApiHash) {
178
+ const registryOpenApiFetch = await fetch(registry);
179
+ const registryOpenApiJson = await registryOpenApiFetch.json();
180
+ return {
181
+ updateRequired: true,
182
+ registryOpenApiJson,
183
+ registryOpenApiHash
184
+ };
185
+ }
186
+ return {
187
+ updateRequired: false
188
+ };
189
+ }
190
+
191
+ // src/core/regex.ts
28
192
  function generateStringFromRegex(regex) {
29
193
  let regexStr = typeof regex === "object" ? regex.source : regex;
30
194
  if (regexStr.startsWith("/")) regexStr = regexStr.slice(1);
@@ -115,7 +279,7 @@ function generateStringFromRegex(regex) {
115
279
  return result;
116
280
  }
117
281
 
118
- // src/utils/resolvePath.ts
282
+ // src/core/resolvePath.ts
119
283
  function getSdkPath(path) {
120
284
  let sdkPath = path;
121
285
  if (Array.isArray(path)) {
@@ -131,14 +295,42 @@ function getSdkPath(path) {
131
295
  }
132
296
 
133
297
  // src/universalSdk.ts
134
- var UniversalSdk = class {
298
+ var UniversalSdk = class _UniversalSdk {
299
+ constructor(host, ajv, registryOptions, contentTypeParserMap, registryOpenApiJson, registryOpenApiHash) {
300
+ this.host = host;
301
+ this.ajv = ajv;
302
+ this.registryOptions = registryOptions;
303
+ this.contentTypeParserMap = contentTypeParserMap;
304
+ this.registryOpenApiJson = registryOpenApiJson;
305
+ this.registryOpenApiHash = registryOpenApiHash;
306
+ }
135
307
  /**
136
308
  * Creates an instance of UniversalSdk.
137
309
  *
138
310
  * @param {string} host - The host URL for the SDK.
139
311
  */
140
- constructor(host) {
141
- this.host = host;
312
+ static async create(host, registryOptions, contentTypeParserMap) {
313
+ const refreshResult = await refreshOpenApi(host, registryOptions);
314
+ let registryOpenApiJson;
315
+ let registryOpenApiHash;
316
+ if (refreshResult.updateRequired) {
317
+ registryOpenApiJson = refreshResult.registryOpenApiJson;
318
+ registryOpenApiHash = refreshResult.registryOpenApiHash;
319
+ }
320
+ const ajv = new import_ajv.default({
321
+ coerceTypes: true,
322
+ allErrors: true,
323
+ strict: false
324
+ });
325
+ (0, import_ajv_formats.default)(ajv);
326
+ return new _UniversalSdk(
327
+ host,
328
+ ajv,
329
+ registryOptions,
330
+ contentTypeParserMap,
331
+ registryOpenApiJson,
332
+ registryOpenApiHash
333
+ );
142
334
  }
143
335
  /**
144
336
  * Executes an HTTP request.
@@ -149,6 +341,18 @@ var UniversalSdk = class {
149
341
  * @returns {Promise<ResponseType>} - The response object.
150
342
  */
151
343
  async execute(route, method, request) {
344
+ if (!this.host) {
345
+ throw new Error("Host not initialized, please run .create(..) first");
346
+ }
347
+ const refreshResult = await refreshOpenApi(
348
+ this.host,
349
+ this.registryOptions,
350
+ this.registryOpenApiHash
351
+ );
352
+ if (refreshResult.updateRequired) {
353
+ this.registryOpenApiJson = refreshResult.registryOpenApiJson;
354
+ this.registryOpenApiHash = refreshResult.registryOpenApiHash;
355
+ }
152
356
  const { params, body, query, headers } = request || {};
153
357
  let url = getSdkPath(this.host + route);
154
358
  if (params) {
@@ -156,22 +360,193 @@ var UniversalSdk = class {
156
360
  url = url.replace(`:${key}`, encodeURIComponent(params[key]));
157
361
  }
158
362
  }
363
+ let defaultContentType = "application/json";
364
+ let parsedBody;
365
+ if (body != null) {
366
+ if ("schema" in body && body.schema != null) {
367
+ defaultContentType = "application/json";
368
+ parsedBody = (0, import_common2.safeStringify)(body.schema);
369
+ } else if ("json" in body && body.json != null) {
370
+ defaultContentType = "application/json";
371
+ parsedBody = (0, import_common2.safeStringify)(body.json);
372
+ } else if ("text" in body && body.text != null) {
373
+ defaultContentType = "text/plain";
374
+ parsedBody = body.text;
375
+ } else if ("file" in body && body.file != null) {
376
+ defaultContentType = "application/octet-stream";
377
+ parsedBody = await body.file.text();
378
+ } else if ("multipartForm" in body && body.multipartForm != null) {
379
+ defaultContentType = "multipart/form-data";
380
+ const formData = new FormData();
381
+ for (const key in body.multipartForm) {
382
+ if (Object.prototype.hasOwnProperty.call(body.multipartForm, key)) {
383
+ const value = body.multipartForm[key];
384
+ if (value instanceof Blob || value instanceof File) {
385
+ formData.append(key, value);
386
+ } else if (Array.isArray(value)) {
387
+ for (const item of value) {
388
+ formData.append(
389
+ key,
390
+ item instanceof Blob || item instanceof File ? item : (0, import_common2.safeStringify)(item)
391
+ );
392
+ }
393
+ } else {
394
+ formData.append(key, (0, import_common2.safeStringify)(value));
395
+ }
396
+ }
397
+ }
398
+ parsedBody = formData;
399
+ } else if ("urlEncodedForm" in body && body.urlEncodedForm != null) {
400
+ defaultContentType = "application/x-www-form-urlencoded";
401
+ parsedBody = new URLSearchParams(
402
+ Object.entries(body.urlEncodedForm).map(([key, value]) => [
403
+ key,
404
+ (0, import_common2.safeStringify)(value)
405
+ ])
406
+ );
407
+ } else {
408
+ parsedBody = (0, import_common2.safeStringify)(body);
409
+ }
410
+ }
159
411
  if (query) {
160
412
  const queryString = new URLSearchParams(
161
- query
413
+ Object.entries(query).map(([key, value]) => [key, (0, import_common2.safeStringify)(value)])
162
414
  ).toString();
163
415
  url += queryString ? `?${queryString}` : "";
164
416
  }
165
417
  const response = await fetch(encodeURI(url), {
166
- method,
167
- headers: headers ? { ...headers, "Content-Type": "application/json" } : void 0,
168
- body: body ? JSON.stringify(body) : void 0
418
+ method: method.toUpperCase(),
419
+ headers: {
420
+ ...headers,
421
+ ...defaultContentType != "multipart/form-data" ? { "Content-Type": body?.contentType ?? defaultContentType } : {}
422
+ },
423
+ body: parsedBody
169
424
  });
170
- const contentType = response.headers.get("content-type");
171
- const responseBody = contentType && contentType.includes("application/json") ? await response.json() : await response.text();
425
+ const responseOpenApi = this.registryOpenApiJson?.paths?.[route]?.[method.toLowerCase()]?.responses?.[response.status];
426
+ if (responseOpenApi == null) {
427
+ throw new Error(
428
+ `Response ${response.status} not found in OpenAPI spec for route ${route}`
429
+ );
430
+ }
431
+ const contentType = (response.headers.get("content-type") || response.headers.get("Content-Type"))?.split(";")[0];
432
+ const mappedContentType = (contentType != null ? this.contentTypeParserMap != null && contentType in this.contentTypeParserMap ? mapContentType(this.contentTypeParserMap[contentType]) : contentType : "application/json").split(";")[0];
433
+ let responseBody;
434
+ switch (mappedContentType) {
435
+ case "application/octet-stream": {
436
+ const contentDisposition = response.headers.get("content-disposition");
437
+ let fileName = null;
438
+ if (contentDisposition) {
439
+ const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(
440
+ contentDisposition
441
+ );
442
+ if (match) {
443
+ fileName = decodeURIComponent(match[1].replace(/['"]/g, ""));
444
+ }
445
+ }
446
+ const blob = await response.blob();
447
+ if (fileName == null) {
448
+ responseBody = blob;
449
+ } else {
450
+ responseBody = new File([blob], fileName);
451
+ }
452
+ break;
453
+ }
454
+ case "text/event-stream": {
455
+ const ajv = this.ajv;
456
+ async function* streamEvents(reader) {
457
+ const decoder = new TextDecoder();
458
+ let buffer = "";
459
+ let lastEventId;
460
+ while (true) {
461
+ const { done, value } = await reader.read();
462
+ if (done) break;
463
+ buffer += decoder.decode(value, { stream: true });
464
+ let newlineIndex;
465
+ while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
466
+ const line = buffer.slice(0, newlineIndex).trim();
467
+ buffer = buffer.slice(newlineIndex + 1);
468
+ if (line.startsWith("id:")) {
469
+ lastEventId = line.slice(3).trim();
470
+ } else if (line.startsWith("data:")) {
471
+ const data = line.slice(5).trim();
472
+ const json = {
473
+ data: (0, import_common2.safeParse)(data),
474
+ id: lastEventId
475
+ };
476
+ const isValidJson = ajv.validate(
477
+ responseOpenApi.content?.[contentType || mappedContentType].schema,
478
+ json
479
+ );
480
+ if (!isValidJson) {
481
+ throw new Error("Response does not match OpenAPI spec");
482
+ }
483
+ yield coerceSpecialTypes(
484
+ json,
485
+ responseOpenApi.content?.[contentType || mappedContentType].schema
486
+ );
487
+ }
488
+ }
489
+ }
490
+ if (buffer.length > 0) {
491
+ let id;
492
+ let data;
493
+ const lines = buffer.trim().split("\n");
494
+ for (const l of lines) {
495
+ const line = l.trim();
496
+ if (line.startsWith("id:")) {
497
+ id = line.slice(3).trim();
498
+ } else if (line.startsWith("data:")) {
499
+ data = line.slice(5).trim();
500
+ }
501
+ }
502
+ if (data !== void 0) {
503
+ const json = {
504
+ data: (0, import_common2.safeParse)(data),
505
+ id: id ?? lastEventId
506
+ };
507
+ const isValidJson = ajv.validate(
508
+ responseOpenApi.content?.[contentType || mappedContentType].schema,
509
+ json
510
+ );
511
+ if (!isValidJson) {
512
+ throw new Error("Response does not match OpenAPI spec");
513
+ }
514
+ yield coerceSpecialTypes(
515
+ json,
516
+ responseOpenApi.content?.[contentType || mappedContentType].schema
517
+ );
518
+ }
519
+ }
520
+ }
521
+ if (!response.body) {
522
+ throw new Error("No response body for event stream");
523
+ }
524
+ responseBody = streamEvents(response.body.getReader());
525
+ break;
526
+ }
527
+ case "text/plain":
528
+ responseBody = await response.text();
529
+ break;
530
+ case "application/json":
531
+ default: {
532
+ const json = await response.json();
533
+ const isValidJson = this.ajv.validate(
534
+ responseOpenApi.content?.[contentType || mappedContentType].schema,
535
+ json
536
+ );
537
+ if (!isValidJson) {
538
+ throw new Error("Response does not match OpenAPI spec");
539
+ }
540
+ responseBody = coerceSpecialTypes(
541
+ json,
542
+ responseOpenApi.content?.[contentType || mappedContentType].schema
543
+ );
544
+ break;
545
+ }
546
+ }
172
547
  return {
173
548
  code: response.status,
174
- content: responseBody,
549
+ response: responseBody,
175
550
  headers: response.headers
176
551
  };
177
552
  }
@@ -205,7 +580,7 @@ var UniversalSdk = class {
205
580
  * @returns {Promise<ResponseType>} - The response object.
206
581
  */
207
582
  async get(route, request) {
208
- return this.pathParamRequest(route, "GET", request);
583
+ return this.pathParamRequest(route, "get", request);
209
584
  }
210
585
  /**
211
586
  * Executes a POST request.
@@ -215,7 +590,7 @@ var UniversalSdk = class {
215
590
  * @returns {Promise<ResponseType>} - The response object.
216
591
  */
217
592
  async post(route, request) {
218
- return this.bodyRequest(route, "POST", request);
593
+ return this.bodyRequest(route, "post", request);
219
594
  }
220
595
  /**
221
596
  * Executes a PUT request.
@@ -225,7 +600,7 @@ var UniversalSdk = class {
225
600
  * @returns {Promise<ResponseType>} - The response object.
226
601
  */
227
602
  async put(route, request) {
228
- return this.bodyRequest(route, "PUT", request);
603
+ return this.bodyRequest(route, "put", request);
229
604
  }
230
605
  /**
231
606
  * Executes a PATCH request.
@@ -235,7 +610,7 @@ var UniversalSdk = class {
235
610
  * @returns {Promise<ResponseType>} - The response object.
236
611
  */
237
612
  async patch(route, request) {
238
- return this.bodyRequest(route, "PATCH", request);
613
+ return this.bodyRequest(route, "patch", request);
239
614
  }
240
615
  /**
241
616
  * Executes a DELETE request.
@@ -245,19 +620,54 @@ var UniversalSdk = class {
245
620
  * @returns {Promise<ResponseType>} - The response object.
246
621
  */
247
622
  async delete(route, request) {
248
- return this.pathParamRequest(route, "DELETE", request);
623
+ return this.pathParamRequest(route, "delete", request);
249
624
  }
250
625
  };
251
626
 
252
627
  // index.ts
253
- var universalSdk = (host) => {
254
- const sdkInternal = new UniversalSdk(host);
628
+ var universalSdk = async (options) => {
629
+ const sdkInternal = await UniversalSdk.create(
630
+ options.host,
631
+ options.registryOptions,
632
+ "contentTypeParserMap" in options ? options.contentTypeParserMap : void 0
633
+ );
255
634
  const proxyInternal = new Proxy(sdkInternal, {
256
635
  get(target, prop) {
636
+ if (prop === "then" || prop === "catch" || prop === "finally") {
637
+ return void 0;
638
+ }
257
639
  if (typeof prop === "string" && prop in target) {
258
- return target[prop].bind(target);
640
+ const value = target[prop];
641
+ if (typeof value === "function") {
642
+ return value.bind(target);
643
+ }
644
+ return value;
259
645
  }
260
- throw new Error(`Method ${String(prop)} not found`);
646
+ return new Proxy(() => {
647
+ }, {
648
+ get(_innerTarget, innerProp) {
649
+ if (typeof innerProp === "string" && innerProp in target) {
650
+ const value = target[innerProp];
651
+ if (typeof value === "function") {
652
+ return value.bind(target);
653
+ }
654
+ return value;
655
+ }
656
+ return new Proxy(() => {
657
+ }, {
658
+ get(__innerTarget, deepProp) {
659
+ if (typeof deepProp === "string" && deepProp in target) {
660
+ const value = target[deepProp];
661
+ if (typeof value === "function") {
662
+ return value.bind(target);
663
+ }
664
+ return value;
665
+ }
666
+ return void 0;
667
+ }
668
+ });
669
+ }
670
+ });
261
671
  }
262
672
  });
263
673
  return proxyInternal;
package/lib/index.mjs CHANGED
@@ -1,4 +1,158 @@
1
- // src/utils/regex.ts
1
+ // src/universalSdk.ts
2
+ import { safeParse, safeStringify } from "@forklaunch/common";
3
+ import Ajv from "ajv";
4
+ import addFormats from "ajv-formats";
5
+
6
+ // src/core/coerceSpecialTypes.ts
7
+ function handleSpecialString(v, format) {
8
+ if (typeof v !== "string") return v;
9
+ if (format === "date-time") {
10
+ const d = new Date(v);
11
+ return isNaN(d.getTime()) ? v : d;
12
+ }
13
+ if (format === "binary") {
14
+ try {
15
+ return Buffer.from(v, "base64");
16
+ } catch {
17
+ throw new Error("Invalid base64 string");
18
+ }
19
+ }
20
+ return v;
21
+ }
22
+ function getType(type) {
23
+ if (Array.isArray(type)) return type[0];
24
+ return type;
25
+ }
26
+ function getFormatsFromSchema(def) {
27
+ const formats = /* @__PURE__ */ new Set();
28
+ if (!def) return formats;
29
+ if (def.format) formats.add(def.format);
30
+ for (const keyword of ["anyOf", "oneOf"]) {
31
+ if (Array.isArray(def[keyword])) {
32
+ for (const sub of def[keyword]) {
33
+ if (sub && typeof sub === "object") {
34
+ if (sub.format) formats.add(sub.format);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return formats;
40
+ }
41
+ function getTypeFromSchema(def) {
42
+ if (!def) return void 0;
43
+ const t = getType(def.type);
44
+ if (!t) {
45
+ for (const keyword of ["anyOf", "oneOf"]) {
46
+ if (Array.isArray(def[keyword])) {
47
+ for (const sub of def[keyword]) {
48
+ const subType = getType(sub.type);
49
+ if (subType) return subType;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return t;
55
+ }
56
+ function getSpecialFormat(def) {
57
+ const formats = getFormatsFromSchema(def);
58
+ if (formats.has("date-time")) return "date-time";
59
+ if (formats.has("binary")) return "binary";
60
+ return void 0;
61
+ }
62
+ function coerceSpecialTypes(input, schema) {
63
+ const props = schema.properties || {};
64
+ for (const [key, def] of Object.entries(props)) {
65
+ if (!def) continue;
66
+ const value = input[key];
67
+ const type = getTypeFromSchema(def);
68
+ if (type === "object" && def.properties && typeof value === "object" && value !== null) {
69
+ input[key] = coerceSpecialTypes(
70
+ value,
71
+ def
72
+ );
73
+ continue;
74
+ }
75
+ if (type === "array" && def.items && Array.isArray(value)) {
76
+ input[key] = value.map((item) => {
77
+ const itemDef = def.items;
78
+ const itemType = getTypeFromSchema(itemDef);
79
+ if (itemType === "object" && itemDef.properties && typeof item === "object" && item !== null) {
80
+ return coerceSpecialTypes(
81
+ item,
82
+ itemDef
83
+ );
84
+ }
85
+ if (itemType === "string") {
86
+ const format = getSpecialFormat(itemDef);
87
+ return handleSpecialString(item, format);
88
+ }
89
+ return item;
90
+ });
91
+ continue;
92
+ }
93
+ if (type === "string") {
94
+ const format = getSpecialFormat(def);
95
+ input[key] = handleSpecialString(value, format);
96
+ }
97
+ }
98
+ return input;
99
+ }
100
+
101
+ // src/core/mapContentType.ts
102
+ import { isNever } from "@forklaunch/common";
103
+ function mapContentType(contentType) {
104
+ switch (contentType) {
105
+ case "json":
106
+ return "application/json";
107
+ case "file":
108
+ return "application/octet-stream";
109
+ case "text":
110
+ return "text/plain";
111
+ case "stream":
112
+ return "text/event-stream";
113
+ case void 0:
114
+ return "application/json";
115
+ default:
116
+ isNever(contentType);
117
+ return "application/json";
118
+ }
119
+ }
120
+
121
+ // src/core/refreshOpenApi.ts
122
+ async function refreshOpenApi(host, registryOptions, existingRegistryOpenApiHash) {
123
+ if (existingRegistryOpenApiHash === "static" || "static" in registryOptions && registryOptions.static) {
124
+ return {
125
+ updateRequired: false
126
+ };
127
+ }
128
+ if ("raw" in registryOptions) {
129
+ return {
130
+ updateRequired: true,
131
+ registryOpenApiJson: registryOptions.raw,
132
+ registryOpenApiHash: "static"
133
+ };
134
+ }
135
+ const registry = "path" in registryOptions ? `${host}/${registryOptions.path}` : "url" in registryOptions ? registryOptions.url : null;
136
+ if (registry == null) {
137
+ throw new Error("Raw OpenAPI JSON or registry information not provided");
138
+ }
139
+ const registryOpenApiHashFetch = await fetch(`${registry}-hash`);
140
+ const registryOpenApiHash = await registryOpenApiHashFetch.text();
141
+ if (existingRegistryOpenApiHash == null || existingRegistryOpenApiHash !== registryOpenApiHash) {
142
+ const registryOpenApiFetch = await fetch(registry);
143
+ const registryOpenApiJson = await registryOpenApiFetch.json();
144
+ return {
145
+ updateRequired: true,
146
+ registryOpenApiJson,
147
+ registryOpenApiHash
148
+ };
149
+ }
150
+ return {
151
+ updateRequired: false
152
+ };
153
+ }
154
+
155
+ // src/core/regex.ts
2
156
  function generateStringFromRegex(regex) {
3
157
  let regexStr = typeof regex === "object" ? regex.source : regex;
4
158
  if (regexStr.startsWith("/")) regexStr = regexStr.slice(1);
@@ -89,7 +243,7 @@ function generateStringFromRegex(regex) {
89
243
  return result;
90
244
  }
91
245
 
92
- // src/utils/resolvePath.ts
246
+ // src/core/resolvePath.ts
93
247
  function getSdkPath(path) {
94
248
  let sdkPath = path;
95
249
  if (Array.isArray(path)) {
@@ -105,14 +259,42 @@ function getSdkPath(path) {
105
259
  }
106
260
 
107
261
  // src/universalSdk.ts
108
- var UniversalSdk = class {
262
+ var UniversalSdk = class _UniversalSdk {
263
+ constructor(host, ajv, registryOptions, contentTypeParserMap, registryOpenApiJson, registryOpenApiHash) {
264
+ this.host = host;
265
+ this.ajv = ajv;
266
+ this.registryOptions = registryOptions;
267
+ this.contentTypeParserMap = contentTypeParserMap;
268
+ this.registryOpenApiJson = registryOpenApiJson;
269
+ this.registryOpenApiHash = registryOpenApiHash;
270
+ }
109
271
  /**
110
272
  * Creates an instance of UniversalSdk.
111
273
  *
112
274
  * @param {string} host - The host URL for the SDK.
113
275
  */
114
- constructor(host) {
115
- this.host = host;
276
+ static async create(host, registryOptions, contentTypeParserMap) {
277
+ const refreshResult = await refreshOpenApi(host, registryOptions);
278
+ let registryOpenApiJson;
279
+ let registryOpenApiHash;
280
+ if (refreshResult.updateRequired) {
281
+ registryOpenApiJson = refreshResult.registryOpenApiJson;
282
+ registryOpenApiHash = refreshResult.registryOpenApiHash;
283
+ }
284
+ const ajv = new Ajv({
285
+ coerceTypes: true,
286
+ allErrors: true,
287
+ strict: false
288
+ });
289
+ addFormats(ajv);
290
+ return new _UniversalSdk(
291
+ host,
292
+ ajv,
293
+ registryOptions,
294
+ contentTypeParserMap,
295
+ registryOpenApiJson,
296
+ registryOpenApiHash
297
+ );
116
298
  }
117
299
  /**
118
300
  * Executes an HTTP request.
@@ -123,6 +305,18 @@ var UniversalSdk = class {
123
305
  * @returns {Promise<ResponseType>} - The response object.
124
306
  */
125
307
  async execute(route, method, request) {
308
+ if (!this.host) {
309
+ throw new Error("Host not initialized, please run .create(..) first");
310
+ }
311
+ const refreshResult = await refreshOpenApi(
312
+ this.host,
313
+ this.registryOptions,
314
+ this.registryOpenApiHash
315
+ );
316
+ if (refreshResult.updateRequired) {
317
+ this.registryOpenApiJson = refreshResult.registryOpenApiJson;
318
+ this.registryOpenApiHash = refreshResult.registryOpenApiHash;
319
+ }
126
320
  const { params, body, query, headers } = request || {};
127
321
  let url = getSdkPath(this.host + route);
128
322
  if (params) {
@@ -130,22 +324,193 @@ var UniversalSdk = class {
130
324
  url = url.replace(`:${key}`, encodeURIComponent(params[key]));
131
325
  }
132
326
  }
327
+ let defaultContentType = "application/json";
328
+ let parsedBody;
329
+ if (body != null) {
330
+ if ("schema" in body && body.schema != null) {
331
+ defaultContentType = "application/json";
332
+ parsedBody = safeStringify(body.schema);
333
+ } else if ("json" in body && body.json != null) {
334
+ defaultContentType = "application/json";
335
+ parsedBody = safeStringify(body.json);
336
+ } else if ("text" in body && body.text != null) {
337
+ defaultContentType = "text/plain";
338
+ parsedBody = body.text;
339
+ } else if ("file" in body && body.file != null) {
340
+ defaultContentType = "application/octet-stream";
341
+ parsedBody = await body.file.text();
342
+ } else if ("multipartForm" in body && body.multipartForm != null) {
343
+ defaultContentType = "multipart/form-data";
344
+ const formData = new FormData();
345
+ for (const key in body.multipartForm) {
346
+ if (Object.prototype.hasOwnProperty.call(body.multipartForm, key)) {
347
+ const value = body.multipartForm[key];
348
+ if (value instanceof Blob || value instanceof File) {
349
+ formData.append(key, value);
350
+ } else if (Array.isArray(value)) {
351
+ for (const item of value) {
352
+ formData.append(
353
+ key,
354
+ item instanceof Blob || item instanceof File ? item : safeStringify(item)
355
+ );
356
+ }
357
+ } else {
358
+ formData.append(key, safeStringify(value));
359
+ }
360
+ }
361
+ }
362
+ parsedBody = formData;
363
+ } else if ("urlEncodedForm" in body && body.urlEncodedForm != null) {
364
+ defaultContentType = "application/x-www-form-urlencoded";
365
+ parsedBody = new URLSearchParams(
366
+ Object.entries(body.urlEncodedForm).map(([key, value]) => [
367
+ key,
368
+ safeStringify(value)
369
+ ])
370
+ );
371
+ } else {
372
+ parsedBody = safeStringify(body);
373
+ }
374
+ }
133
375
  if (query) {
134
376
  const queryString = new URLSearchParams(
135
- query
377
+ Object.entries(query).map(([key, value]) => [key, safeStringify(value)])
136
378
  ).toString();
137
379
  url += queryString ? `?${queryString}` : "";
138
380
  }
139
381
  const response = await fetch(encodeURI(url), {
140
- method,
141
- headers: headers ? { ...headers, "Content-Type": "application/json" } : void 0,
142
- body: body ? JSON.stringify(body) : void 0
382
+ method: method.toUpperCase(),
383
+ headers: {
384
+ ...headers,
385
+ ...defaultContentType != "multipart/form-data" ? { "Content-Type": body?.contentType ?? defaultContentType } : {}
386
+ },
387
+ body: parsedBody
143
388
  });
144
- const contentType = response.headers.get("content-type");
145
- const responseBody = contentType && contentType.includes("application/json") ? await response.json() : await response.text();
389
+ const responseOpenApi = this.registryOpenApiJson?.paths?.[route]?.[method.toLowerCase()]?.responses?.[response.status];
390
+ if (responseOpenApi == null) {
391
+ throw new Error(
392
+ `Response ${response.status} not found in OpenAPI spec for route ${route}`
393
+ );
394
+ }
395
+ const contentType = (response.headers.get("content-type") || response.headers.get("Content-Type"))?.split(";")[0];
396
+ const mappedContentType = (contentType != null ? this.contentTypeParserMap != null && contentType in this.contentTypeParserMap ? mapContentType(this.contentTypeParserMap[contentType]) : contentType : "application/json").split(";")[0];
397
+ let responseBody;
398
+ switch (mappedContentType) {
399
+ case "application/octet-stream": {
400
+ const contentDisposition = response.headers.get("content-disposition");
401
+ let fileName = null;
402
+ if (contentDisposition) {
403
+ const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(
404
+ contentDisposition
405
+ );
406
+ if (match) {
407
+ fileName = decodeURIComponent(match[1].replace(/['"]/g, ""));
408
+ }
409
+ }
410
+ const blob = await response.blob();
411
+ if (fileName == null) {
412
+ responseBody = blob;
413
+ } else {
414
+ responseBody = new File([blob], fileName);
415
+ }
416
+ break;
417
+ }
418
+ case "text/event-stream": {
419
+ const ajv = this.ajv;
420
+ async function* streamEvents(reader) {
421
+ const decoder = new TextDecoder();
422
+ let buffer = "";
423
+ let lastEventId;
424
+ while (true) {
425
+ const { done, value } = await reader.read();
426
+ if (done) break;
427
+ buffer += decoder.decode(value, { stream: true });
428
+ let newlineIndex;
429
+ while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
430
+ const line = buffer.slice(0, newlineIndex).trim();
431
+ buffer = buffer.slice(newlineIndex + 1);
432
+ if (line.startsWith("id:")) {
433
+ lastEventId = line.slice(3).trim();
434
+ } else if (line.startsWith("data:")) {
435
+ const data = line.slice(5).trim();
436
+ const json = {
437
+ data: safeParse(data),
438
+ id: lastEventId
439
+ };
440
+ const isValidJson = ajv.validate(
441
+ responseOpenApi.content?.[contentType || mappedContentType].schema,
442
+ json
443
+ );
444
+ if (!isValidJson) {
445
+ throw new Error("Response does not match OpenAPI spec");
446
+ }
447
+ yield coerceSpecialTypes(
448
+ json,
449
+ responseOpenApi.content?.[contentType || mappedContentType].schema
450
+ );
451
+ }
452
+ }
453
+ }
454
+ if (buffer.length > 0) {
455
+ let id;
456
+ let data;
457
+ const lines = buffer.trim().split("\n");
458
+ for (const l of lines) {
459
+ const line = l.trim();
460
+ if (line.startsWith("id:")) {
461
+ id = line.slice(3).trim();
462
+ } else if (line.startsWith("data:")) {
463
+ data = line.slice(5).trim();
464
+ }
465
+ }
466
+ if (data !== void 0) {
467
+ const json = {
468
+ data: safeParse(data),
469
+ id: id ?? lastEventId
470
+ };
471
+ const isValidJson = ajv.validate(
472
+ responseOpenApi.content?.[contentType || mappedContentType].schema,
473
+ json
474
+ );
475
+ if (!isValidJson) {
476
+ throw new Error("Response does not match OpenAPI spec");
477
+ }
478
+ yield coerceSpecialTypes(
479
+ json,
480
+ responseOpenApi.content?.[contentType || mappedContentType].schema
481
+ );
482
+ }
483
+ }
484
+ }
485
+ if (!response.body) {
486
+ throw new Error("No response body for event stream");
487
+ }
488
+ responseBody = streamEvents(response.body.getReader());
489
+ break;
490
+ }
491
+ case "text/plain":
492
+ responseBody = await response.text();
493
+ break;
494
+ case "application/json":
495
+ default: {
496
+ const json = await response.json();
497
+ const isValidJson = this.ajv.validate(
498
+ responseOpenApi.content?.[contentType || mappedContentType].schema,
499
+ json
500
+ );
501
+ if (!isValidJson) {
502
+ throw new Error("Response does not match OpenAPI spec");
503
+ }
504
+ responseBody = coerceSpecialTypes(
505
+ json,
506
+ responseOpenApi.content?.[contentType || mappedContentType].schema
507
+ );
508
+ break;
509
+ }
510
+ }
146
511
  return {
147
512
  code: response.status,
148
- content: responseBody,
513
+ response: responseBody,
149
514
  headers: response.headers
150
515
  };
151
516
  }
@@ -179,7 +544,7 @@ var UniversalSdk = class {
179
544
  * @returns {Promise<ResponseType>} - The response object.
180
545
  */
181
546
  async get(route, request) {
182
- return this.pathParamRequest(route, "GET", request);
547
+ return this.pathParamRequest(route, "get", request);
183
548
  }
184
549
  /**
185
550
  * Executes a POST request.
@@ -189,7 +554,7 @@ var UniversalSdk = class {
189
554
  * @returns {Promise<ResponseType>} - The response object.
190
555
  */
191
556
  async post(route, request) {
192
- return this.bodyRequest(route, "POST", request);
557
+ return this.bodyRequest(route, "post", request);
193
558
  }
194
559
  /**
195
560
  * Executes a PUT request.
@@ -199,7 +564,7 @@ var UniversalSdk = class {
199
564
  * @returns {Promise<ResponseType>} - The response object.
200
565
  */
201
566
  async put(route, request) {
202
- return this.bodyRequest(route, "PUT", request);
567
+ return this.bodyRequest(route, "put", request);
203
568
  }
204
569
  /**
205
570
  * Executes a PATCH request.
@@ -209,7 +574,7 @@ var UniversalSdk = class {
209
574
  * @returns {Promise<ResponseType>} - The response object.
210
575
  */
211
576
  async patch(route, request) {
212
- return this.bodyRequest(route, "PATCH", request);
577
+ return this.bodyRequest(route, "patch", request);
213
578
  }
214
579
  /**
215
580
  * Executes a DELETE request.
@@ -219,19 +584,54 @@ var UniversalSdk = class {
219
584
  * @returns {Promise<ResponseType>} - The response object.
220
585
  */
221
586
  async delete(route, request) {
222
- return this.pathParamRequest(route, "DELETE", request);
587
+ return this.pathParamRequest(route, "delete", request);
223
588
  }
224
589
  };
225
590
 
226
591
  // index.ts
227
- var universalSdk = (host) => {
228
- const sdkInternal = new UniversalSdk(host);
592
+ var universalSdk = async (options) => {
593
+ const sdkInternal = await UniversalSdk.create(
594
+ options.host,
595
+ options.registryOptions,
596
+ "contentTypeParserMap" in options ? options.contentTypeParserMap : void 0
597
+ );
229
598
  const proxyInternal = new Proxy(sdkInternal, {
230
599
  get(target, prop) {
600
+ if (prop === "then" || prop === "catch" || prop === "finally") {
601
+ return void 0;
602
+ }
231
603
  if (typeof prop === "string" && prop in target) {
232
- return target[prop].bind(target);
604
+ const value = target[prop];
605
+ if (typeof value === "function") {
606
+ return value.bind(target);
607
+ }
608
+ return value;
233
609
  }
234
- throw new Error(`Method ${String(prop)} not found`);
610
+ return new Proxy(() => {
611
+ }, {
612
+ get(_innerTarget, innerProp) {
613
+ if (typeof innerProp === "string" && innerProp in target) {
614
+ const value = target[innerProp];
615
+ if (typeof value === "function") {
616
+ return value.bind(target);
617
+ }
618
+ return value;
619
+ }
620
+ return new Proxy(() => {
621
+ }, {
622
+ get(__innerTarget, deepProp) {
623
+ if (typeof deepProp === "string" && deepProp in target) {
624
+ const value = target[deepProp];
625
+ if (typeof value === "function") {
626
+ return value.bind(target);
627
+ }
628
+ return value;
629
+ }
630
+ return void 0;
631
+ }
632
+ });
633
+ }
634
+ });
235
635
  }
236
636
  });
237
637
  return proxyInternal;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forklaunch/universal-sdk",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Cross runtime fetch library for forklaunch router sdks",
5
5
  "keywords": [
6
6
  "fetch",
@@ -30,12 +30,19 @@
30
30
  "files": [
31
31
  "lib/**"
32
32
  ],
33
+ "dependencies": {
34
+ "ajv": "^8.17.1",
35
+ "ajv-formats": "^3.0.1",
36
+ "@forklaunch/common": "0.3.0"
37
+ },
33
38
  "devDependencies": {
34
39
  "fetch-mock": "^12.5.2",
35
40
  "jest": "^29.7.0",
36
- "ts-jest": "^29.3.2",
37
- "tsup": "^8.4.0",
38
- "typedoc": "^0.28.2"
41
+ "openapi3-ts": "^4.4.0",
42
+ "prettier": "^3.5.3",
43
+ "ts-jest": "^29.3.4",
44
+ "tsup": "^8.5.0",
45
+ "typedoc": "^0.28.4"
39
46
  },
40
47
  "scripts": {
41
48
  "build": "tsc --noEmit && tsup index.ts --format cjs,esm --no-splitting --dts --tsconfig tsconfig.json --out-dir lib --clean",