@iarayan/ch-orm
Version:
A Developer-First ClickHouse ORM with Powerful CLI Tools
346 lines • 12.6 kB
JavaScript
"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 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