@microlink/mql
Version:
Microlink Query Language. The official HTTP client to interact with Microlink API for Node.js, browsers & Deno.
813 lines (764 loc) • 28.7 kB
JavaScript
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function getAugmentedNamespace(n) {
if (n.__esModule) return n;
var f = n.default;
if (typeof f == "function") {
var a = function a () {
if (this instanceof a) {
return Reflect.construct(f, arguments, this.constructor);
}
return f.apply(this, arguments);
};
a.prototype = f.prototype;
} else a = {};
Object.defineProperty(a, '__esModule', {value: true});
Object.keys(n).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(n, k);
Object.defineProperty(a, k, d.get ? d : {
enumerable: true,
get: function () {
return n[k];
}
});
});
return a;
}
var lightweight$1 = {exports: {}};
var dist = {};
function iter(output, nullish, sep, val, key) {
var k, pfx = key ? (key + sep) : key;
if (val == null) {
if (nullish) output[key] = val;
} else if (typeof val != 'object') {
output[key] = val;
} else if (Array.isArray(val)) {
for (k=0; k < val.length; k++) {
iter(output, nullish, sep, val[k], pfx + k);
}
} else {
for (k in val) {
iter(output, nullish, sep, val[k], pfx + k);
}
}
}
function flattie(input, glue, toNull) {
var output = {};
if (typeof input == 'object') {
iter(output, !!toNull, glue || '.', input, '');
}
return output;
}
dist.flattie = flattie;
class HTTPError extends Error {
response;
request;
options;
constructor(response, request, options) {
const code = (response.status || response.status === 0) ? response.status : '';
const title = response.statusText || '';
const status = `${code} ${title}`.trim();
const reason = status ? `status code ${status}` : 'an unknown error';
super(`Request failed with ${reason}: ${request.method} ${request.url}`);
this.name = 'HTTPError';
this.response = response;
this.request = request;
this.options = options;
}
}
class TimeoutError extends Error {
request;
constructor(request) {
super(`Request timed out: ${request.method} ${request.url}`);
this.name = 'TimeoutError';
this.request = request;
}
}
// eslint-disable-next-line @typescript-eslint/ban-types
const isObject$1 = (value) => value !== null && typeof value === 'object';
const validateAndMerge = (...sources) => {
for (const source of sources) {
if ((!isObject$1(source) || Array.isArray(source)) && source !== undefined) {
throw new TypeError('The `options` argument must be an object');
}
}
return deepMerge({}, ...sources);
};
const mergeHeaders = (source1 = {}, source2 = {}) => {
const result = new globalThis.Headers(source1);
const isHeadersInstance = source2 instanceof globalThis.Headers;
const source = new globalThis.Headers(source2);
for (const [key, value] of source.entries()) {
if ((isHeadersInstance && value === 'undefined') || value === undefined) {
result.delete(key);
}
else {
result.set(key, value);
}
}
return result;
};
function newHookValue(original, incoming, property) {
return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
? []
: deepMerge(original[property] ?? [], incoming[property] ?? []);
}
const mergeHooks = (original = {}, incoming = {}) => ({
beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
afterResponse: newHookValue(original, incoming, 'afterResponse'),
beforeError: newHookValue(original, incoming, 'beforeError'),
});
// TODO: Make this strongly-typed (no `any`).
const deepMerge = (...sources) => {
let returnValue = {};
let headers = {};
let hooks = {};
for (const source of sources) {
if (Array.isArray(source)) {
if (!Array.isArray(returnValue)) {
returnValue = [];
}
returnValue = [...returnValue, ...source];
}
else if (isObject$1(source)) {
for (let [key, value] of Object.entries(source)) {
if (isObject$1(value) && key in returnValue) {
value = deepMerge(returnValue[key], value);
}
returnValue = { ...returnValue, [key]: value };
}
if (isObject$1(source.hooks)) {
hooks = mergeHooks(hooks, source.hooks);
returnValue.hooks = hooks;
}
if (isObject$1(source.headers)) {
headers = mergeHeaders(headers, source.headers);
returnValue.headers = headers;
}
}
}
return returnValue;
};
const supportsRequestStreams = (() => {
let duplexAccessed = false;
let hasContentType = false;
const supportsReadableStream = typeof globalThis.ReadableStream === 'function';
const supportsRequest = typeof globalThis.Request === 'function';
if (supportsReadableStream && supportsRequest) {
try {
hasContentType = new globalThis.Request('https://empty.invalid', {
body: new globalThis.ReadableStream(),
method: 'POST',
// @ts-expect-error - Types are outdated.
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');
}
catch (error) {
// QQBrowser on iOS throws "unsupported BodyInit type" error (see issue #581)
if (error instanceof Error && error.message === 'unsupported BodyInit type') {
return false;
}
throw error;
}
}
return duplexAccessed && !hasContentType;
})();
const supportsAbortController = typeof globalThis.AbortController === 'function';
const supportsResponseStreams = typeof globalThis.ReadableStream === 'function';
const supportsFormData = typeof globalThis.FormData === 'function';
const requestMethods = ['get', 'post', 'put', 'patch', 'head', 'delete'];
const responseTypes = {
json: 'application/json',
text: 'text/*',
formData: 'multipart/form-data',
arrayBuffer: '*/*',
blob: '*/*',
};
// The maximum value of a 32bit int (see issue #117)
const maxSafeTimeout = 2_147_483_647;
const stop = Symbol('stop');
const kyOptionKeys = {
json: true,
parseJson: true,
stringifyJson: true,
searchParams: true,
prefixUrl: true,
retry: true,
timeout: true,
hooks: true,
throwHttpErrors: true,
onDownloadProgress: true,
fetch: true,
};
const requestOptionsRegistry = {
method: true,
headers: true,
body: true,
mode: true,
credentials: true,
cache: true,
redirect: true,
referrer: true,
referrerPolicy: true,
integrity: true,
keepalive: true,
signal: true,
window: true,
dispatcher: true,
duplex: true,
priority: true,
};
const normalizeRequestMethod = (input) => requestMethods.includes(input) ? input.toUpperCase() : input;
const retryMethods = ['get', 'put', 'head', 'delete', 'options', 'trace'];
const retryStatusCodes = [408, 413, 429, 500, 502, 503, 504];
const retryAfterStatusCodes = [413, 429, 503];
const defaultRetryOptions = {
limit: 2,
methods: retryMethods,
statusCodes: retryStatusCodes,
afterStatusCodes: retryAfterStatusCodes,
maxRetryAfter: Number.POSITIVE_INFINITY,
backoffLimit: Number.POSITIVE_INFINITY,
delay: attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000,
};
const normalizeRetryOptions = (retry = {}) => {
if (typeof retry === 'number') {
return {
...defaultRetryOptions,
limit: retry,
};
}
if (retry.methods && !Array.isArray(retry.methods)) {
throw new Error('retry.methods must be an array');
}
if (retry.statusCodes && !Array.isArray(retry.statusCodes)) {
throw new Error('retry.statusCodes must be an array');
}
return {
...defaultRetryOptions,
...retry,
};
};
// `Promise.race()` workaround (#91)
async function timeout(request, init, abortController, options) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
if (abortController) {
abortController.abort();
}
reject(new TimeoutError(request));
}, options.timeout);
void options
.fetch(request, init)
.then(resolve)
.catch(reject)
.then(() => {
clearTimeout(timeoutId);
});
});
}
// https://github.com/sindresorhus/delay/tree/ab98ae8dfcb38e1593286c94d934e70d14a4e111
async function delay(ms, { signal }) {
return new Promise((resolve, reject) => {
if (signal) {
signal.throwIfAborted();
signal.addEventListener('abort', abortHandler, { once: true });
}
function abortHandler() {
clearTimeout(timeoutId);
reject(signal.reason);
}
const timeoutId = setTimeout(() => {
signal?.removeEventListener('abort', abortHandler);
resolve();
}, ms);
});
}
const findUnknownOptions = (request, options) => {
const unknownOptions = {};
for (const key in options) {
if (!(key in requestOptionsRegistry) && !(key in kyOptionKeys) && !(key in request)) {
unknownOptions[key] = options[key];
}
}
return unknownOptions;
};
class Ky {
static create(input, options) {
const ky = new Ky(input, options);
const function_ = async () => {
if (typeof ky._options.timeout === 'number' && ky._options.timeout > maxSafeTimeout) {
throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
}
// Delay the fetch so that body method shortcuts can set the Accept header
await Promise.resolve();
let response = await ky._fetch();
for (const hook of ky._options.hooks.afterResponse) {
// eslint-disable-next-line no-await-in-loop
const modifiedResponse = await hook(ky.request, ky._options, ky._decorateResponse(response.clone()));
if (modifiedResponse instanceof globalThis.Response) {
response = modifiedResponse;
}
}
ky._decorateResponse(response);
if (!response.ok && ky._options.throwHttpErrors) {
let error = new HTTPError(response, ky.request, ky._options);
for (const hook of ky._options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
error = await hook(error);
}
throw error;
}
// If `onDownloadProgress` is passed, it uses the stream API internally
/* istanbul ignore next */
if (ky._options.onDownloadProgress) {
if (typeof ky._options.onDownloadProgress !== 'function') {
throw new TypeError('The `onDownloadProgress` option must be a function');
}
if (!supportsResponseStreams) {
throw new Error('Streams are not supported in your environment. `ReadableStream` is missing.');
}
return ky._stream(response.clone(), ky._options.onDownloadProgress);
}
return response;
};
const isRetriableMethod = ky._options.retry.methods.includes(ky.request.method.toLowerCase());
const result = (isRetriableMethod ? ky._retry(function_) : function_());
for (const [type, mimeType] of Object.entries(responseTypes)) {
result[type] = async () => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
ky.request.headers.set('accept', ky.request.headers.get('accept') || mimeType);
const awaitedResult = await result;
const response = awaitedResult.clone();
if (type === 'json') {
if (response.status === 204) {
return '';
}
const arrayBuffer = await response.clone().arrayBuffer();
const responseSize = arrayBuffer.byteLength;
if (responseSize === 0) {
return '';
}
if (options.parseJson) {
return options.parseJson(await response.text());
}
}
return response[type]();
};
}
return result;
}
request;
abortController;
_retryCount = 0;
_input;
_options;
// eslint-disable-next-line complexity
constructor(input, options = {}) {
this._input = input;
this._options = {
...options,
headers: mergeHeaders(this._input.headers, options.headers),
hooks: mergeHooks({
beforeRequest: [],
beforeRetry: [],
beforeError: [],
afterResponse: [],
}, options.hooks),
method: normalizeRequestMethod(options.method ?? this._input.method),
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
prefixUrl: String(options.prefixUrl || ''),
retry: normalizeRetryOptions(options.retry),
throwHttpErrors: options.throwHttpErrors !== false,
timeout: options.timeout ?? 10_000,
fetch: options.fetch ?? globalThis.fetch.bind(globalThis),
};
if (typeof this._input !== 'string' && !(this._input instanceof URL || this._input instanceof globalThis.Request)) {
throw new TypeError('`input` must be a string, URL, or Request');
}
if (this._options.prefixUrl && typeof this._input === 'string') {
if (this._input.startsWith('/')) {
throw new Error('`input` must not begin with a slash when using `prefixUrl`');
}
if (!this._options.prefixUrl.endsWith('/')) {
this._options.prefixUrl += '/';
}
this._input = this._options.prefixUrl + this._input;
}
if (supportsAbortController) {
this.abortController = new globalThis.AbortController();
const originalSignal = this._options.signal ?? this._input.signal;
originalSignal?.addEventListener('abort', () => {
this.abortController.abort(originalSignal.reason);
});
this._options.signal = this.abortController.signal;
}
if (supportsRequestStreams) {
// @ts-expect-error - Types are outdated.
this._options.duplex = 'half';
}
if (this._options.json !== undefined) {
this._options.body = this._options.stringifyJson?.(this._options.json) ?? JSON.stringify(this._options.json);
this._options.headers.set('content-type', this._options.headers.get('content-type') ?? 'application/json');
}
this.request = new globalThis.Request(this._input, this._options);
if (this._options.searchParams) {
// eslint-disable-next-line unicorn/prevent-abbreviations
const textSearchParams = typeof this._options.searchParams === 'string'
? this._options.searchParams.replace(/^\?/, '')
: new URLSearchParams(this._options.searchParams).toString();
// eslint-disable-next-line unicorn/prevent-abbreviations
const searchParams = '?' + textSearchParams;
const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams);
// To provide correct form boundary, Content-Type header should be deleted each time when new Request instantiated from another one
if (((supportsFormData && this._options.body instanceof globalThis.FormData)
|| this._options.body instanceof URLSearchParams) && !(this._options.headers && this._options.headers['content-type'])) {
this.request.headers.delete('content-type');
}
// The spread of `this.request` is required as otherwise it misses the `duplex` option for some reason and throws.
this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options);
}
}
_calculateRetryDelay(error) {
this._retryCount++;
if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) {
throw error;
}
if (error instanceof HTTPError) {
if (!this._options.retry.statusCodes.includes(error.response.status)) {
throw error;
}
const retryAfter = error.response.headers.get('Retry-After')
?? error.response.headers.get('RateLimit-Reset')
?? error.response.headers.get('X-RateLimit-Reset') // GitHub
?? error.response.headers.get('X-Rate-Limit-Reset'); // Twitter
if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
let after = Number(retryAfter) * 1000;
if (Number.isNaN(after)) {
after = Date.parse(retryAfter) - Date.now();
}
else if (after >= Date.parse('2024-01-01')) {
// A large number is treated as a timestamp (fixed threshold protects against clock skew)
after -= Date.now();
}
const max = this._options.retry.maxRetryAfter ?? after;
return after < max ? after : max;
}
if (error.response.status === 413) {
throw error;
}
}
const retryDelay = this._options.retry.delay(this._retryCount);
return Math.min(this._options.retry.backoffLimit, retryDelay);
}
_decorateResponse(response) {
if (this._options.parseJson) {
response.json = async () => this._options.parseJson(await response.text());
}
return response;
}
async _retry(function_) {
try {
return await function_();
}
catch (error) {
const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout);
if (this._retryCount < 1) {
throw error;
}
await delay(ms, { signal: this._options.signal });
for (const hook of this._options.hooks.beforeRetry) {
// eslint-disable-next-line no-await-in-loop
const hookResult = await hook({
request: this.request,
options: this._options,
error: error,
retryCount: this._retryCount,
});
// If `stop` is returned from the hook, the retry process is stopped
if (hookResult === stop) {
return;
}
}
return this._retry(function_);
}
}
async _fetch() {
for (const hook of this._options.hooks.beforeRequest) {
// eslint-disable-next-line no-await-in-loop
const result = await hook(this.request, this._options);
if (result instanceof Request) {
this.request = result;
break;
}
if (result instanceof Response) {
return result;
}
}
const nonRequestOptions = findUnknownOptions(this.request, this._options);
// Cloning is done here to prepare in advance for retries
const mainRequest = this.request;
this.request = mainRequest.clone();
if (this._options.timeout === false) {
return this._options.fetch(mainRequest, nonRequestOptions);
}
return timeout(mainRequest, nonRequestOptions, this.abortController, this._options);
}
/* istanbul ignore next */
_stream(response, onDownloadProgress) {
const totalBytes = Number(response.headers.get('content-length')) || 0;
let transferredBytes = 0;
if (response.status === 204) {
if (onDownloadProgress) {
onDownloadProgress({ percent: 1, totalBytes, transferredBytes }, new Uint8Array());
}
return new globalThis.Response(null, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
return new globalThis.Response(new globalThis.ReadableStream({
async start(controller) {
const reader = response.body.getReader();
if (onDownloadProgress) {
onDownloadProgress({ percent: 0, transferredBytes: 0, totalBytes }, new Uint8Array());
}
async function read() {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
if (onDownloadProgress) {
transferredBytes += value.byteLength;
const percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes;
onDownloadProgress({ percent, transferredBytes, totalBytes }, value);
}
controller.enqueue(value);
await read();
}
await read();
},
}), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
}
/*! MIT License © Sindre Sorhus */
const createInstance = (defaults) => {
// eslint-disable-next-line @typescript-eslint/promise-function-async
const ky = (input, options) => Ky.create(input, validateAndMerge(defaults, options));
for (const method of requestMethods) {
// eslint-disable-next-line @typescript-eslint/promise-function-async
ky[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method }));
}
ky.create = (newDefaults) => createInstance(validateAndMerge(newDefaults));
ky.extend = (newDefaults) => {
if (typeof newDefaults === 'function') {
newDefaults = newDefaults(defaults ?? {});
}
return createInstance(validateAndMerge(defaults, newDefaults));
};
ky.stop = stop;
return ky;
};
const ky$1 = createInstance();
var distribution = /*#__PURE__*/Object.freeze({
__proto__: null,
HTTPError: HTTPError,
TimeoutError: TimeoutError,
default: ky$1
});
var require$$1 = /*@__PURE__*/getAugmentedNamespace(distribution);
const ENDPOINT = {
FREE: 'https://api.microlink.io/',
PRO: 'https://pro.microlink.io/'
};
const isObject = input => input !== null && typeof input === 'object';
const isBuffer = input =>
input != null &&
input.constructor != null &&
typeof input.constructor.isBuffer === 'function' &&
input.constructor.isBuffer(input);
const parseBody = (input, error, url) => {
try {
return JSON.parse(input)
} catch (_) {
const message = input || error.message;
return {
status: 'error',
data: { url: message },
more: 'https://microlink.io/efatalclient',
code: 'EFATALCLIENT',
message,
url
}
}
};
const isURL = url => {
try {
return /^https?:\/\//i.test(new URL(url).href)
} catch (_) {
return false
}
};
const factory$1 = streamResponseType => ({
VERSION,
MicrolinkError,
got,
flatten
}) => {
const assertUrl = (url = '') => {
if (!isURL(url)) {
const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`;
throw new MicrolinkError({
status: 'fail',
data: { url: message },
more: 'https://microlink.io/einvalurlclient',
code: 'EINVALURLCLIENT',
message,
url
})
}
};
const mapRules = rules => {
if (!isObject(rules)) return
const flatRules = flatten(rules);
return Object.keys(flatRules).reduce((acc, key) => {
acc[`data.${key}`] = flatRules[key].toString();
return acc
}, {})
};
const fetchFromApi = async (apiUrl, opts = {}) => {
try {
const response = await got(apiUrl, opts);
return opts.responseType === streamResponseType
? response
: { ...response.body, response }
} catch (error) {
const { response = {} } = error;
const {
statusCode,
body: rawBody,
headers = {},
url: uri = apiUrl
} = response;
const isBodyBuffer = isBuffer(rawBody);
const body =
isObject(rawBody) && !isBodyBuffer
? rawBody
: parseBody(isBodyBuffer ? rawBody.toString() : rawBody, error, uri);
throw new MicrolinkError({
...body,
message: body.message,
url: uri,
statusCode,
headers
})
}
};
const getApiUrl = (
url,
{ data, apiKey, endpoint, retry, cache, ...opts } = {},
{ responseType = 'json', headers: gotHeaders, ...gotOpts } = {}
) => {
const isPro = !!apiKey;
const apiEndpoint = endpoint || ENDPOINT[isPro ? 'PRO' : 'FREE'];
const apiUrl = `${apiEndpoint}?${new URLSearchParams({
url,
...mapRules(data),
...flatten(opts)
}).toString()}`;
const headers = isPro
? { ...gotHeaders, 'x-api-key': apiKey }
: { ...gotHeaders };
if (opts.stream) {
responseType = streamResponseType;
}
return [apiUrl, { ...gotOpts, responseType, cache, retry, headers }]
};
const createMql = defaultOpts => async (url, opts, gotOpts) => {
assertUrl(url);
const [apiUrl, fetchOpts] = getApiUrl(url, opts, {
...defaultOpts,
...gotOpts
});
return fetchFromApi(apiUrl, fetchOpts)
};
const mql = createMql();
mql.extend = createMql;
mql.MicrolinkError = MicrolinkError;
mql.getApiUrl = getApiUrl;
mql.fetchFromApi = fetchFromApi;
mql.mapRules = mapRules;
mql.version = VERSION;
mql.stream = got.stream;
return mql
};
var factory_1 = factory$1;
const { flattie: flatten } = dist;
const { default: ky } = require$$1;
const factory = factory_1('arrayBuffer');
class MicrolinkError extends Error {
constructor (props) {
super();
this.name = 'MicrolinkError';
Object.assign(this, props);
this.description = this.message;
this.message = this.code
? `${this.code}, ${this.description}`
: this.description;
}
}
const got = async (url, { responseType, ...opts }) => {
try {
if (opts.timeout === undefined) opts.timeout = false;
const response = await ky(url, opts);
const body = await response[responseType]();
const { headers, status: statusCode } = response;
return { url: response.url, body, headers, statusCode }
} catch (error) {
if (error.response) {
const { response } = error;
error.response = {
...response,
headers: Array.from(response.headers.entries()).reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc
},
{}
),
statusCode: response.status,
body: await response.text()
};
}
throw error
}
};
got.stream = (...args) => ky(...args).then(res => res.body);
const mql = factory({
MicrolinkError,
got,
flatten,
VERSION: '0.13.14'
});
lightweight$1.exports = mql;
var arrayBuffer = lightweight$1.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' });
var extend = lightweight$1.exports.extend = mql.extend;
var fetchFromApi = lightweight$1.exports.fetchFromApi = mql.fetchFromApi;
var getApiUrl = lightweight$1.exports.getApiUrl = mql.getApiUrl;
var mapRules = lightweight$1.exports.mapRules = mql.mapRules;
var MicrolinkError_1 = lightweight$1.exports.MicrolinkError = mql.MicrolinkError;
var version = lightweight$1.exports.version = mql.version;
var lightweightExports = lightweight$1.exports;
var lightweight = /*@__PURE__*/getDefaultExportFromCjs(lightweightExports);
export { MicrolinkError_1 as MicrolinkError, arrayBuffer, lightweight as default, extend, fetchFromApi, getApiUrl, mapRules, version };