d1mapper
Version:
A simple ORM-like wrapper for Cloudflare D1 databases in TypeScript
197 lines (196 loc) • 7.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Database = void 0;
/**
* A wrapper around Cloudflare D1 providing basic CRUD operations.
* @template T - Record type representing table schema.
*/
class Database {
/**
* Construct a Database instance.
* @param options - Options for the database instance.
*/
constructor(options) {
this.db = options.db;
this.tableName = options.tableName;
this.defaultProperties = options.defaultProperties ?? {};
this.primaryKeyName = options.primaryKeyName;
}
/**
* Execute a SQL query with positional parameters.
* @param query - SQL query string.
* @param params - Array of parameters to bind.
* @returns DatabaseResult containing success status and change count.
*/
async exec(query, params) {
const result = await this.db.prepare(query).bind(...params).run();
if (!result.success) {
throw new Error(`Query failed: ${query}`);
}
return { success: true, changes: result.meta?.changes };
}
/**
* Insert a record into the table. Missing props use defaults.
* @param record - Partial record to insert.
* @param inPlaceModify - Whether to modify the record in place (default true).
* @returns Result of the insert operation.
*/
async insert(record, inPlaceModify = true) {
// merge any defaults with provided record, omit undefined keys
if (inPlaceModify) {
Object.assign(record, this.defaultProperties, record);
}
else {
record = { ...this.defaultProperties, ...record };
}
const keys = Object.keys(record);
const values = keys.map(k => record[k]);
const placeholders = keys.map((_, i) => `?${i + 1}`).join(', ');
const query = `INSERT INTO ${this.tableName} (${keys.join(', ')}) VALUES (${placeholders})`;
return this.exec(query, values);
}
async findOne(props, conditionKey, conditionValue) {
let columns;
if (Array.isArray(props)) {
columns = props.length > 0 ? props.join(', ') : '*';
}
else {
columns = props;
}
const query = `SELECT ${columns} FROM ${this.tableName} WHERE ${String(conditionKey)} = ?1 LIMIT 1`;
return await this.db.prepare(query).bind(conditionValue).first();
}
async findAll(props, paginationOptions) {
let columns;
if (Array.isArray(props)) {
columns = props.length > 0 ? props.join(', ') : '*';
}
else {
columns = props;
}
let query = `SELECT ${columns} FROM ${this.tableName}`;
const params = [];
if (paginationOptions?.limit !== undefined) {
query += ` LIMIT ?1`;
params.push(paginationOptions.limit);
}
if (paginationOptions?.offset !== undefined) {
query += ` OFFSET ?2`;
params.push(paginationOptions.offset);
}
const { results } = await this.db.prepare(query).bind(...params).all();
return results;
}
async findMany(props, filter, paginationOptions) {
let columns;
if (Array.isArray(props)) {
columns = props.length > 0 ? props.join(', ') : '*';
}
else {
columns = props;
}
const keys = Object.keys(filter);
const clauses = keys.map((k, i) => `${String(k)} = ?${i + 1}`).join(' AND ');
let query = `SELECT ${columns} FROM ${this.tableName}` + (clauses ? ` WHERE ${clauses}` : '');
let params = [];
let paramIndex = keys.length + 1;
if (paginationOptions?.limit !== undefined) {
query += ` LIMIT ?${paramIndex}`;
params.push(paginationOptions.limit);
paramIndex++;
}
if (paginationOptions?.offset !== undefined) {
query += ` OFFSET ?${paramIndex}`;
params.push(paginationOptions.offset);
paramIndex++;
}
const values = keys.map(k => filter[k]);
const { results } = await this.db.prepare(query).bind(...values, ...params).all();
return results;
}
/**
* Update records matching a condition.
* @param record - Partial properties to update.
* @param conditionKey - Column to filter by.
* @param conditionValue - Value to match.
* @returns Result containing change count.
*/
async update(record, conditionKey, conditionValue) {
const keys = Object.keys(record);
if (keys.length === 0) {
return { success: true, changes: 0 };
}
const setters = keys.map((key, i) => `${String(key)} = ?${i + 2}`).join(', ');
const query = `UPDATE ${this.tableName} SET ${setters} WHERE ${String(conditionKey)} = ?1`;
const values = [conditionValue, ...keys.map(k => record[k])];
return this.exec(query, values);
}
/**
* Delete records matching a condition.
* @param conditionKey - Column to filter by.
* @param conditionValue - Value to match.
* @returns Result containing change count.
*/
async delete(conditionKey, conditionValue) {
const query = `DELETE FROM ${this.tableName} WHERE ${String(conditionKey)} = ?1`;
return this.exec(query, [conditionValue]);
}
/**
* Increment a numeric column by a step for matching records.
* @param column - Column to increment.
* @param step - Amount to add.
* @param conditionKey - Column to filter by.
* @param conditionValue - Value to match.
* @returns Result containing change count.
*/
async increment(column, step, conditionKey, conditionValue) {
const query = `UPDATE ${this.tableName} SET ${String(column)} = ${String(column)} + ?1 WHERE ${String(conditionKey)} = ?2`;
return this.exec(query, [step, conditionValue]);
}
/**
* Check existence of a record matching a condition.
* @param conditionKey - Column to filter by.
* @param conditionValue - Value to match.
* @returns True if record exists, else false.
*/
async exists(conditionKey, conditionValue) {
const query = `SELECT 1 FROM ${this.tableName} WHERE ${String(conditionKey)} = ?1 LIMIT 1`;
const result = await this.db.prepare(query).bind(conditionValue).first();
return Boolean(result);
}
async findById(props, id) {
return this.findOne(props, this.primaryKeyName, id);
}
/**
* Update a record by primary key.
* @param record - Partial properties to update.
* @param id - Primary key value.
*/
async updateById(record, id) {
return this.update(record, this.primaryKeyName, id);
}
/**
* Delete a record by primary key.
* @param id - Primary key value.
*/
async deleteById(id) {
return this.delete(this.primaryKeyName, id);
}
/**
* Increment a numeric column by primary key.
* @param column - Column to increment.
* @param step - Amount to add.
* @param id - Primary key value.
*/
async incrementById(column, step, id) {
return this.increment(column, step, this.primaryKeyName, id);
}
/**
* Check existence by primary key.
* @param id - Primary key value.
*/
async existsById(id) {
return this.exists(this.primaryKeyName, id);
}
}
exports.Database = Database;