UNPKG

chanfana

Version:

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!

140 lines (118 loc) 4.98 kB
import type { InputValidationException } from "../../exceptions"; import type { Filters, Logger, O, UpdateFilters } from "../types"; import { UpdateEndpoint } from "../update"; import { buildPrimaryKeyFilters, buildWhereClause, getD1Binding, handleDbError, validateColumnName, validateTableName, } from "./base"; /** * D1-specific UpdateEndpoint implementation. * Provides automatic UPDATE operations with SQL injection prevention. */ export class D1UpdateEndpoint<HandleArgs extends Array<object> = Array<object>> extends UpdateEndpoint<HandleArgs> { /** Name of the D1 database binding in the worker environment. Defaults to "DB" */ dbName = "DB"; /** Optional logger for debugging and error tracking */ logger?: Logger; /** Custom error messages for UNIQUE constraint violations. Keys are constraint names (e.g., "users.email") */ constraintsMessages: Record<string, InputValidationException> = {}; /** * Gets the D1 database binding from the worker environment. * @returns D1Database instance * @throws ApiException if binding is not defined or is not a D1 binding */ getDBBinding(): D1Database { return getD1Binding((args) => this.params.router.getBindings(args), this.args, this.dbName); } /** * Gets the list of valid column names from the model schema. * @returns Array of valid column names */ protected getValidColumns(): string[] { return Object.keys(this.meta.model.schema.shape); } /** * Builds safe filters that only apply to primary keys. * @param filters - Filters object containing all filter conditions * @returns SafeFilters with validated conditions and parameters */ protected getSafeFilters(filters: Filters) { return buildPrimaryKeyFilters(filters, this.meta.model.primaryKeys, this.getValidColumns()); } /** * Fetches the existing object before update. * @param filters - Filter conditions for finding the object * @returns The existing record or null if not found */ async getObject(filters: Filters): Promise<Record<string, unknown> | null> { const tableName = validateTableName(this.meta.model.tableName); const safeFilters = this.getSafeFilters(filters); const whereClause = buildWhereClause(safeFilters.conditions); const sql = `SELECT * FROM ${tableName} ${whereClause} LIMIT 1`; if (this.logger) { this.logger.debug?.(`[D1UpdateEndpoint] getObject SQL: ${sql}`); } const oldObj = await this.getDBBinding() .prepare(sql) .bind(...safeFilters.conditionsParams) .all(); if (!oldObj.results || oldObj.results.length === 0) { return null; } return oldObj.results[0] as O<typeof this._meta>; } /** * Updates a record in the database. * @param _oldObj - The existing record (for reference) * @param filters - Filter conditions and data to update * @returns The updated record * @throws ApiException on database errors */ async update(_oldObj: O<typeof this._meta>, filters: UpdateFilters): Promise<O<typeof this._meta>> { const tableName = validateTableName(this.meta.model.tableName); const validColumns = this.getValidColumns(); const safeFilters = this.getSafeFilters(filters); const whereClause = buildWhereClause(safeFilters.conditions); // Filter updatedData to only include columns defined in the schema. // SELECT * may return DB columns not present in the Zod schema (e.g., internal columns), // and attempting to SET those would fail validateColumnName(). const validUpdateData = Object.fromEntries( Object.entries(filters.updatedData).filter(([key]) => validColumns.includes(key)), ); // Validate and build SET clause const updateColumns = Object.keys(validUpdateData); const updateValues = Object.values(validUpdateData); // If no fields to update, return the existing object unchanged if (updateColumns.length === 0) { return _oldObj as O<typeof this._meta>; } // Build SET clause with proper parameter indices (starting after condition params) const setClause = updateColumns .map((col, i) => { const validatedCol = validateColumnName(col, validColumns); return `${validatedCol} = ?${safeFilters.conditionsParams.length + i + 1}`; }) .join(", "); const sql = `UPDATE ${tableName} SET ${setClause} ${whereClause} RETURNING *`; if (this.logger) { this.logger.debug?.(`[D1UpdateEndpoint] update SQL: ${sql}`); } try { const obj = await this.getDBBinding() .prepare(sql) .bind(...safeFilters.conditionsParams, ...updateValues) .all(); const result = obj.results[0] as O<typeof this._meta>; if (this.logger) { this.logger.log(`Successfully updated record in ${tableName}`); } return result; } catch (e: unknown) { handleDbError(e as Error, this.constraintsMessages, this.logger, `update ${tableName}`); } } }