@planetscale/database
Version:
A Fetch API-compatible PlanetScale database driver
196 lines (195 loc) • 6.68 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.connect = exports.Connection = exports.Client = exports.DatabaseError = exports.hex = exports.format = exports.cast = void 0;
const cast_js_1 = require("./cast.js");
var cast_js_2 = require("./cast.js");
Object.defineProperty(exports, "cast", { enumerable: true, get: function () { return cast_js_2.cast; } });
const sanitization_js_1 = require("./sanitization.js");
var sanitization_js_2 = require("./sanitization.js");
Object.defineProperty(exports, "format", { enumerable: true, get: function () { return sanitization_js_2.format; } });
var text_js_1 = require("./text.js");
Object.defineProperty(exports, "hex", { enumerable: true, get: function () { return text_js_1.hex; } });
const version_js_1 = require("./version.js");
class DatabaseError extends Error {
constructor(message, status, body) {
super(message);
this.status = status;
this.name = 'DatabaseError';
this.body = body;
}
}
exports.DatabaseError = DatabaseError;
class Client {
constructor(config) {
this.config = config;
}
async transaction(fn) {
return this.connection().transaction(fn);
}
async execute(query, args = null, options = { as: 'object' }) {
return this.connection().execute(query, args, options);
}
connection() {
return new Connection(this.config);
}
}
exports.Client = Client;
class Tx {
constructor(conn) {
this.conn = conn;
}
async execute(query, args = null, options = { as: 'object' }) {
return this.conn.execute(query, args, options);
}
}
function protocol(protocol) {
return protocol === 'http:' ? protocol : 'https:';
}
function buildURL(url) {
const scheme = `${protocol(url.protocol)}//`;
return new URL(url.pathname, `${scheme}${url.host}`).toString();
}
class Connection {
constructor(config) {
this.config = config;
this.fetch = config.fetch || fetch;
this.session = null;
if (config.url) {
const url = new URL(config.url);
this.config.username = url.username;
this.config.password = url.password;
this.config.host = url.hostname;
this.url = buildURL(url);
}
else {
this.url = new URL(`https://${this.config.host}`).toString();
}
}
async transaction(fn) {
const conn = new Connection(this.config);
const tx = new Tx(conn);
try {
await tx.execute('BEGIN');
const res = await fn(tx);
await tx.execute('COMMIT');
return res;
}
catch (err) {
await tx.execute('ROLLBACK');
throw err;
}
}
async refresh() {
await this.createSession();
}
async execute(query, args = null, options = { as: 'object' }) {
const url = new URL('/psdb.v1alpha1.Database/Execute', this.url);
const formatter = this.config.format || sanitization_js_1.format;
const sql = args ? formatter(query, args) : query;
const saved = await postJSON(this.config, this.fetch, url, {
query: sql,
session: this.session
});
const { result, session, error, timing } = saved;
if (session) {
this.session = session;
}
if (error) {
throw new DatabaseError(error.message, 400, error);
}
const rowsAffected = result?.rowsAffected ? parseInt(result.rowsAffected, 10) : 0;
const insertId = result?.insertId ?? '0';
const fields = result?.fields ?? [];
for (const field of fields) {
field.type || (field.type = 'NULL');
}
const castFn = options.cast || this.config.cast || cast_js_1.cast;
const rows = result ? parse(result, castFn, options.as || 'object') : [];
const headers = fields.map((f) => f.name);
const typeByName = (acc, { name, type }) => ({ ...acc, [name]: type });
const types = fields.reduce(typeByName, {});
const timingSeconds = timing ?? 0;
return {
headers,
types,
fields,
rows,
rowsAffected,
insertId,
size: rows.length,
statement: sql,
time: timingSeconds * 1000
};
}
async createSession() {
const url = new URL('/psdb.v1alpha1.Database/CreateSession', this.url);
const { session } = await postJSON(this.config, this.fetch, url);
this.session = session;
return session;
}
}
exports.Connection = Connection;
async function postJSON(config, fetch, url, body = {}) {
const auth = btoa(`${config.username}:${config.password}`);
const response = await fetch(url.toString(), {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'User-Agent': `database-js/${version_js_1.Version}`,
Authorization: `Basic ${auth}`
},
cache: 'no-store'
});
if (response.ok) {
return await response.json();
}
else {
let error = null;
try {
const e = (await response.json()).error;
error = new DatabaseError(e.message, response.status, e);
}
catch {
error = new DatabaseError(response.statusText, response.status, {
code: 'internal',
message: response.statusText
});
}
throw error;
}
}
function connect(config) {
return new Connection(config);
}
exports.connect = connect;
function parseArrayRow(fields, rawRow, cast) {
const row = decodeRow(rawRow);
return fields.map((field, ix) => {
return cast(field, row[ix]);
});
}
function parseObjectRow(fields, rawRow, cast) {
const row = decodeRow(rawRow);
return fields.reduce((acc, field, ix) => {
acc[field.name] = cast(field, row[ix]);
return acc;
}, {});
}
function parse(result, cast, returnAs) {
const fields = result.fields ?? [];
const rows = result.rows ?? [];
return rows.map((row) => returnAs === 'array' ? parseArrayRow(fields, row, cast) : parseObjectRow(fields, row, cast));
}
function decodeRow(row) {
const values = row.values ? atob(row.values) : '';
let offset = 0;
return row.lengths.map((size) => {
const width = parseInt(size, 10);
if (width < 0)
return null;
const splice = values.substring(offset, offset + width);
offset += width;
return splice;
});
}