UNPKG

d1mapper

Version:

A simple ORM-like wrapper for Cloudflare D1 databases in TypeScript

197 lines (196 loc) 7.5 kB
"use strict"; 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;