@cloudflare/db-connect
Version:
Connect your SQL database to Cloudflare Workers.
411 lines (404 loc) • 16.6 kB
JavaScript
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); 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
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
/**
* DbConnect, access your SQL database from Cloudflare Workers or the browser.
*
* @example
* const db = new DbConnect('sql.mysite.com')
*
* db.ping()
* db.exec('CREATE TABLE firewall VALUES (ip INT)')
* db.query('INSERT INTO firewall VALUES (?), (?)', [1111, 1001])
* db.query('SELECT COUNT(*) FROM firewall', {cacheTtl: 60})
*
* async function do() {
* const resp = await db.query('SELECT * FROM firewall')
* if(resp.ok) {
* const rows = await resp.json()
* // [ { "ip": 1111 }, { "ip": 1001 } ]
* }
* }
*
* @author Ashcon Partovi
* @copyright Cloudflare, Inc.
*/
var DbConnect = /** @class */ (function () {
/**
* Creates a DbConnect instance with host and credentials.
*
* @param host required, hostname or url of your Argo Tunnel running in db-connect mode.
* @param clientId recommended, client id of the Access policy for your host.
* @param clientSecret recommended, client secret of the Access policy for your host.
*/
function DbConnect(parameters) {
var init = new DbConnectInit(parameters);
var url = new URL(init.host);
var headers = new Headers();
if (init.clientId) {
headers.set('Cf-access-client-id', init.clientId);
headers.set('Cf-access-client-secret', init.clientSecret);
}
this.httpClient = new HttpClient(url, {
headers: headers,
keepalive: true,
cf: {
cacheEverything: true
}
});
}
/**
* Ping tests the connection to the database.
*
* To reduce latency, pings will be served stale for up to 3 seconds.
*
* @example
* const db = new DbConnect({...})
*
* async function doPing() {
* const resp = await db.ping()
* if(resp.ok) {
* return true
* }
* throw new Error(await resp.text())
* }
*/
DbConnect.prototype.ping = function () {
return __awaiter(this, void 0, Promise, function () {
return __generator(this, function (_a) {
return [2 /*return*/, this.httpClient.fetch('ping', { method: 'GET' }, 0, 3)];
});
});
};
/**
* Submit sends a Command to the database and fetches a Response.
*
* @example
* const db = new DbConnect({...})
*
* async function doSubmit() {
* const cmd = new Command('SELECT * FROM users WHERE name = ? AND age > ?', ['matthew', 21])
* const resp = await db.submit(cmd)
* if(resp.ok) {
* return await resp.json()
* }
* throw new Error(await resp.text())
* }
*
* @param command required, the command to submit.
* @param cacheTtl optional, number of seconds to cache the response.
* @param staleTtl optional, number of seconds to serve the response while stale.
*/
DbConnect.prototype.submit = function (command) {
return __awaiter(this, void 0, Promise, function () {
var cmd, init;
return __generator(this, function (_a) {
if (!(command instanceof Command))
command = new Command(command);
cmd = command;
init = {
method: 'POST',
body: JSON.stringify(cmd),
headers: { 'Content-type': 'application/json' }
};
return [2 /*return*/, this.httpClient.fetch('submit', init, cmd.cacheTtl, cmd.staleTtl)];
});
});
};
return DbConnect;
}());
/**
* Initializer for DbConnect with host and credentials.
*
* @see DbConnect
*/
var DbConnectInit = /** @class */ (function () {
function DbConnectInit(parameters) {
Object.assign(this, parameters);
if (!this.host)
throw new TypeError('host is a required argument');
if (!this.host.startsWith('http'))
this.host = "https://" + this.host;
if (!this.clientId != !this.clientSecret)
throw new TypeError('both clientId and clientSecret must be specified');
}
return DbConnectInit;
}());
/**
* Command is a standard, non-vendor format for submitting database commands.
*/
var Command = /** @class */ (function () {
/**
* Creates a new database Command.
*
* @param statement required, statement of the command.
* @param args an array or map of arguments, defaults to an empty array.
* @param mode mode of the command, defaults to 'query'.
* @param isolation isolation of the command, defaults to 'default'.
* @param timeout timeout in seconds of the command, defaults to indefinite.
* @param cacheTtl number of seconds to cache responses, defaults to -1.
* @param staleTtl after cacheTtl expires, number of seconds to serve stale responses.
*/
function Command(parameters) {
var init = new CommandInit(parameters);
Object.assign(this, init);
}
return Command;
}());
/**
* Initializer for Command with statement and options.
*
* @see Command
*/
var CommandInit = /** @class */ (function () {
function CommandInit(parameters) {
Object.assign(this, parameters);
if (!this.statement)
throw new TypeError('statement is a required argument');
if (!this.arguments)
this.arguments = [];
if (!this.mode)
this.mode = Mode.query;
if (!this.timeout)
this.timeout = 0;
if (!this.isolation)
this.isolation = Isolation.none;
if (!this.cacheTtl)
this.cacheTtl = -1;
if (!this.staleTtl)
this.staleTtl = this.cacheTtl;
}
return CommandInit;
}());
/**
* Mode is a kind of Command.
* * query, a request for a set of rows or objects.
* * exec, an execution that returns a single result.
*
* @link https://golang.org/pkg/database/sql/#DB.Exec
*/
var Mode;
(function (Mode) {
Mode["query"] = "query";
Mode["exec"] = "exec";
})(Mode || (Mode = {}));
/**
* Isolation is a transaction type when executing a Command.
*
* @link https://golang.org/pkg/database/sql/#IsolationLevel
*/
var Isolation;
(function (Isolation) {
Isolation["none"] = "none";
Isolation["default"] = "default";
Isolation["readUncommitted"] = "read_uncommitted";
Isolation["readCommitted"] = "read_committed";
Isolation["writeCommitted"] = "write_committed";
Isolation["repeatableRead"] = "repeatable_read";
Isolation["snapshot"] = "snapshot";
Isolation["serializable"] = "serializable";
Isolation["linearizable"] = "linearizable";
})(Isolation || (Isolation = {}));
/**
* HttpClient is a convience wrapper for doing common transforms,
* such as injecting authentication headers, to fetch requests.
*/
var HttpClient = /** @class */ (function () {
/**
* Creates a new HttpClient.
*
* @param url required, the base url of all requests.
* @param init initializer for requests, defaults to empty.
* @param cache cache storage for requests, defaults to global.
*/
function HttpClient(url, init, cache) {
if (!url)
throw new TypeError('url is a required argument');
this.url = url;
this.init = init || {};
if (!this.init.headers)
this.init.headers = {};
this.cache = cache || caches.default;
}
/**
* Fetch a path from the origin or cache.
*
* @param path required, the path to fetch, joined by the client url.
* @param init initializer for the request, recursively merges with client initializer.
* @param cacheTtl required, number of seconds to cache the response.
* @param staleTtl required, number of seconds to serve the response stale.
*/
HttpClient.prototype.fetch = function (path, init, cacheTtl, staleTtl) {
return __awaiter(this, void 0, Promise, function () {
var key, response;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.cacheKey(path, init)];
case 1:
key = _a.sent();
return [4 /*yield*/, this.cache.match(key, { ignoreMethod: true })];
case 2:
response = _a.sent();
if (!!response) return [3 /*break*/, 5];
return [4 /*yield*/, this.fetchOrigin(path, init)];
case 3:
response = _a.sent();
response.headers.set('Cache-control', this.cacheHeader(cacheTtl, staleTtl));
return [4 /*yield*/, this.cache.put(key, response.clone())];
case 4:
_a.sent();
_a.label = 5;
case 5: return [2 /*return*/, response];
}
});
});
};
/**
* Fetch a path directly from the origin.
*
* @param path required, the path to fetch, joined by the client url.
* @param init initializer for the request, recursively merges with client initializer.
*/
HttpClient.prototype.fetchOrigin = function (path, init) {
return __awaiter(this, void 0, Promise, function () {
var response;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
path = new URL(path, this.url).toString();
init = this.initMerge(init);
return [4 /*yield*/, fetch(path, init)
// FIXME: access sometimes redirects to a 200 login page when client credentials are invalid.
];
case 1:
response = _a.sent();
// FIXME: access sometimes redirects to a 200 login page when client credentials are invalid.
if (response.redirected && new URL(response.url).hostname.endsWith('cloudflareaccess.com')) {
return [2 /*return*/, new Response('client credentials rejected by cloudflare access', response)];
}
return [2 /*return*/, new Response(response.body, response)];
}
});
});
};
/**
* Creates a new RequestInit for requests.
*
* @param init the initializer to merge into the client initializer.
*/
HttpClient.prototype.initMerge = function (init) {
init = Object.assign({ headers: {} }, init || {});
for (var _i = 0, _a = Object.entries(this.init.headers); _i < _a.length; _i++) {
var kv = _a[_i];
init.headers[kv[0]] = [1];
}
return Object.assign(init, this.init);
};
/**
* Creates a cache key for a Request.
*
* @param path required, the resource path of the request.
* @param init the initializer for the request, defaults to empty.
*/
HttpClient.prototype.cacheKey = function (path, init) {
return __awaiter(this, void 0, Promise, function () {
var hash;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
path = new URL(path, this.url).toString();
init = this.initMerge(init);
if (init.method != 'POST')
return [2 /*return*/, new Request(path, init)];
return [4 /*yield*/, sha256(init.body)];
case 1:
hash = _a.sent();
return [2 /*return*/, new Request(path + "/_/" + hash, { method: 'GET', headers: init.headers })];
}
});
});
};
/**
* Creates a Cache-control header for a Response.
*
* @param cacheTtl required, number of seconds to cache the response.
* @param staleTtl required, number of seconds to serve the response stale.
*/
HttpClient.prototype.cacheHeader = function (cacheTtl, staleTtl) {
var cache = 'public';
if (cacheTtl < 0 && staleTtl < 0)
cache = 'no-store';
if (cacheTtl >= 0)
cache += ", max-age=" + cacheTtl;
if (staleTtl >= 0)
cache += ", stale-while-revalidate=" + staleTtl;
return cache;
};
return HttpClient;
}());
/**
* Generate a SHA-256 hash of any object.
*
* @param object the object to generate a hash.
*/
function sha256(object) {
return __awaiter(this, void 0, Promise, function () {
var buffer, hashBuffer, hashArray;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
buffer = new TextEncoder().encode(JSON.stringify(object));
return [4 /*yield*/, crypto.subtle.digest('SHA-256', buffer)];
case 1:
hashBuffer = _a.sent();
hashArray = Array.from(new Uint8Array(hashBuffer));
return [2 /*return*/, hashArray.map(function (b) { return ('00' + b.toString(16)).slice(-2); }).join('')];
}
});
});
}
export { DbConnect, Command, Mode, Isolation };