@forklaunch/universal-sdk
Version:
Cross runtime fetch library for forklaunch router sdks
695 lines (685 loc) • 23 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// index.ts
var index_exports = {};
__export(index_exports, {
universalSdk: () => universalSdk
});
module.exports = __toCommonJS(index_exports);
// src/universalSdk.ts
var import_common2 = require("@forklaunch/common");
var import_ajv = __toESM(require("ajv"));
var import_ajv_formats = __toESM(require("ajv-formats"));
// src/core/coerceSpecialTypes.ts
function base64ToArrayBuffer(base64) {
if (typeof atob === "function") {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; ++i) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
} else if (typeof Buffer !== "undefined") {
const buf = Buffer.from(base64, "base64");
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
} else {
throw new Error("No base64 decoder available in this environment");
}
}
function handleSpecialString(v, format) {
if (typeof v !== "string") return v;
if (format === "date-time") {
const d = new Date(v);
return isNaN(d.getTime()) ? v : d;
}
if (format === "binary") {
try {
return base64ToArrayBuffer(v);
} catch {
throw new Error("Invalid base64 string");
}
}
return v;
}
function getType(type) {
if (Array.isArray(type)) return type[0];
return type;
}
function getFormatsFromSchema(def) {
const formats = /* @__PURE__ */ new Set();
if (!def) return formats;
if (def.format) formats.add(def.format);
for (const keyword of ["anyOf", "oneOf"]) {
if (Array.isArray(def[keyword])) {
for (const sub of def[keyword]) {
if (sub && typeof sub === "object") {
if (sub.format) formats.add(sub.format);
}
}
}
}
return formats;
}
function getTypeFromSchema(def) {
if (!def) return void 0;
const t = getType(def.type);
if (!t) {
for (const keyword of ["anyOf", "oneOf"]) {
if (Array.isArray(def[keyword])) {
for (const sub of def[keyword]) {
const subType = getType(sub.type);
if (subType) return subType;
}
}
}
}
return t;
}
function getSpecialFormat(def) {
const formats = getFormatsFromSchema(def);
if (formats.has("date-time")) return "date-time";
if (formats.has("binary")) return "binary";
return void 0;
}
function coerceSpecialTypes(input, schema) {
const props = schema.properties || {};
for (const [key, def] of Object.entries(props)) {
if (!def) continue;
const value = input[key];
const type = getTypeFromSchema(def);
if (type === "object" && def.properties && typeof value === "object" && value !== null) {
input[key] = coerceSpecialTypes(
value,
def
);
continue;
}
if (type === "array" && def.items && Array.isArray(value)) {
input[key] = value.map((item) => {
const itemDef = def.items;
const itemType = getTypeFromSchema(itemDef);
if (itemType === "object" && itemDef.properties && typeof item === "object" && item !== null) {
return coerceSpecialTypes(
item,
itemDef
);
}
if (itemType === "string") {
const format = getSpecialFormat(itemDef);
return handleSpecialString(item, format);
}
return item;
});
continue;
}
if (type === "string") {
const format = getSpecialFormat(def);
input[key] = handleSpecialString(value, format);
}
}
return input;
}
// src/core/mapContentType.ts
var import_common = require("@forklaunch/common");
function mapContentType(contentType) {
switch (contentType) {
case "json":
return "application/json";
case "file":
return "application/octet-stream";
case "text":
return "text/plain";
case "stream":
return "text/event-stream";
case void 0:
return "application/json";
default:
(0, import_common.isNever)(contentType);
return "application/json";
}
}
// src/core/refreshOpenApi.ts
async function refreshOpenApi(host, registryOptions, existingRegistryOpenApiHash) {
if (existingRegistryOpenApiHash === "static" || "static" in registryOptions && registryOptions.static) {
return {
updateRequired: false
};
}
if ("raw" in registryOptions) {
return {
updateRequired: true,
registryOpenApiJson: registryOptions.raw,
registryOpenApiHash: "static"
};
}
const registry = "path" in registryOptions ? `${host}/${registryOptions.path}` : "url" in registryOptions ? registryOptions.url : null;
if (registry == null) {
throw new Error("Raw OpenAPI JSON or registry information not provided");
}
const registryOpenApiHashFetch = await fetch(`${registry}-hash`);
const registryOpenApiHash = await registryOpenApiHashFetch.text();
if (existingRegistryOpenApiHash == null || existingRegistryOpenApiHash !== registryOpenApiHash) {
const registryOpenApiFetch = await fetch(registry);
const registryOpenApiJson = await registryOpenApiFetch.json();
return {
updateRequired: true,
registryOpenApiJson,
registryOpenApiHash
};
}
return {
updateRequired: false
};
}
// src/core/regex.ts
function generateStringFromRegex(regex) {
let regexStr = typeof regex === "object" ? regex.source : regex;
if (regexStr.startsWith("/")) regexStr = regexStr.slice(1);
if (regexStr.endsWith("/g")) regexStr = regexStr.slice(0, -2);
if (regexStr.endsWith("/")) regexStr = regexStr.slice(0, -1);
let result = "";
let i = 0;
while (i < regexStr.length) {
const char = regexStr[i];
switch (char) {
case "\\": {
const nextChar = regexStr[i + 1];
switch (nextChar) {
case "b":
if (result.length > 0 && /\w/.test(result[result.length - 1])) {
result += " ";
}
break;
case "d":
result += "0";
break;
case "w":
result += "a";
break;
case "s":
result += " ";
break;
default:
result += nextChar;
}
i += 2;
break;
}
case ".":
result += "a";
i++;
break;
case "[": {
const endIdx = regexStr.indexOf("]", i);
if (endIdx === -1) {
throw new Error("Unmatched [");
}
const charClass = regexStr.slice(i + 1, endIdx);
result += charClass[0];
i = endIdx + 1;
break;
}
case "(": {
const endGroupIdx = regexStr.indexOf(")", i);
if (endGroupIdx === -1) {
throw new Error("Unmatched (");
}
const groupContent = regexStr.slice(i + 1, endGroupIdx);
result += generateStringFromRegex(groupContent);
i = endGroupIdx + 1;
break;
}
case "{": {
const endQuantIdx = regexStr.indexOf("}", i);
if (endQuantIdx === -1) {
throw new Error("Unmatched {");
}
const quantifier = regexStr.slice(i + 1, endQuantIdx);
const min = parseInt(quantifier.split(",")[0], 10) || 1;
const lastChar = result[result.length - 1];
result += lastChar.repeat(min - 1);
i = endQuantIdx + 1;
break;
}
case "*":
case "+":
case "?": {
const prevChar = result[result.length - 1];
if (char === "*") {
result += prevChar;
} else if (char === "+") {
result += prevChar;
}
i++;
break;
}
default:
result += char;
i++;
break;
}
}
return result;
}
// src/core/resolvePath.ts
function getSdkPath(path) {
let sdkPath = path;
if (Array.isArray(path)) {
sdkPath = path.pop() || path[0];
}
if (!sdkPath) {
throw new Error("Path is not defined");
}
if (sdkPath instanceof RegExp) {
sdkPath = generateStringFromRegex(sdkPath);
}
return sdkPath;
}
// src/universalSdk.ts
var UniversalSdk = class _UniversalSdk {
constructor(host, ajv, registryOptions, contentTypeParserMap, registryOpenApiJson, registryOpenApiHash) {
this.host = host;
this.ajv = ajv;
this.registryOptions = registryOptions;
this.contentTypeParserMap = contentTypeParserMap;
this.registryOpenApiJson = registryOpenApiJson;
this.registryOpenApiHash = registryOpenApiHash;
}
/**
* Creates an instance of UniversalSdk.
*
* @param {string} host - The host URL for the SDK.
*/
static async create(host, registryOptions, contentTypeParserMap) {
const refreshResult = await refreshOpenApi(host, registryOptions);
let registryOpenApiJson;
let registryOpenApiHash;
if (refreshResult.updateRequired) {
registryOpenApiJson = refreshResult.registryOpenApiJson;
registryOpenApiHash = refreshResult.registryOpenApiHash;
}
const ajv = new import_ajv.default({
coerceTypes: true,
allErrors: true,
strict: false
});
(0, import_ajv_formats.default)(ajv);
return new _UniversalSdk(
host,
ajv,
registryOptions,
contentTypeParserMap,
registryOpenApiJson,
registryOpenApiHash
);
}
/**
* Executes an HTTP request.
*
* @param {string} route - The route path for the request.
* @param {'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'} method - The HTTP method.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async execute(route, method, request) {
if (!this.host) {
throw new Error("Host not initialized, please run .create(..) first");
}
const refreshResult = await refreshOpenApi(
this.host,
this.registryOptions,
this.registryOpenApiHash
);
if (refreshResult.updateRequired) {
this.registryOpenApiJson = refreshResult.registryOpenApiJson;
this.registryOpenApiHash = refreshResult.registryOpenApiHash;
}
const { params, body, query, headers } = request || {};
let url = getSdkPath(this.host + route);
if (params) {
for (const key in params) {
url = url.replace(`:${key}`, encodeURIComponent(params[key]));
}
}
let defaultContentType = "application/json";
let parsedBody;
if (body != null) {
if ("schema" in body && body.schema != null) {
defaultContentType = "application/json";
parsedBody = (0, import_common2.safeStringify)(body.schema);
} else if ("json" in body && body.json != null) {
defaultContentType = "application/json";
parsedBody = (0, import_common2.safeStringify)(body.json);
} else if ("text" in body && body.text != null) {
defaultContentType = "text/plain";
parsedBody = body.text;
} else if ("file" in body && body.file != null) {
defaultContentType = "application/octet-stream";
parsedBody = await body.file.text();
} else if ("multipartForm" in body && body.multipartForm != null) {
defaultContentType = "multipart/form-data";
const formData = new FormData();
for (const key in body.multipartForm) {
if (Object.prototype.hasOwnProperty.call(body.multipartForm, key)) {
const value = body.multipartForm[key];
if (value instanceof Blob || value instanceof File) {
formData.append(key, value);
} else if (Array.isArray(value)) {
for (const item of value) {
formData.append(
key,
item instanceof Blob || item instanceof File ? item : (0, import_common2.safeStringify)(item)
);
}
} else {
formData.append(key, (0, import_common2.safeStringify)(value));
}
}
}
parsedBody = formData;
} else if ("urlEncodedForm" in body && body.urlEncodedForm != null) {
defaultContentType = "application/x-www-form-urlencoded";
parsedBody = new URLSearchParams(
Object.entries(body.urlEncodedForm).map(([key, value]) => [
key,
(0, import_common2.safeStringify)(value)
])
);
} else {
parsedBody = (0, import_common2.safeStringify)(body);
}
}
if (query) {
const queryString = new URLSearchParams(
Object.entries(query).map(([key, value]) => [key, (0, import_common2.safeStringify)(value)])
).toString();
url += queryString ? `?${queryString}` : "";
}
const response = await fetch(encodeURI(url), {
method: method.toUpperCase(),
headers: {
...headers,
...defaultContentType != "multipart/form-data" ? { "Content-Type": body?.contentType ?? defaultContentType } : {}
},
body: parsedBody
});
const responseOpenApi = this.registryOpenApiJson?.paths?.[route]?.[method.toLowerCase()]?.responses?.[response.status];
if (responseOpenApi == null) {
throw new Error(
`Response ${response.status} not found in OpenAPI spec for route ${route}`
);
}
const contentType = (response.headers.get("content-type") || response.headers.get("Content-Type"))?.split(";")[0];
const mappedContentType = (contentType != null ? this.contentTypeParserMap != null && contentType in this.contentTypeParserMap ? mapContentType(this.contentTypeParserMap[contentType]) : contentType : "application/json").split(";")[0];
let responseBody;
switch (mappedContentType) {
case "application/octet-stream": {
const contentDisposition = response.headers.get("content-disposition");
let fileName = null;
if (contentDisposition) {
const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(
contentDisposition
);
if (match) {
fileName = decodeURIComponent(match[1].replace(/['"]/g, ""));
}
}
const blob = await response.blob();
if (fileName == null) {
responseBody = blob;
} else {
responseBody = new File([blob], fileName);
}
break;
}
case "text/event-stream": {
const ajv = this.ajv;
async function* streamEvents(reader) {
const decoder = new TextDecoder();
let buffer = "";
let lastEventId;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIndex;
while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line.startsWith("id:")) {
lastEventId = line.slice(3).trim();
} else if (line.startsWith("data:")) {
const data = line.slice(5).trim();
const json = {
data: (0, import_common2.safeParse)(data),
id: lastEventId
};
const isValidJson = ajv.validate(
responseOpenApi.content?.[contentType || mappedContentType].schema,
json
);
if (!isValidJson) {
throw new Error("Response does not match OpenAPI spec");
}
yield coerceSpecialTypes(
json,
responseOpenApi.content?.[contentType || mappedContentType].schema
);
}
}
}
if (buffer.length > 0) {
let id;
let data;
const lines = buffer.trim().split("\n");
for (const l of lines) {
const line = l.trim();
if (line.startsWith("id:")) {
id = line.slice(3).trim();
} else if (line.startsWith("data:")) {
data = line.slice(5).trim();
}
}
if (data !== void 0) {
const json = {
data: (0, import_common2.safeParse)(data),
id: id ?? lastEventId
};
const isValidJson = ajv.validate(
responseOpenApi.content?.[contentType || mappedContentType].schema,
json
);
if (!isValidJson) {
throw new Error("Response does not match OpenAPI spec");
}
yield coerceSpecialTypes(
json,
responseOpenApi.content?.[contentType || mappedContentType].schema
);
}
}
}
if (!response.body) {
throw new Error("No response body for event stream");
}
responseBody = streamEvents(response.body.getReader());
break;
}
case "text/plain":
responseBody = await response.text();
break;
case "application/json":
default: {
const json = await response.json();
const isValidJson = this.ajv.validate(
responseOpenApi.content?.[contentType || mappedContentType].schema,
json
);
if (!isValidJson) {
throw new Error("Response does not match OpenAPI spec");
}
responseBody = coerceSpecialTypes(
json,
responseOpenApi.content?.[contentType || mappedContentType].schema
);
break;
}
}
return {
code: response.status,
response: responseBody,
headers: response.headers
};
}
/**
* Executes a request with path parameters.
*
* @param {string} route - The route path for the request.
* @param {'GET' | 'DELETE'} method - The HTTP method.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async pathParamRequest(route, method, request) {
return this.execute(route, method, request);
}
/**
* Executes a request with a body.
*
* @param {string} route - The route path for the request.
* @param {'POST' | 'PUT' | 'PATCH'} method - The HTTP method.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async bodyRequest(route, method, request) {
return this.execute(route, method, request);
}
/**
* Executes a GET request.
*
* @param {string} route - The route path for the request.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async get(route, request) {
return this.pathParamRequest(route, "get", request);
}
/**
* Executes a POST request.
*
* @param {string} route - The route path for the request.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async post(route, request) {
return this.bodyRequest(route, "post", request);
}
/**
* Executes a PUT request.
*
* @param {string} route - The route path for the request.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async put(route, request) {
return this.bodyRequest(route, "put", request);
}
/**
* Executes a PATCH request.
*
* @param {string} route - The route path for the request.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async patch(route, request) {
return this.bodyRequest(route, "patch", request);
}
/**
* Executes a DELETE request.
*
* @param {string} route - The route path for the request.
* @param {RequestType} [request] - The request object.
* @returns {Promise<ResponseType>} - The response object.
*/
async delete(route, request) {
return this.pathParamRequest(route, "delete", request);
}
};
// index.ts
var universalSdk = async (options) => {
const sdkInternal = await UniversalSdk.create(
options.host,
options.registryOptions,
"contentTypeParserMap" in options ? options.contentTypeParserMap : void 0
);
const proxyInternal = new Proxy(sdkInternal, {
get(target, prop) {
if (prop === "then" || prop === "catch" || prop === "finally") {
return void 0;
}
if (typeof prop === "string" && prop in target) {
const value = target[prop];
if (typeof value === "function") {
return value.bind(target);
}
return value;
}
return new Proxy(() => {
}, {
get(_innerTarget, innerProp) {
if (typeof innerProp === "string" && innerProp in target) {
const value = target[innerProp];
if (typeof value === "function") {
return value.bind(target);
}
return value;
}
return new Proxy(() => {
}, {
get(__innerTarget, deepProp) {
if (typeof deepProp === "string" && deepProp in target) {
const value = target[deepProp];
if (typeof value === "function") {
return value.bind(target);
}
return value;
}
return void 0;
}
});
}
});
}
});
return proxyInternal;
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
universalSdk
});