supertokens-node
Version:
NodeJS driver for SuperTokens core
588 lines (587 loc) • 28.5 kB
JavaScript
"use strict";
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Querier = void 0;
/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
const utils_1 = require("./utils");
const version_1 = require("./version");
const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath"));
const processState_1 = require("./processState");
const constants_1 = require("./constants");
const logger_1 = require("./logger");
const supertokens_1 = __importDefault(require("./supertokens"));
class Querier {
// we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able
// to support multiple rIds per API
constructor(hosts, rIdToCore) {
this.getAPIVersion = async (userContext) => {
var _a;
if (Querier.apiVersion !== undefined) {
return Querier.apiVersion;
}
processState_1.ProcessState.getInstance().addState(
processState_1.PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION
);
const st = supertokens_1.default.getInstanceOrThrowError();
const appInfo = st.appInfo;
const request = st.getRequestFromUserContext(userContext);
const queryParamsObj = {
apiDomain: appInfo.apiDomain.getAsStringDangerous(),
websiteDomain: appInfo.getOrigin({ request, userContext }).getAsStringDangerous(),
};
const queryParams = new URLSearchParams(queryParamsObj).toString();
let { body: response } = await this.sendRequestHelper(
new normalisedURLPath_1.default("/apiversion"),
"GET",
async (url) => {
let headers = {};
if (Querier.apiKey !== undefined) {
headers = {
"api-key": Querier.apiKey,
};
}
if (Querier.networkInterceptor !== undefined) {
let request = Querier.networkInterceptor(
{
url: url,
method: "get",
headers: headers,
params: queryParamsObj,
},
userContext
);
url = request.url;
headers = request.headers;
}
let response = await (0, utils_1.doFetch)(url + `?${queryParams}`, {
method: "GET",
headers,
});
return response;
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
let cdiSupportedByServer = response.versions;
let supportedVersion = (0, utils_1.getLargestVersionFromIntersection)(
cdiSupportedByServer,
version_1.cdiSupported
);
if (supportedVersion === undefined) {
throw Error(
"The running SuperTokens core version is not compatible with this NodeJS SDK. Please visit https://supertokens.io/docs/community/compatibility to find the right versions"
);
}
Querier.apiVersion = supportedVersion;
return Querier.apiVersion;
};
this.getHostsAliveForTesting = () => {
if (!(0, utils_1.isTestEnv)()) {
throw Error("calling testing function in non testing env");
}
return Querier.hostsAliveForTesting;
};
this.getPath = (path) => {
var _a;
const template = typeof path === "string" ? path : path.path;
const params = typeof path === "string" ? {} : (_a = path.params) !== null && _a !== void 0 ? _a : {};
let populated = String(template);
for (const [key, value] of Object.entries(params)) {
populated = populated.replace(new RegExp(`<${key}>`, "g"), String(value));
}
return new normalisedURLPath_1.default(populated);
};
// path should start with "/"
this.sendPostRequest = async (templatePath, body, userContext) => {
var _a;
this.invalidateCoreCallCache(userContext);
const path = this.getPath(templatePath);
const { body: respBody } = await this.sendRequestHelper(
path,
"POST",
async (url) => {
let apiVersion = await this.getAPIVersion(userContext);
let headers = {
"cdi-version": apiVersion,
};
headers["content-type"] = "application/json; charset=utf-8";
if (Querier.apiKey !== undefined) {
headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey });
}
if (path.isARecipePath() && this.rIdToCore !== undefined) {
headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore });
}
if (Querier.networkInterceptor !== undefined) {
let request = Querier.networkInterceptor(
{
url: url,
method: "post",
headers: headers,
body: body,
},
userContext
);
url = request.url;
headers = request.headers;
if (request.body !== undefined) {
body = request.body;
}
}
return (0, utils_1.doFetch)(url, {
method: "POST",
body: body !== undefined ? JSON.stringify(body) : undefined,
headers,
});
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
return respBody;
};
// path should start with "/"
this.sendDeleteRequest = async (templatePath, body, params, userContext) => {
var _a;
this.invalidateCoreCallCache(userContext);
const path = this.getPath(templatePath);
const { body: respBody } = await this.sendRequestHelper(
path,
"DELETE",
async (url) => {
let apiVersion = await this.getAPIVersion(userContext);
let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" };
if (Querier.apiKey !== undefined) {
headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey });
}
if (path.isARecipePath() && this.rIdToCore !== undefined) {
headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore });
}
if (Querier.networkInterceptor !== undefined) {
let request = Querier.networkInterceptor(
{
url: url,
method: "delete",
headers: headers,
params: params,
body: body,
},
userContext
);
url = request.url;
headers = request.headers;
if (request.body !== undefined) {
body = request.body;
}
if (request.params !== undefined) {
params = request.params;
}
}
const finalURL = new URL(url);
const searchParams = new URLSearchParams(params);
finalURL.search = searchParams.toString();
return (0, utils_1.doFetch)(finalURL.toString(), {
method: "DELETE",
body: body !== undefined ? JSON.stringify(body) : undefined,
headers,
});
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
return respBody;
};
// path should start with "/"
this.sendGetRequest = async (templatePath, params, userContext) => {
var _a;
const path = this.getPath(templatePath);
const { body: respBody } = await this.sendRequestHelper(
path,
"GET",
async (url) => {
var _a, _b, _c, _d;
let apiVersion = await this.getAPIVersion(userContext);
let headers = { "cdi-version": apiVersion };
if (Querier.apiKey !== undefined) {
headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey });
}
if (path.isARecipePath() && this.rIdToCore !== undefined) {
headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore });
}
/* CACHE CHECK BEGIN */
const sortedKeys = Object.keys(params).sort();
const sortedHeaderKeys = Object.keys(headers).sort();
let uniqueKey = path.getAsStringDangerous();
for (const key of sortedKeys) {
const value = params[key];
uniqueKey += `;${key}=${value}`;
}
uniqueKey += ";hdrs";
for (const key of sortedHeaderKeys) {
const value = headers[key];
uniqueKey += `;${key}=${value}`;
}
// If globalCacheTag doesn't match the current one (or if it's not defined), we invalidate the cache, because that means
// that there was a non-GET call that didn't have a proper userContext passed to it.
// However, we do not want to invalidate all global caches for a GET call even if it was made without a proper user context.
if (
((_a = userContext._default) === null || _a === void 0 ? void 0 : _a.globalCacheTag) !==
Querier.globalCacheTag
) {
this.invalidateCoreCallCache(userContext, false);
}
(0, logger_1.logDebugMessage)("Checking cache existence");
if (
!Querier.disableCache &&
uniqueKey in
((_c =
(_b = userContext._default) === null || _b === void 0 ? void 0 : _b.coreCallCache) !==
null && _c !== void 0
? _c
: {})
) {
// Clone the cached response before returning it
const cachedResponse = userContext._default.coreCallCache[uniqueKey];
return cachedResponse.clone();
}
/* CACHE CHECK END */
(0, logger_1.logDebugMessage)("Cache does not exist, making network request");
if (Querier.networkInterceptor !== undefined) {
(0, logger_1.logDebugMessage)("Network interceptor found, applying interceptor");
let request = Querier.networkInterceptor(
{
url: url,
method: "get",
headers: headers,
params: params,
},
userContext
);
url = request.url;
headers = request.headers;
if (request.params !== undefined) {
params = request.params;
}
}
const finalURL = new URL(url);
const searchParams = new URLSearchParams(
Object.entries(params).filter(([_, value]) => value !== undefined)
);
finalURL.search = searchParams.toString();
// Update cache and return
let response = await (0, utils_1.doFetch)(finalURL.toString(), {
method: "GET",
headers,
});
if (response.status === 302) {
return response;
}
if (response.status === 200 && !Querier.disableCache) {
// Clone the response before caching it
const responseClone = response.clone();
userContext._default = Object.assign(Object.assign({}, userContext._default), {
coreCallCache: Object.assign(
Object.assign(
{},
(_d = userContext._default) === null || _d === void 0 ? void 0 : _d.coreCallCache
),
{ [uniqueKey]: responseClone }
),
globalCacheTag: Querier.globalCacheTag,
});
}
return response;
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
return respBody;
};
this.sendGetRequestWithResponseHeaders = async (templatePath, params, inpHeaders, userContext) => {
var _a;
const path = this.getPath(templatePath);
return await this.sendRequestHelper(
path,
"GET",
async (url) => {
let apiVersion = await this.getAPIVersion(userContext);
let headers = inpHeaders !== null && inpHeaders !== void 0 ? inpHeaders : {};
headers["cdi-version"] = apiVersion;
if (Querier.apiKey !== undefined) {
headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey });
}
if (path.isARecipePath() && this.rIdToCore !== undefined) {
headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore });
}
if (Querier.networkInterceptor !== undefined) {
let request = Querier.networkInterceptor(
{
url: url,
method: "get",
headers: headers,
params: params,
},
userContext
);
url = request.url;
headers = request.headers;
if (request.params !== undefined) {
params = request.params;
}
}
const finalURL = new URL(url);
const searchParams = new URLSearchParams(
Object.entries(params).filter(([_, value]) => value !== undefined)
);
finalURL.search = searchParams.toString();
return (0, utils_1.doFetch)(finalURL.toString(), {
method: "GET",
headers,
});
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
};
// path should start with "/"
this.sendPutRequest = async (templatePath, body, params, userContext) => {
var _a;
this.invalidateCoreCallCache(userContext);
const path = this.getPath(templatePath);
const { body: respBody } = await this.sendRequestHelper(
path,
"PUT",
async (url) => {
let apiVersion = await this.getAPIVersion(userContext);
let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" };
if (Querier.apiKey !== undefined) {
headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey });
}
if (path.isARecipePath() && this.rIdToCore !== undefined) {
headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore });
}
if (Querier.networkInterceptor !== undefined) {
let request = Querier.networkInterceptor(
{
url: url,
method: "put",
headers: headers,
body: body,
params: params,
},
userContext
);
url = request.url;
headers = request.headers;
if (request.body !== undefined) {
body = request.body;
}
}
const finalURL = new URL(url);
const searchParams = new URLSearchParams(
Object.entries(params).filter(([_, value]) => value !== undefined)
);
finalURL.search = searchParams.toString();
return (0, utils_1.doFetch)(finalURL.toString(), {
method: "PUT",
body: body !== undefined ? JSON.stringify(body) : undefined,
headers,
});
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
return respBody;
};
// path should start with "/"
this.sendPatchRequest = async (templatePath, body, userContext) => {
var _a;
this.invalidateCoreCallCache(userContext);
const path = this.getPath(templatePath);
const { body: respBody } = await this.sendRequestHelper(
path,
"PATCH",
async (url) => {
let apiVersion = await this.getAPIVersion(userContext);
let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" };
if (Querier.apiKey !== undefined) {
headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey });
}
if (path.isARecipePath() && this.rIdToCore !== undefined) {
headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore });
}
if (Querier.networkInterceptor !== undefined) {
let request = Querier.networkInterceptor(
{
url: url,
method: "patch",
headers: headers,
body: body,
},
userContext
);
url = request.url;
headers = request.headers;
if (request.body !== undefined) {
body = request.body;
}
}
return (0, utils_1.doFetch)(url, {
method: "PATCH",
body: body !== undefined ? JSON.stringify(body) : undefined,
headers,
});
},
((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0
);
return respBody;
};
this.invalidateCoreCallCache = (userContext, updGlobalCacheTagIfNecessary = true) => {
var _a;
if (
updGlobalCacheTagIfNecessary &&
((_a = userContext._default) === null || _a === void 0 ? void 0 : _a.keepCacheAlive) !== true
) {
Querier.globalCacheTag = Date.now();
}
userContext._default = Object.assign(Object.assign({}, userContext._default), { coreCallCache: {} });
};
// path should start with "/"
this.sendRequestHelper = async (path, method, requestFunc, numberOfTries, retryInfoMap) => {
var _a;
if (this.__hosts === undefined) {
throw Error(
"No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using."
);
}
if (numberOfTries === 0) {
throw Error("No SuperTokens core available to query");
}
let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous();
let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous();
let strPath = path.getAsStringDangerous();
const url = currentDomain + currentBasePath + strPath;
const maxRetries = 5;
if (retryInfoMap === undefined) {
retryInfoMap = {};
}
if (retryInfoMap[url] === undefined) {
retryInfoMap[url] = maxRetries;
}
Querier.lastTriedIndex++;
Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length;
try {
processState_1.ProcessState.getInstance().addState(
processState_1.PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER
);
(0, logger_1.logDebugMessage)(`core-call: ${method} ${url}`);
let response = await requestFunc(url);
if ((0, utils_1.isTestEnv)()) {
Querier.hostsAliveForTesting.add(currentDomain + currentBasePath);
}
if (response.status !== 200) {
throw response;
}
if (
(_a = response.headers.get("content-type")) === null || _a === void 0
? void 0
: _a.startsWith("text")
) {
return { body: await response.clone().text(), headers: response.headers };
}
return { body: await response.clone().json(), headers: response.headers };
} catch (err) {
if (
err.message !== undefined &&
(err.message.includes("Failed to fetch") ||
err.message.includes("fetch failed") ||
err.message.includes("ECONNREFUSED") ||
err.code === "ECONNREFUSED")
) {
return this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap);
}
if ("status" in err && "text" in err) {
if (err.status === constants_1.RATE_LIMIT_STATUS_CODE) {
const retriesLeft = retryInfoMap[url];
if (retriesLeft > 0) {
retryInfoMap[url] = retriesLeft - 1;
const attemptsMade = maxRetries - retriesLeft;
const delay = 10 + 250 * attemptsMade;
await new Promise((resolve) => setTimeout(resolve, delay));
return this.sendRequestHelper(path, method, requestFunc, numberOfTries, retryInfoMap);
}
}
throw new Error(
"SuperTokens core threw an error for a " +
method +
" request to path: '" +
path.getAsStringDangerous() +
"' with status code: " +
err.status +
" and message: " +
(await err.text())
);
}
throw err;
}
};
this.__hosts = hosts;
this.rIdToCore = rIdToCore;
}
static reset() {
if (!(0, utils_1.isTestEnv)()) {
throw Error("calling testing function in non testing env");
}
Querier.initCalled = false;
Querier.apiVersion = undefined;
}
static getNewInstanceOrThrowError(rIdToCore) {
if (!Querier.initCalled) {
throw Error("Please call the supertokens.init function before using SuperTokens");
}
return new Querier(Querier.hosts, rIdToCore);
}
static init(hosts, apiKey, networkInterceptor, disableCache) {
if (!Querier.initCalled) {
(0, logger_1.logDebugMessage)("querier initialized");
Querier.initCalled = true;
Querier.hosts = hosts;
Querier.apiKey = apiKey;
Querier.apiVersion = undefined;
Querier.lastTriedIndex = 0;
Querier.hostsAliveForTesting = new Set();
Querier.networkInterceptor = networkInterceptor;
Querier.disableCache = disableCache !== null && disableCache !== void 0 ? disableCache : false;
}
}
getAllCoreUrlsForPath(path) {
if (this.__hosts === undefined) {
return [];
}
const normalisedPath = new normalisedURLPath_1.default(path);
return this.__hosts.map((h) => {
const currentDomain = h.domain.getAsStringDangerous();
const currentBasePath = h.basePath.getAsStringDangerous();
return currentDomain + currentBasePath + normalisedPath.getAsStringDangerous();
});
}
}
exports.Querier = Querier;
Querier.initCalled = false;
Querier.hosts = undefined;
Querier.apiKey = undefined;
Querier.apiVersion = undefined;
Querier.lastTriedIndex = 0;
Querier.hostsAliveForTesting = new Set();
Querier.networkInterceptor = undefined;
Querier.globalCacheTag = Date.now();
Querier.disableCache = false;