UNPKG

dynamic-orm

Version:

A flexible and powerful dynamic ORM for SQL databases with Redis caching support and many-to-many relationship handling

1,086 lines • 50.8 kB
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamicModel = void 0;
const uuid_1 = require("uuid");
// Default logger that uses console
const defaultLogger = {
    error: (message, ...args) => console.error(message, ...args),
    warn: (message, ...args) => console.warn(message, ...args),
    info: (message, ...args) => console.info(message, ...args),
    debug: (message, ...args) => console.debug(message, ...args)
};
/**
 * Enhanced Dynamic Model for database operations
 * Provides a flexible and powerful abstraction over database tables
 */
class DynamicModel {
    /**
     * Create a new Dynamic Model instance
     * @param table - Database table name
     * @param options - Configuration options
     * @param db - Database adapter
     * @param cache - Cache adapter
     */
    constructor(table, options = {}, db, cache) {
        this.table = table;
        this.useCache = options.useCache || false;
        this.cacheTTL = options.cacheTTL || 3600; // Default: 1 hour
        this.primaryKey = options.primaryKey || 'id';
        this.defaultLimit = options.defaultLimit || 100;
        this.maxLimit = options.maxLimit || 1000;
        this.searchableFields = options.searchableFields || [];
        this.db = db;
        this.cache = cache;
        this.logger = options.logger || defaultLogger;
    }
    /**
     * Find records with filtering, pagination, sorting and field selection
     */
    async findAll(options = {}) {
        // For backward compatibility with getAll
        if (typeof this.getAll === 'undefined') {
            this.getAll = this.findAll;
        }
        // Handle both new options object format and old filters direct parameter
        const isLegacyCall = !options || typeof options !== 'object' || !Object.keys(options).some(k => ['filters', 'sort', 'fields', 'pagination', 'search', 'relations'].includes(k));
        const filters = isLegacyCall ? options : (options.filters || {});
        const sort = isLegacyCall ? null : options.sort;
        const fields = isLegacyCall ? null : options.fields;
        const pagination = isLegacyCall ? null : options.pagination;
        const search = isLegacyCall ? null : options.search;
        const relations = isLegacyCall ? null : options.relations;
        // Build cache key
        const cacheKey = this._buildCacheKey('findAll', { filters, sort, fields, pagination, search, relations });
        // Try to get from cache
        let cached = null;
        if (this.useCache) {
            try {
                cached = await this.cache.get(cacheKey);
                if (cached) {
                    return JSON.parse(cached);
                }
            }
            catch (cacheError) {
                // Log cache error but continue without cache
                this.logger.error(`[${this.table}] Cache error in findAll:`, cacheError);
                // Continue execution without using cache
            }
        }
        try {
            // Prepare query components
            let params = [];
            let conditions = [];
            let whereClause = '';
            let joins = [];
            let groupBy = '';
            let tableAliases = {};
            let mainTableAlias = 't1';
            // Add table alias for main table
            tableAliases[this.table] = mainTableAlias;
            // Process relations if provided
            if (relations && Array.isArray(relations) && relations.length > 0) {
                let joinIndex = 2;
                for (const relation of relations) {
                    if (!relation.table)
                        continue;
                    const as = relation.as || relation.table;
                    // Handle many-to-many relationship (using a junction table)
                    if (relation.through && relation.throughLocalKey && relation.throughForeignKey) {
                        // For many-to-many relationships, we need two joins: one to the junction table and one to the related table
                        const junctionAlias = `t${joinIndex++}`;
                        const relAlias = `t${joinIndex}`;
                        // Join to the junction table first
                        joins.push(`LEFT JOIN ${relation.through} ${junctionAlias} ON ${mainTableAlias}.${this.primaryKey} = ${junctionAlias}.${relation.throughLocalKey}`);
                        // Then join from junction table to the related table
                        joins.push(`LEFT JOIN ${relation.table} ${relAlias} ON ${junctionAlias}.${relation.throughForeignKey} = ${relAlias}.${relation.foreignKey}`);
                        // Save table alias for later use
                        tableAliases[relation.table] = relAlias;
                        tableAliases[relation.through] = junctionAlias;
                        // Add relation filters if provided
                        if (relation.filters && typeof relation.filters === 'object') {
                            for (const [key, value] of Object.entries(relation.filters)) {
                                // Apply filters to the related table, not the junction table
                                // Handle different types of filter values for relation
                                if (value === null) {
                                    conditions.push(`${relAlias}.${key} IS NULL`);
                                }
                                else if (Array.isArray(value)) {
                                    if (value.length === 0) {
                                        conditions.push('FALSE');
                                    }
                                    else {
                                        const placeholders = value.map(() => '?').join(', ');
                                        conditions.push(`${relAlias}.${key} IN (${placeholders})`);
                                        params.push(...value);
                                    }
                                }
                                else if (typeof value === 'object') {
                                    for (const [op, val] of Object.entries(value)) {
                                        let sqlOp;
                                        switch (op) {
                                            case 'gt':
                                                sqlOp = '>';
                                                break;
                                            case 'lt':
                                                sqlOp = '<';
                                                break;
                                            case 'gte':
                                                sqlOp = '>=';
                                                break;
                                            case 'lte':
                                                sqlOp = '<=';
                                                break;
                                            case 'ne':
                                                sqlOp = '!=';
                                                break;
                                            case 'like':
                                                sqlOp = 'LIKE';
                                                break;
                                            case 'ilike':
                                                sqlOp = 'ILIKE';
                                                break;
                                            default: sqlOp = '=';
                                        }
                                        if (val === null && op === 'ne') {
                                            conditions.push(`${relAlias}.${key} IS NOT NULL`);
                                        }
                                        else if (val === null) {
                                            conditions.push(`${relAlias}.${key} IS NULL`);
                                        }
                                        else {
                                            conditions.push(`${relAlias}.${key} ${sqlOp} ?`);
                                            params.push(val);
                                        }
                                    }
                                }
                                else {
                                    conditions.push(`${relAlias}.${key} = ?`);
                                    params.push(value);
                                }
                            }
                        }
                    }
                    else {
                        // Handle regular one-to-many or one-to-one relationship
                        if (!relation.foreignKey)
                            continue;
                        const joinType = (relation.type || 'left').toUpperCase();
                        const relAlias = `t${joinIndex}`;
                        const localKey = relation.localKey || this.primaryKey;
                        // Save table alias for later use
                        tableAliases[relation.table] = relAlias;
                        // Build join clause - swap the order of fields in the ON clause
                        joins.push(`${joinType} JOIN ${relation.table} ${relAlias} ON ${mainTableAlias}.${localKey} = ${relAlias}.${relation.foreignKey}`);
                        // Add relation filters if provided
                        if (relation.filters && typeof relation.filters === 'object') {
                            for (const [key, value] of Object.entries(relation.filters)) {
                                // Handle different types of filter values for relation
                                if (value === null) {
                                    conditions.push(`${relAlias}.${key} IS NULL`);
                                }
                                else if (Array.isArray(value)) {
                                    if (value.length === 0) {
                                        conditions.push('FALSE');
                                    }
                                    else {
                                        const placeholders = value.map(() => '?').join(', ');
                                        conditions.push(`${relAlias}.${key} IN (${placeholders})`);
                                        params.push(...value);
                                    }
                                }
                                else if (typeof value === 'object') {
                                    for (const [op, val] of Object.entries(value)) {
                                        let sqlOp;
                                        switch (op) {
                                            case 'gt':
                                                sqlOp = '>';
                                                break;
                                            case 'lt':
                                                sqlOp = '<';
                                                break;
                                            case 'gte':
                                                sqlOp = '>=';
                                                break;
                                            case 'lte':
                                                sqlOp = '<=';
                                                break;
                                            case 'ne':
                                                sqlOp = '!=';
                                                break;
                                            case 'like':
                                                sqlOp = 'LIKE';
                                                break;
                                            case 'ilike':
                                                sqlOp = 'ILIKE';
                                                break;
                                            default: sqlOp = '=';
                                        }
                                        if (val === null && op === 'ne') {
                                            conditions.push(`${relAlias}.${key} IS NOT NULL`);
                                        }
                                        else if (val === null) {
                                            conditions.push(`${relAlias}.${key} IS NULL`);
                                        }
                                        else {
                                            conditions.push(`${relAlias}.${key} ${sqlOp} ?`);
                                            params.push(val);
                                        }
                                    }
                                }
                                else {
                                    conditions.push(`${relAlias}.${key} = ?`);
                                    params.push(value);
                                }
                            }
                        }
                    }
                    joinIndex++;
                }
                // Need GROUP BY when using joins to avoid duplicates
                if (joins.length > 0) {
                    groupBy = `GROUP BY ${mainTableAlias}.${this.primaryKey}`;
                }
            }
            // Handle search if provided
            if (search && this.searchableFields.length > 0) {
                const searchConditions = this.searchableFields.map(field => {
                    const dotIndex = field.indexOf('.');
                    // Handle fields with table names
                    if (dotIndex > -1) {
                        const tableName = field.substring(0, dotIndex);
                        const columnName = field.substring(dotIndex + 1);
                        const tableAlias = tableAliases[tableName] || mainTableAlias;
                        params.push(`%${search}%`);
                        return `${tableAlias}.${columnName} LIKE ?`;
                    }
                    else {
                        params.push(`%${search}%`);
                        return `${mainTableAlias}.${field} LIKE ?`;
                    }
                });
                if (searchConditions.length > 0) {
                    conditions.push(`(${searchConditions.join(' OR ')})`);
                }
            }
            // Process main table filters
            if (filters && Object.keys(filters).length > 0) {
                for (const [key, value] of Object.entries(filters)) {
                    // Check if filter key contains table reference (tableName.columnName)
                    const dotIndex = key.indexOf('.');
                    let tableAlias = mainTableAlias;
                    let columnName = key;
                    if (dotIndex > -1) {
                        const tableName = key.substring(0, dotIndex);
                        columnName = key.substring(dotIndex + 1);
                        tableAlias = tableAliases[tableName] || mainTableAlias;
                    }
                    // Handle different types of filter values
                    if (value === null) {
                        conditions.push(`${tableAlias}.${columnName} IS NULL`);
                    }
                    else if (Array.isArray(value)) {
                        if (value.length === 0) {
                            conditions.push('FALSE');
                        }
                        else {
                            const placeholders = value.map(() => '?').join(', ');
                            conditions.push(`${tableAlias}.${columnName} IN (${placeholders})`);
                            params.push(...value);
                        }
                    }
                    else if (typeof value === 'object') {
                        for (const [op, val] of Object.entries(value)) {
                            let sqlOp;
                            switch (op) {
                                case 'gt':
                                    sqlOp = '>';
                                    break;
                                case 'lt':
                                    sqlOp = '<';
                                    break;
                                case 'gte':
                                    sqlOp = '>=';
                                    break;
                                case 'lte':
                                    sqlOp = '<=';
                                    break;
                                case 'ne':
                                    sqlOp = '!=';
                                    break;
                                case 'like':
                                    sqlOp = 'LIKE';
                                    break;
                                case 'ilike':
                                    sqlOp = 'ILIKE';
                                    break;
                                default: sqlOp = '=';
                            }
                            if (val === null && op === 'ne') {
                                conditions.push(`${tableAlias}.${columnName} IS NOT NULL`);
                            }
                            else if (val === null) {
                                conditions.push(`${tableAlias}.${columnName} IS NULL`);
                            }
                            else {
                                conditions.push(`${tableAlias}.${columnName} ${sqlOp} ?`);
                                params.push(val);
                            }
                        }
                    }
                    else {
                        conditions.push(`${tableAlias}.${columnName} = ?`);
                        params.push(value);
                    }
                }
            }
            if (conditions.length > 0) {
                whereClause = `WHERE ${conditions.join(' AND ')}`;
            }
            // Build SELECT clause
            let selectFields = '';
            if (relations && Array.isArray(relations) && relations.length > 0) {
                // Handle selections from multiple tables
                const selectParts = [];
                // Main table fields
                if (fields) {
                    if (typeof fields === 'string') {
                        selectParts.push(`${mainTableAlias}.${fields}`);
                    }
                    else if (Array.isArray(fields)) {
                        selectParts.push(fields.map(f => `${mainTableAlias}.${f}`).join(', '));
                    }
                }
                else {
                    selectParts.push(`${mainTableAlias}.*`);
                }
                // Related table fields with JSON aggregation
                for (const relation of relations) {
                    if (!relation.table)
                        continue;
                    const relAlias = tableAliases[relation.table];
                    const as = relation.as || relation.table;
                    // Include specific relation fields or all
                    const relationFields = relation.select || '*';
                    let relFieldsStr = '';
                    if (relationFields === '*') {
                        relFieldsStr = `${relAlias}.*`;
                    }
                    else if (Array.isArray(relationFields)) {
                        relFieldsStr = relationFields.map(f => `${relAlias}.${f} AS "${as}.${f}"`).join(', ');
                    }
                    else if (typeof relationFields === 'string') {
                        relFieldsStr = `${relAlias}.${relationFields} AS "${as}.${relationFields}"`;
                    }
                    if (relFieldsStr) {
                        selectParts.push(relFieldsStr);
                    }
                }
                selectFields = selectParts.join(', ');
            }
            else {
                // Simple table select
                selectFields = this._buildSelectClause(fields);
                // Add table alias if we have it
                if (mainTableAlias) {
                    // If selectFields contains specific field names
                    if (selectFields !== '*') {
                        // Add table alias to each field
                        selectFields = selectFields.split(', ')
                            .map(field => `${mainTableAlias}.${field.trim()}`)
                            .join(', ');
                    }
                    else {
                        selectFields = `${mainTableAlias}.*`;
                    }
                }
            }
            // Build ORDER BY clause with table alias
            let orderByClause = '';
            if (sort) {
                let sortFields = [];
                if (typeof sort === 'string') {
                    const direction = sort.startsWith('-') ? 'DESC' : 'ASC';
                    const field = sort.startsWith('-') ? sort.substring(1) : sort;
                    const dotIndex = field.indexOf('.');
                    let tableAlias = mainTableAlias;
                    let columnName = field;
                    if (dotIndex > -1) {
                        const tableName = field.substring(0, dotIndex);
                        columnName = field.substring(dotIndex + 1);
                        tableAlias = tableAliases[tableName] || mainTableAlias;
                    }
                    sortFields.push(`${tableAlias}.${columnName} ${direction}`);
                }
                else if (Array.isArray(sort)) {
                    sortFields = sort.map(field => {
                        const direction = field.startsWith('-') ? 'DESC' : 'ASC';
                        const fieldName = field.startsWith('-') ? field.substring(1) : field;
                        const dotIndex = fieldName.indexOf('.');
                        let tableAlias = mainTableAlias;
                        let columnName = fieldName;
                        if (dotIndex > -1) {
                            const tableName = fieldName.substring(0, dotIndex);
                            columnName = fieldName.substring(dotIndex + 1);
                            tableAlias = tableAliases[tableName] || mainTableAlias;
                        }
                        return `${tableAlias}.${columnName} ${direction}`;
                    });
                }
                else if (typeof sort === 'object') {
                    sortFields = Object.entries(sort).map(([field, direction]) => {
                        const dotIndex = field.indexOf('.');
                        let tableAlias = mainTableAlias;
                        let columnName = field;
                        if (dotIndex > -1) {
                            const tableName = field.substring(0, dotIndex);
                            columnName = field.substring(dotIndex + 1);
                            tableAlias = tableAliases[tableName] || mainTableAlias;
                        }
                        return `${tableAlias}.${columnName} ${(direction || '').toString().toLowerCase() === 'desc' ? 'DESC' : 'ASC'}`;
                    });
                }
                if (sortFields.length > 0) {
                    orderByClause = `ORDER BY ${sortFields.join(', ')}`;
                }
            }
            // Build pagination clauses
            let limitClause = '';
            let offsetClause = '';
            let page = 1;
            let limit = 0;
            let offset = 0;
            if (pagination) {
                page = Math.max(1, parseInt(pagination.page) || 1);
                limit = pagination.limit ?
                    Math.min(this.maxLimit, Math.max(1, parseInt(pagination.limit))) :
                    this.defaultLimit;
                offset = (page - 1) * limit;
                limitClause = `LIMIT ${limit}`;
                offsetClause = `OFFSET ${offset}`;
            }
            // Build FROM clause with joins
            const fromClause = joins.length > 0
                ? `FROM ${this.table} ${mainTableAlias} ${joins.join(' ')}`
                : `FROM ${this.table} ${mainTableAlias}`;
            // Execute count query for pagination
            let total = 0;
            if (pagination || isLegacyCall) {
                // We need to use a subquery for accurate counts with JOINs
                let countQuery;
                if (joins.length > 0) {
                    countQuery = `
            SELECT COUNT(DISTINCT ${mainTableAlias}.${this.primaryKey}) as total 
            ${fromClause} 
            ${whereClause}
          `;
                }
                else {
                    countQuery = `
            SELECT COUNT(*) as total 
            ${fromClause} 
            ${whereClause}
          `;
                }
                const [countResult] = await this.db.prepare(countQuery, params);
                total = parseInt(countResult.total);
            }
            // Execute data query
            const dataQuery = `
        SELECT ${selectFields} 
        ${fromClause} 
        ${whereClause} 
        ${groupBy}
        ${orderByClause} 
        ${limitClause} 
        ${offsetClause}
      `;
            const rawData = await this.db.prepare(dataQuery, params);
            // Process results to nest relation data
            let data = rawData;
            if (relations && Array.isArray(relations) && relations.length > 0) {
                // Group related data into nested objects
                data = await this._processRelatedData(rawData, relations);
            }
            // Build result
            const result = {
                data,
                pagination: {
                    total,
                    page,
                    limit: limit || total,
                    pages: limit ? Math.ceil(total / limit) : 1,
                    hasNext: limit ? (offset + limit < total) : false
                }
            };
            // Cache the result
            if (this.useCache) {
                try {
                    await this.cache.set(cacheKey, JSON.stringify(result), 'EX', this.cacheTTL);
                }
                catch (cacheError) {
                    // Just log cache error but continue
                    this.logger.error(`[${this.table}] Cache error during set in findAll:`, cacheError);
                }
            }
            return result;
        }
        catch (error) {
            this.logger.error(`[${this.table}] findAll error:`, error);
            throw error;
        }
    }
    // Alias getAll to findAll for backward compatibility
    async getAll(options = {}) {
        return this.findAll(options);
    }
    /**
     * Process query results to organize related data
     * @private
     */
    async _processRelatedData(records, relations) {
        if (!records || !records.length)
            return [];
        const result = [];
        const primaryKeyField = this.primaryKey;
        const processedIds = new Set();
        for (const row of records) {
            const id = row[primaryKeyField];
            // Skip if we already processed this main record
            if (processedIds.has(id))
                continue;
            processedIds.add(id);
            // Create the main record (exclude relation fields)
            const mainRecord = { ...row };
            // Process each relation
            for (const relation of relations) {
                const as = relation.as || relation.table;
                // Handle many-to-many relationship
                if (relation.through) {
                    // Always treat many-to-many relationships as "many" type
                    // Special case for when only one field is selected
                    const isSingleField = relation.select && Array.isArray(relation.select) && relation.select.length === 1;
                    const singleFieldName = isSingleField ? relation.select[0] : null;
                    // We need to collect all rows for this record to get all related data
                    const relatedRows = records.filter(r => r[primaryKeyField] === id);
                    // Set to track processed related entities to avoid duplicates
                    const processedRelatedIds = new Set();
                    const hasAnyRelatedData = relatedRows.some(row => {
                        // Check if there's any related data
                        if (relation.select && Array.isArray(relation.select)) {
                            for (const field of relation.select) {
                                const relFieldKey = `${as}.${field}`;
                                if (relFieldKey in row && row[relFieldKey] !== null) {
                                    return true;
                                }
                            }
                        }
                        return false;
                    });
                    // Initialize the array only if we actually found related data
                    if (hasAnyRelatedData) {
                        // Initialize with empty array
                        mainRecord[as] = [];
                        for (const relatedRow of relatedRows) {
                            if (relation.select && Array.isArray(relation.select)) {
                                const relObject = {};
                                let hasRelatedData = false;
                                let relatedEntityId = null;
                                for (const field of relation.select) {
                                    const relFieldKey = `${as}.${field}`;
                                    // If the related field exists and has a value
                                    if (relFieldKey in relatedRow && relatedRow[relFieldKey] !== null) {
                                        relObject[field] = relatedRow[relFieldKey];
                                        // Mark that we found related data
                                        hasRelatedData = true;
                                        // If this is the FK field from the related table
                                        if (field === relation.foreignKey) {
                                            relatedEntityId = relatedRow[relFieldKey];
                                        }
                                        // Remove the dotted field from the main record
                                        delete mainRecord[relFieldKey];
                                    }
                                }
                                // Only add if we found related data and haven't processed this related entity yet
                                if (hasRelatedData && relatedEntityId && !processedRelatedIds.has(relatedEntityId)) {
                                    processedRelatedIds.add(relatedEntityId);
                                    if (isSingleField) {
                                        // For single field selection, add the value directly to the array
                                        mainRecord[as].push(relObject[singleFieldName]);
                                    }
                                    else {
                                        // For multiple fields, add the object to the array
                                        mainRecord[as].push(relObject);
                                    }
                                }
                            }
                        }
                    }
                    else {
                        // If no related data was found, don't include the field at all
                        // This ensures the property won't appear in the output when there's no data
                        // Do nothing - don't set the property at all
                    }
                }
                else {
                    // Handle one-to-one or one-to-many relationship
                    // Initialize relation container
                    mainRecord[as] = relation.type === 'many' ? [] : {};
                    // Extract relation fields from the row
                    if (relation.select && Array.isArray(relation.select)) {
                        const relObject = relation.type === 'many' ? [] : {};
                        let hasRelatedData = false;
                        for (const field of relation.select) {
                            const relFieldKey = `${as}.${field}`;
                            // If the relation field exists in the result
                            if (relFieldKey in row) {
                                // Add the field to the relation object
                                if (relation.type === 'many') {
                                    // Handle many relation case
                                    if (!Array.isArray(relObject) || !relObject[0]) {
                                        relObject[0] = {};
                                    }
                                    relObject[0][field] = row[relFieldKey];
                                }
                                else {
                                    // Handle single relation case
                                    relObject[field] = row[relFieldKey];
                                }
                                // Mark that we found related data
                                hasRelatedData = true;
                                // Remove the dotted field from the main record
                                delete mainRecord[relFieldKey];
                            }
                        }
                        // Only set if we actually found related data
                        if (hasRelatedData) {
                            if (relation.type === 'many') {
                                mainRecord[as] = relObject;
                            }
                            else {
                                // If relation has only a single field, extract the value directly
                                if (relation.select.length === 1) {
                                    const singleField = relation.select[0];
                                    mainRecord[as] = relObject[singleField];
                                }
                                else {
                                    mainRecord[as] = relObject;
                                }
                            }
                        }
                        else {
                            // If no relation data was found, set to null for single relations
                            // or empty array for many relations
                            mainRecord[as] = relation.type === 'many' ? [] : null;
                        }
                    }
                    else {
                        // If no relation fields were selected, set to null for single relations
                        // or empty array for many relations
                        mainRecord[as] = relation.type === 'many' ? [] : null;
                    }
                }
            }
            result.push(mainRecord);
        }
        return result;
    }
    /**
     * Find a record by its primary key
     */
    async findById(id, fields) {
        if (!id)
            return null;
        const cacheKey = this._buildCacheKey('findById', { id, fields });
        // Try to get from cache
        let cached = null;
        if (this.useCache) {
            try {
                cached = await this.cache.get(cacheKey);
                if (cached) {
                    return JSON.parse(cached);
                }
            }
            catch (cacheError) {
                // Log cache error but continue without cache
                this.logger.error(`[${this.table}] Cache error in findById:`, cacheError);
                // Continue execution without using cache
            }
        }
        try {
            const selectClause = this._buildSelectClause(fields);
            const query = `SELECT ${selectClause} FROM ${this.table} WHERE ${this.primaryKey} = ?`;
            const results = await this.db.prepare(query, [id]);
            const record = results.length > 0 ? results[0] : null;
            // Cache result
            if (this.useCache && record) {
                try {
                    await this.cache.set(cacheKey, JSON.stringify(record), 'EX', this.cacheTTL);
                }
                catch (cacheError) {
                    // Just log cache error but continue
                    this.logger.error(`[${this.table}] Cache error during set in findById:`, cacheError);
                }
            }
            return record;
        }
        catch (error) {
            this.logger.error(`[${this.table}] findById error:`, error);
            throw error;
        }
    }
    /**
     * Find a record by a specific field value
     */
    async findByField(field, value, fields) {
        let cached = null;
        const cacheKey = this._buildCacheKey('findByField', { field, value, fields });
        if (this.useCache) {
            try {
                cached = await this.cache.get(cacheKey);
                if (cached) {
                    return JSON.parse(cached);
                }
            }
            catch (error) {
                this.logger.error(`[${this.table}] Cache error when getting in findByField:`, error);
            }
        }
        try {
            const selectClause = this._buildSelectClause(fields);
            const query = `SELECT ${selectClause} FROM ${this.table} WHERE ${field} = ? LIMIT 1`;
            const results = await this.db.prepare(query, [value]);
            const record = results.length > 0 ? results[0] : null;
            // Cache result
            if (this.useCache && record) {
                try {
                    await this.cache.set(cacheKey, JSON.stringify(record), 'EX', this.cacheTTL);
                }
                catch (error) {
                    this.logger.error(`[${this.table}] Cache error when setting in findByField:`, error);
                }
            }
            return record;
        }
        catch (error) {
            this.logger.error(`[${this.table}] findByField error:`, error);
            throw error;
        }
    }
    /**
     * Create a new record
     */
    async create(data, returnRecord = true) {
        if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
            throw new Error('Data must be a non-empty object');
        }
        // Add UUID if no ID is provided and primaryKey is 'id'
        if (this.primaryKey === 'id' && !data.id) {
            data.id = (0, uuid_1.v4)();
        }
        try {
            const keys = Object.keys(data).join(", ");
            const values = Object.values(data);
            const placeholders = values.map(() => "?").join(", ");
            let query = `INSERT INTO ${this.table} (${keys}) VALUES (${placeholders})`;
            if (returnRecord) {
                query += ` RETURNING *`;
            }
            const result = await this.db.prepare(query, values);
            // Store the return value before trying to invalidate cache
            const returnValue = returnRecord ? result[0] : result;
            // Invalidate cache
            if (this.useCache) {
                await this.invalidateTableCache();
            }
            return returnValue;
        }
        catch (error) {
            this.logger.error(`[${this.table}] create error:`, error);
            throw error;
        }
    }
    /**
     * Update a record by ID
     */
    async update(id, data, returnRecord = true) {
        if (!id) {
            throw new Error('ID is required');
        }
        if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
            throw new Error('Update data must be a non-empty object');
        }
        try {
            const updates = Object.keys(data).map((key) => `${key} = ?`).join(", ");
            let query = `UPDATE ${this.table} SET ${updates} WHERE ${this.primaryKey} = ?`;
            if (returnRecord) {
                query += ` RETURNING *`;
            }
            const result = await this.db.prepare(query, [...Object.values(data), id]);
            // Invalidate cache
            if (this.useCache) {
                try {
                    await Promise.all([
                        this.cache.del(this._buildCacheKey('findById', { id })),
                        this.invalidateTableCache()
                    ]);
                }
                catch (cacheError) {
                    // Just log cache error but continue
                    this.logger.error(`[${this.table}] Cache error during invalidation in update:`, cacheError);
                }
            }
            return returnRecord ? (result.length > 0 ? result[0] : null) : result;
        }
        catch (error) {
            this.logger.error(`[${this.table}] update error:`, error);
            throw error;
        }
    }
    /**
     * Delete a record by ID
     */
    async delete(id, returnRecord = false) {
        if (!id) {
            throw new Error('ID is required');
        }
        try {
            let deletedRecord = null;
            // If we need to return the record, get it first
            if (returnRecord) {
                deletedRecord = await this.findById(id);
                if (!deletedRecord)
                    return null;
            }
            const query = `DELETE FROM ${this.table} WHERE ${this.primaryKey} = ?`;
            const result = await this.db.prepare(query, [id]);
            // Invalidate cache
            if (this.useCache) {
                try {
                    await Promise.all([
                        this.cache.del(this._buildCacheKey('findById', { id })),
                        this.invalidateTableCache()
                    ]);
                }
                catch (cacheError) {
                    // Just log cache error but continue
                    this.logger.error(`[${this.table}] Cache error during invalidation in delete:`, cacheError);
                }
            }
            return returnRecord ? deletedRecord : result;
        }
        catch (error) {
            this.logger.error(`[${this.table}] delete error:`, error);
            throw error;
        }
    }
    /**
     * Count records matching filters
     */
    async count(filters = {}) {
        const cacheKey = this._buildCacheKey('count', { filters });
        // Try to get from cache
        let cached = null;
        if (this.useCache) {
            try {
                cached = await this.cache.get(cacheKey);
                if (cached) {
                    return parseInt(cached);
                }
            }
            catch (cacheError) {
                // Log cache error but continue without cache
                this.logger.error(`[${this.table}] Cache error in count:`, cacheError);
                // Continue execution without using cache
            }
        }
        try {
            // Build WHERE clause
            let params = [];
            let conditions = [];
            for (const [key, value] of Object.entries(filters)) {
                if (value === null) {
                    conditions.push(`${key} IS NULL`);
                }
                else if (Array.isArray(value)) {
                    if (value.length === 0) {
                        conditions.push('FALSE');
                    }
                    else {
                        const placeholders = value.map(() => '?').join(', ');
                        conditions.push(`${key} IN (${placeholders})`);
                        params.push(...value);
                    }
                }
                else if (typeof value === 'object') {
                    for (const [op, val] of Object.entries(value)) {
                        let sqlOp;
                        switch (op) {
                            case 'gt':
                                sqlOp = '>';
                                break;
                            case 'lt':
                                sqlOp = '<';
                                break;
                            case 'gte':
                                sqlOp = '>=';
                                break;
                            case 'lte':
                                sqlOp = '<=';
                                break;
                            case 'ne':
                                sqlOp = '!=';
                                break;
                            case 'like':
                                sqlOp = 'LIKE';
                                break;
                            default: sqlOp = '=';
                        }
                        conditions.push(`${key} ${sqlOp} ?`);
                        params.push(val);
                    }
                }
                else {
                    conditions.push(`${key} = ?`);
                    params.push(value);
                }
            }
            const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
            const query = `SELECT COUNT(*) as count FROM ${this.table} ${whereClause}`;
            const [result] = await this.db.prepare(query, params);
            const count = parseInt(result.count);
            // Cache result
            if (this.useCache) {
                try {
                    await this.cache.set(cacheKey, count.toString(), 'EX', this.cacheTTL);
                }
                catch (cacheError) {
                    // Just log cache error but continue
                    this.logger.error(`[${this.table}] Cache error during set in count:`, cacheError);
                }
            }
            return count;
        }
        catch (error) {
            this.logger.error(`[${this.table}] count error:`, error);
            throw error;
        }
    }
    /**
     * Execute multiple operations in a transaction
     */
    async withTransaction(callback) {
        return this.db.transaction(async (conn) => {
            // Create a transaction-specific model
            const txModel = {
                table: this.table,
                primaryKey: this.primaryKey,
                // Transaction-specific methods
                executeQuery: async (sql, params = []) => {
                    return await conn.prepare(sql, params);
                },
                create: async (data, returnRecord = true) => {
                    const keys = Object.keys(data).join(', ');
                    const values = Object.values(data);
                    const placeholders = values.map(() => '?').join(', ');
                    let query = `INSERT INTO ${this.table} (${keys}) VALUES (${placeholders})`;
                    if (returnRecord) {
                        query += ` RETURNING *`;
                    }
                    const result = await conn.prepare(query, values);
                    return returnRecord ? result[0] : result;
                },
                update: async (id, data, returnRecord = true) => {
                    const updates = Object.keys(data).map(key => `${key} = ?`).join(', ');
                    let query = `UPDATE ${this.table} SET ${updates} WHERE ${this.primaryKey} = ?`;
                    if (returnRecord) {
                        query += ` RETURNING *`;
                    }
                    const result = await conn.prepare(query, [...Object.values(data), id]);
                    return returnRecord ? (result.length > 0 ? result[0] : null) : result;
                },
                delete: async (id, returnRecord = false) => {
                    if (returnRecord) {
                        const query = `SELECT * FROM ${this.table} WHERE ${this.primaryKey} = ?`;
                        const records = await conn.prepare(query, [id]);
                        const record = records.length > 0 ? records[0] : null;
                        if (!record)
                            return null;
                        await conn.prepare(`DELETE FROM ${this.table} WHERE ${this.primaryKey} = ?`, [id]);
                        return record;
                    }
                    else {
                        const query = `DELETE FROM ${this.table} WHERE ${this.primaryKey} = ?`;
                        return await conn.prepare(query, [id]);
                    }
                },
                findById: async (id, fields) => {
                    const selectFields = this._buildSelectClause(fields);
                    const query = `SELECT ${selectFields} FROM ${this.table} WHERE ${this.primaryKey} = ?`;
                    const records = await conn.prepare(query, [id]);
                    return records.length > 0 ? records[0] : null;
                }
            };
            // Execute the callback with our transaction model
            const result = await callback(txModel);
            // Invalidate cache after successful transaction
            if (this.useCache) {
                await this.invalidateTableCache();
            }
            return result;
        });
    }
    /**
     * Execute a custom query
     */
    async executeQuery(sql, params = []) {
        try {
            return await this.db.prepare(sql, params);
        }
        catch (error) {
            this.logger.error(`[${this.table}] executeQuery error:`, error);
            throw error;
        }
    }
    /**
     * Invalidate all cache for this table
     */
    async invalidateTableCache() {
        if (!this.useCache)
            return;
        try {
            const keys = await this.cache.keys(`${this.table}:*`);
            if (keys.length > 0) {
                await this.cache.del(keys);
            }
        }
        catch (error) {
            this.logger.error(`[${this.table}] invalidateTableCache error:`, error);
            // Don't throw as cache invalidation should not break functionality
        }
    }
    /**
     * Build a cache key for an operation
     * @private
     */
    _buildCacheKey(operation, params = {}) {
        return `${this.table}:${operation}:${JSON.stringify(params)}`;
    }
    /**
     * Build field selection clause
     * @private
     */
    _buildSelectClause(fields) {
        if (!fields || (Array.isArray(fields) && fields.length === 0)) {
            return '*';
        }
        if (typeof fields === 'string') {
            return fields;
        }
        if (Array.isArray(fields)) {
            return fields.join(', ');
        }
        return '*';
    }
}
exports.DynamicModel = DynamicModel;