@worker-tools/deno-kv-storage
Version:
An implementation of the StorageArea (1,2,3) interface for Deno with an extensible system for supporting various database backends.
332 lines • 12.7 kB
JavaScript
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _QueryResult_row_description;
import { encodeArgument } from "./encode.js";
import { decode } from "./decode.js";
const commandTagRegexp = /^([A-Za-z]+)(?: (\d+))?(?: (\d+))?/;
export var ResultType;
(function (ResultType) {
ResultType[ResultType["ARRAY"] = 0] = "ARRAY";
ResultType[ResultType["OBJECT"] = 1] = "OBJECT";
})(ResultType || (ResultType = {}));
export class RowDescription {
constructor(columnCount, columns) {
Object.defineProperty(this, "columnCount", {
enumerable: true,
configurable: true,
writable: true,
value: columnCount
});
Object.defineProperty(this, "columns", {
enumerable: true,
configurable: true,
writable: true,
value: columns
});
}
}
/**
* This function transforms template string arguments into a query
*
* ```ts
* ["SELECT NAME FROM TABLE WHERE ID = ", " AND DATE < "]
* // "SELECT NAME FROM TABLE WHERE ID = $1 AND DATE < $2"
* ```
*/
export function templateStringToQuery(template, args, result_type) {
const text = template.reduce((curr, next, index) => {
return `${curr}$${index}${next}`;
});
return new Query(text, result_type, args);
}
function objectQueryToQueryArgs(query, args) {
args = normalizeObjectQueryArgs(args);
let counter = 0;
const clean_args = [];
const clean_query = query.replaceAll(/(?<=\$)\w+/g, (match) => {
match = match.toLowerCase();
if (match in args) {
clean_args.push(args[match]);
}
else {
throw new Error(`No value was provided for the query argument "${match}"`);
}
return String(++counter);
});
return [clean_query, clean_args];
}
/** This function lowercases all the keys of the object passed to it and checks for collission names */
function normalizeObjectQueryArgs(args) {
const normalized_args = Object.fromEntries(Object.entries(args).map(([key, value]) => [key.toLowerCase(), value]));
if (Object.keys(normalized_args).length !== Object.keys(args).length) {
throw new Error("The arguments provided for the query must be unique (insensitive)");
}
return normalized_args;
}
export class QueryResult {
constructor(query) {
Object.defineProperty(this, "query", {
enumerable: true,
configurable: true,
writable: true,
value: query
});
Object.defineProperty(this, "command", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "rowCount", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* This variable will be set after the class initialization, however it's required to be set
* in order to handle result rows coming in
*/
_QueryResult_row_description.set(this, void 0);
Object.defineProperty(this, "warnings", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
}
get rowDescription() {
return __classPrivateFieldGet(this, _QueryResult_row_description, "f");
}
set rowDescription(row_description) {
// Prevent #row_description from being changed once set
if (row_description && !__classPrivateFieldGet(this, _QueryResult_row_description, "f")) {
__classPrivateFieldSet(this, _QueryResult_row_description, row_description, "f");
}
}
/**
* This function is required to parse each column
* of the results
*/
loadColumnDescriptions(description) {
this.rowDescription = description;
}
handleCommandComplete(commandTag) {
const match = commandTagRegexp.exec(commandTag);
if (match) {
this.command = match[1];
if (match[3]) {
// COMMAND OID ROWS
this.rowCount = parseInt(match[3], 10);
}
else {
// COMMAND ROWS
this.rowCount = parseInt(match[2], 10);
}
}
}
/**
* Add a row to the result based on metadata provided by `rowDescription`
* This implementation depends on row description not being modified after initialization
*
* This function can throw on validation, so any errors must be handled in the message loop accordingly
*/
insertRow(_row) {
throw new Error("No implementation for insertRow is defined");
}
}
_QueryResult_row_description = new WeakMap();
export class QueryArrayResult extends QueryResult {
constructor() {
super(...arguments);
Object.defineProperty(this, "rows", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
}
insertRow(row_data) {
if (!this.rowDescription) {
throw new Error("The row descriptions required to parse the result data weren't initialized");
}
// Row description won't be modified after initialization
const row = row_data.map((raw_value, index) => {
const column = this.rowDescription.columns[index];
if (raw_value === null) {
return null;
}
return decode(raw_value, column);
});
this.rows.push(row);
}
}
function findDuplicatesInArray(array) {
return array.reduce((duplicates, item, index) => {
const is_duplicate = array.indexOf(item) !== index;
if (is_duplicate && !duplicates.includes(item)) {
duplicates.push(item);
}
return duplicates;
}, []);
}
function snakecaseToCamelcase(input) {
return input
.split("_")
.reduce((res, word, i) => {
if (i !== 0) {
word = word[0].toUpperCase() + word.slice(1);
}
res += word;
return res;
}, "");
}
export class QueryObjectResult extends QueryResult {
constructor() {
super(...arguments);
/**
* The column names will be undefined on the first run of insertRow, since
*/
Object.defineProperty(this, "columns", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "rows", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
}
insertRow(row_data) {
if (!this.rowDescription) {
throw new Error("The row description required to parse the result data wasn't initialized");
}
// This will only run on the first iteration after row descriptions have been set
if (!this.columns) {
if (this.query.fields) {
if (this.rowDescription.columns.length !== this.query.fields.length) {
throw new RangeError("The fields provided for the query don't match the ones returned as a result " +
`(${this.rowDescription.columns.length} expected, ${this.query.fields.length} received)`);
}
this.columns = this.query.fields;
}
else {
let column_names;
if (this.query.camelcase) {
column_names = this.rowDescription.columns.map((column) => snakecaseToCamelcase(column.name));
}
else {
column_names = this.rowDescription.columns.map((column) => column.name);
}
// Check field names returned by the database are not duplicated
const duplicates = findDuplicatesInArray(column_names);
if (duplicates.length) {
throw new Error(`Field names ${duplicates.map((str) => `"${str}"`).join(", ")} are duplicated in the result of the query`);
}
this.columns = column_names;
}
}
// It's safe to assert columns as defined from now on
const columns = this.columns;
if (columns.length !== row_data.length) {
throw new RangeError("The result fields returned by the database don't match the defined structure of the result");
}
const row = row_data.reduce((row, raw_value, index) => {
const current_column = this.rowDescription.columns[index];
if (raw_value === null) {
row[columns[index]] = null;
}
else {
row[columns[index]] = decode(raw_value, current_column);
}
return row;
}, {});
this.rows.push(row);
}
}
export class Query {
constructor(config_or_text, result_type, args = []) {
Object.defineProperty(this, "args", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "camelcase", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* The explicitly set fields for the query result, they have been validated beforehand
* for duplicates and invalid names
*/
Object.defineProperty(this, "fields", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// TODO
// Should be private
Object.defineProperty(this, "result_type", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// TODO
// Document that this text is the one sent to the database, not the original one
Object.defineProperty(this, "text", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.result_type = result_type;
if (typeof config_or_text === "string") {
if (!Array.isArray(args)) {
[config_or_text, args] = objectQueryToQueryArgs(config_or_text, args);
}
this.text = config_or_text;
this.args = args.map(encodeArgument);
}
else {
let { args = [], camelcase, encoder = encodeArgument, fields,
// deno-lint-ignore no-unused-vars
name, text, } = config_or_text;
// Check that the fields passed are valid and can be used to map
// the result of the query
if (fields) {
const fields_are_clean = fields.every((field) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field));
if (!fields_are_clean) {
throw new TypeError("The fields provided for the query must contain only letters and underscores");
}
if (new Set(fields).size !== fields.length) {
throw new TypeError("The fields provided for the query must be unique");
}
this.fields = fields;
}
this.camelcase = camelcase;
if (!Array.isArray(args)) {
[text, args] = objectQueryToQueryArgs(text, args);
}
this.args = args.map(encoder);
this.text = text;
}
}
}
//# sourceMappingURL=query.js.map