UNPKG

@iarayan/ch-orm

Version:

A Developer-First ClickHouse ORM with Powerful CLI Tools

346 lines 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Model = void 0; const ConnectionPool_1 = require("../connection/ConnectionPool"); const ModelDecorators_1 = require("../decorators/ModelDecorators"); const QueryBuilder_1 = require("../query/QueryBuilder"); const helpers_1 = require("../utils/helpers"); /** * Base Model class with ORM functionality * Provides static and instance methods for interacting with ClickHouse tables */ class Model { /** * Set the database connection for all models * @param connection - ClickHouse connection or connection pool */ static setConnection(connection) { if (connection instanceof ConnectionPool_1.ConnectionPool) { // If it's a pool, create a provider that uses the pool this.connectionProvider = { getConnection: async () => { return connection.getConnection(); }, releaseConnection: (conn) => { connection.releaseConnection(conn); }, execute: async (callback) => { // Use the pool's withConnection method return connection.withConnection(callback); }, }; } else { // If it's a regular connection, create a simple provider this.connectionProvider = { getConnection: async () => { return connection; }, releaseConnection: () => { // Do nothing, single connections don't need to be released }, execute: async (callback) => { // Simply execute with the connection return callback(connection); }, }; } } /** * Get the database connection * @returns ClickHouse connection */ static getConnection() { if (!this.connectionProvider) { throw new Error("Database connection not set. Call Model.setConnection() first."); } // For backward compatibility with existing code return new Proxy({}, { get: (target, prop) => { return async (...args) => { return this.connectionProvider.execute(async (conn) => { return conn[prop](...args); }); }; }, }); } /** * Get the table name for this model * @returns Table name */ static getTableName() { const tableName = ModelDecorators_1.MetadataStorage.getTableName(this); if (!tableName) { throw new Error(`Table name not defined for model ${this.name}. Use @Table decorator.`); } return tableName; } /** * Get the column metadata for this model * @returns Map of property names to column metadata */ static getColumns() { return ModelDecorators_1.MetadataStorage.getColumns(this); } /** * Get the primary key columns for this model * @returns Array of primary key column names */ static getPrimaryKeys() { return ModelDecorators_1.MetadataStorage.getPrimaryKeys(this); } /** * Create a new query builder for this model * @returns Query builder instance */ static query() { if (!this.connectionProvider) { throw new Error(`No connection set for model ${this.name}`); } // For a query builder, we need to use the proxied connection const connection = this.getConnection(); return new QueryBuilder_1.QueryBuilder(connection, this.getTableName()); } /** * Get all records from the table * @param options - Query options * @returns Promise that resolves to array of model instances */ static async all(options) { const modelClass = this; return this.connectionProvider.execute(async (connection) => { const qb = new QueryBuilder_1.QueryBuilder(connection, modelClass.getTableName()); const records = await qb.get(options); return modelClass.hydrate(records); }); } /** * Find a record by its primary key * @param id - Primary key value * @param options - Query options * @returns Promise that resolves to model instance or null if not found */ static async find(id, options) { const modelClass = this; const primaryKeys = modelClass.getPrimaryKeys(); if (primaryKeys.length === 0) { throw new Error(`No primary key defined for model ${modelClass.name}`); } // Use the first primary key if there are multiple const primaryKey = primaryKeys[0]; return this.connectionProvider.execute(async (connection) => { const qb = new QueryBuilder_1.QueryBuilder(connection, modelClass.getTableName()); const record = await qb.where(primaryKey, "=", id).first(options); if (!record) { return null; } return modelClass.hydrate([record])[0]; }); } /** * Find a record by some conditions or throw an error if not found * @param id - Primary key value * @param options - Query options * @returns Promise that resolves to model instance * @throws Error if record not found */ static async findOrFail(id, options) { const modelClass = this; const model = await modelClass.find(id, options); if (!model) { throw new Error(`Record with id ${id} not found in table ${modelClass.getTableName()}`); } return model; } /** * Find first record matching the conditions * @param conditions - Conditions to match * @param options - Query options * @returns Promise that resolves to model instance or null if not found */ static async findBy(conditions, options) { const modelClass = this; const record = await modelClass.query().where(conditions).first(options); if (!record) { return null; } return modelClass.hydrate([record])[0]; } /** * Create a new model instance with the given attributes * @param attributes - Attributes to set on the model * @returns Model instance */ static create(attributes) { const modelClass = this; const model = new modelClass(); // Set attributes on the model Object.entries(attributes).forEach(([key, value]) => { model[key] = value; }); return model; } /** * Create a new model instance with the given attributes and save it to the database * @param attributes - Attributes to set on the model * @param options - Query options * @returns Promise that resolves to model instance */ static async createAndSave(attributes, options) { const modelClass = this; const model = modelClass.create(attributes); await model.save(options); return model; } /** * Convert raw database records to model instances * @param records - Raw database records * @returns Array of model instances */ static hydrate(records) { const modelClass = this; return records.map((record) => { const model = new modelClass(); // Set attributes on the model Object.entries(record).forEach(([key, value]) => { // Convert snake_case database keys to camelCase for model const propertyKey = (0, helpers_1.snakeToCamel)(key); model[propertyKey] = value; }); return model; }); } /** * Count records in the table * @param options - Query options * @returns Promise that resolves to record count */ static async count(options) { return this.query().count(options); } /** * Get the maximum value of a column * @param column - Column name * @param options - Query options * @returns Promise that resolves to maximum value */ static async max(column, options) { return this.query().max(column, options); } /** * Get the minimum value of a column * @param column - Column name * @param options - Query options * @returns Promise that resolves to minimum value */ static async min(column, options) { return this.query().min(column, options); } /** * Get the sum of values in a column * @param column - Column name * @param options - Query options * @returns Promise that resolves to sum of values */ static async sum(column, options) { return this.query().sum(column, options); } /** * Get the average of values in a column * @param column - Column name * @param options - Query options * @returns Promise that resolves to average of values */ static async avg(column, options) { return this.query().avg(column, options); } /** * Insert records into the table * @param data - Data to insert (single record or array of records) * @param options - Query options * @returns Promise that resolves to query result */ static async insert(data, options) { return this.query().insert(data, options); } /** * Convert the model instance to a database record * @returns Record with column names and values */ toRecord() { const constructor = this.constructor; const columns = constructor.getColumns(); const record = {}; // Add values for each column columns.forEach((metadata, propertyName) => { const value = this[propertyName]; // Use database column name record[metadata.name] = value; }); return record; } /** * Save the model to the database * @param options - Query options * @returns Promise that resolves to query result */ async save(options) { const constructor = this.constructor; const record = this.toRecord(); return constructor.connectionProvider.execute(async (connection) => { const qb = new QueryBuilder_1.QueryBuilder(connection, constructor.getTableName()); return qb.insert(record, options); }); } /** * Delete records by condition * @param conditions - Conditions to match for deletion * @param options - Query options * @returns Promise that resolves to query result */ static async deleteWhere(conditions, options) { return this.query().where(conditions).delete(options); } /** * Delete a record by its primary key * @param id - Primary key value * @param options - Query options * @returns Promise that resolves to query result */ static async deleteById(id, options) { const primaryKeys = this.getPrimaryKeys(); if (primaryKeys.length === 0) { throw new Error(`No primary key defined for model ${this.name}`); } // Use the first primary key if there are multiple const primaryKey = primaryKeys[0]; return this.query().where(primaryKey, "=", id).delete(options); } /** * Delete the current model instance from the database * @param options - Query options * @returns Promise that resolves to query result */ async delete(options) { const modelClass = this.constructor; const primaryKeys = modelClass.getPrimaryKeys(); if (primaryKeys.length === 0) { throw new Error(`Cannot delete model instance: No primary key defined for model ${modelClass.name}`); } return modelClass.connectionProvider.execute(async (connection) => { // Build query using all available primary keys const qb = new QueryBuilder_1.QueryBuilder(connection, modelClass.getTableName()); for (const key of primaryKeys) { const value = this[key]; if (value === undefined) { throw new Error(`Cannot delete model instance: Primary key '${key}' is undefined`); } qb.where(key, "=", value); } return qb.delete(options); }); } } exports.Model = Model; //# sourceMappingURL=Model.js.map