UNPKG

artmapper

Version:

Spring Boot clone for Node.js with TypeScript/JavaScript - JPA-like ORM, Lombok decorators, dependency injection, and MySQL support

309 lines 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryBuilder = exports.EntityManager = void 0; require("reflect-metadata"); const entity_1 = require("../decorators/entity"); const RelationshipManager_1 = require("./RelationshipManager"); class EntityManager { constructor(pool) { this.pool = pool; this.relationshipManager = new RelationshipManager_1.RelationshipManager(pool); } /** * Persist an entity to the database * @param entity The entity to persist * @param fieldsToUpdate Optional array of field names to update (for partial updates) */ async persist(entity, fieldsToUpdate) { const entityClass = entity.constructor; const entityMetadata = Reflect.getMetadata(entity_1.ENTITY_METADATA_KEY, entityClass); const tableMetadata = Reflect.getMetadata(entity_1.TABLE_METADATA_KEY, entityClass); const columnMetadata = Reflect.getMetadata(entity_1.COLUMN_METADATA_KEY, entityClass) || {}; const idProperty = Reflect.getMetadata(entity_1.ID_METADATA_KEY, entityClass); const generatedValue = Reflect.getMetadata(entity_1.GENERATED_VALUE_METADATA_KEY, entityClass); if (!entityMetadata) { throw new Error(`Class ${entityClass.name} is not an entity`); } const tableName = tableMetadata?.name || entityClass.name.toLowerCase(); const columns = Object.keys(columnMetadata); // Check if entity has an ID (for update vs insert) const idValue = idProperty ? entity[idProperty] : null; const idColumn = idProperty ? columnMetadata[idProperty]?.name || idProperty : null; // If entity has an ID value, it's an UPDATE (regardless of AUTO_INCREMENT strategy) if (idValue !== null && idValue !== undefined) { // Update existing entity // If fieldsToUpdate is specified, only update those fields // Otherwise, update all fields that are not undefined const updateColumns = columns.filter(col => { if (col === idProperty) return false; // If fieldsToUpdate is specified, only include those fields if (fieldsToUpdate && fieldsToUpdate.length > 0) { return fieldsToUpdate.includes(col); } // Otherwise, include all fields that are not undefined const value = entity[col]; return value !== undefined; }); if (updateColumns.length === 0) { // No fields to update, return entity as-is return entity; } const setClause = updateColumns .map(col => { const colName = columnMetadata[col].name || col; return `\`${colName}\` = ?`; }) .join(', '); const updateValues = updateColumns.map(col => { const value = entity[col]; return value === undefined ? null : value; }); const sql = `UPDATE \`${tableName}\` SET ${setClause} WHERE \`${idColumn}\` = ?`; await this.pool.execute(sql, [...updateValues, idValue]); return entity; } else { // Insert new entity // Exclude ID column if it's AUTO_INCREMENT const isAutoIncrement = generatedValue?.strategy === 'AUTO' && idProperty; // Only include columns that are actually set (not undefined) // Let the database handle default values - don't include undefined fields const insertColumns = columns.filter(col => { if (isAutoIncrement && col === idProperty) return false; const value = entity[col]; // Only include if value is explicitly set (not undefined) return value !== undefined; }); if (insertColumns.length === 0) { throw new Error(`Cannot insert entity: no columns to insert`); } const columnNames = insertColumns.map(col => columnMetadata[col].name || col); const placeholders = insertColumns.map(() => '?').join(', '); const insertValues = insertColumns.map(col => { const value = entity[col]; // Convert undefined to null for MySQL (shouldn't happen since we filtered, but safety check) return value === undefined ? null : value; }); const sql = `INSERT INTO ${tableName} (${columnNames.join(', ')}) VALUES (${placeholders})`; const [result] = await this.pool.execute(sql, insertValues); // Set the generated ID back to the entity if (isAutoIncrement && result.insertId) { entity[idProperty] = result.insertId; } return entity; } } /** * Find an entity by ID */ async find(entityClass, id) { const entityMetadata = Reflect.getMetadata(entity_1.ENTITY_METADATA_KEY, entityClass); const tableMetadata = Reflect.getMetadata(entity_1.TABLE_METADATA_KEY, entityClass); const columnMetadata = Reflect.getMetadata(entity_1.COLUMN_METADATA_KEY, entityClass) || {}; const idProperty = Reflect.getMetadata(entity_1.ID_METADATA_KEY, entityClass); if (!entityMetadata) { throw new Error(`Class ${entityClass.name} is not an entity`); } const tableName = tableMetadata?.name || entityClass.name.toLowerCase(); const idColumn = idProperty ? columnMetadata[idProperty]?.name || idProperty : 'id'; const sql = `SELECT * FROM ${tableName} WHERE ${idColumn} = ? LIMIT 1`; const [rows] = await this.pool.execute(sql, [id]); if (rows.length === 0) { return null; } return this.mapRowToEntity(entityClass, rows[0], columnMetadata); } /** * Find all entities */ async findAll(entityClass) { const entityMetadata = Reflect.getMetadata(entity_1.ENTITY_METADATA_KEY, entityClass); const tableMetadata = Reflect.getMetadata(entity_1.TABLE_METADATA_KEY, entityClass); const columnMetadata = Reflect.getMetadata(entity_1.COLUMN_METADATA_KEY, entityClass) || {}; if (!entityMetadata) { throw new Error(`Class ${entityClass.name} is not an entity`); } const tableName = tableMetadata?.name || entityClass.name.toLowerCase(); const sql = `SELECT * FROM ${tableName}`; const [rows] = await this.pool.execute(sql); const entities = rows.map(row => this.mapRowToEntity(entityClass, row, columnMetadata)); // Load EAGER relationships for all entities for (const entity of entities) { await this.loadRelationships(entity); } return entities; } /** * Remove an entity from the database */ async remove(entity) { const entityClass = entity.constructor; const entityMetadata = Reflect.getMetadata(entity_1.ENTITY_METADATA_KEY, entityClass); const tableMetadata = Reflect.getMetadata(entity_1.TABLE_METADATA_KEY, entityClass); const columnMetadata = Reflect.getMetadata(entity_1.COLUMN_METADATA_KEY, entityClass) || {}; const idProperty = Reflect.getMetadata(entity_1.ID_METADATA_KEY, entityClass); if (!entityMetadata) { throw new Error(`Class ${entityClass.name} is not an entity`); } if (!idProperty) { throw new Error(`Entity ${entityClass.name} does not have an ID property`); } const tableName = tableMetadata?.name || entityClass.name.toLowerCase(); const idColumn = columnMetadata[idProperty]?.name || idProperty; const idValue = entity[idProperty]; if (!idValue) { throw new Error(`Entity ${entityClass.name} does not have an ID value`); } const sql = `DELETE FROM ${tableName} WHERE ${idColumn} = ?`; await this.pool.execute(sql, [idValue]); } /** * Execute a custom query */ async query(sql, params) { const [rows] = await this.pool.execute(sql, params || []); return rows; } /** * Execute a custom query and return single result */ async queryOne(sql, params) { const results = await this.query(sql, params); return results.length > 0 ? results[0] : null; } /** * Create a query builder */ createQueryBuilder(entityClass) { return new QueryBuilder(this.pool, entityClass); } /** * Map database row to entity instance */ mapRowToEntity(entityClass, row, columnMetadata) { const entity = new entityClass(); const reverseColumnMap = {}; // Create reverse mapping: column name -> property name Object.keys(columnMetadata).forEach(prop => { const colName = columnMetadata[prop].name || prop; reverseColumnMap[colName] = prop; }); // Map row data to entity properties Object.keys(row).forEach(colName => { const propName = reverseColumnMap[colName] || colName; entity[propName] = row[colName]; }); return entity; } /** * Load relationships for an entity (EAGER loading) */ async loadRelationships(entity) { const entityClass = entity.constructor; // Load ManyToOne relationships (EAGER by default) const manyToOneRelations = Reflect.getMetadata(entity_1.MANY_TO_ONE_METADATA_KEY, entityClass) || {}; for (const [propertyKey, relationMeta] of Object.entries(manyToOneRelations)) { const meta = relationMeta; if (meta.fetch === 'EAGER' && meta.targetEntity) { const targetEntityClass = meta.targetEntity(); const related = await this.relationshipManager.loadManyToOne(entity, propertyKey, meta, targetEntityClass); entity[propertyKey] = related; } } // Load OneToOne relationships (EAGER by default) const oneToOneRelations = Reflect.getMetadata(entity_1.ONE_TO_ONE_METADATA_KEY, entityClass) || {}; for (const [propertyKey, relationMeta] of Object.entries(oneToOneRelations)) { const meta = relationMeta; if (meta.fetch === 'EAGER' && meta.targetEntity) { const targetEntityClass = meta.targetEntity(); const related = await this.relationshipManager.loadOneToOne(entity, propertyKey, meta, targetEntityClass); entity[propertyKey] = related; } } return entity; } /** * Get relationship manager for manual relationship loading */ getRelationshipManager() { return this.relationshipManager; } } exports.EntityManager = EntityManager; /** * Query Builder for type-safe queries */ class QueryBuilder { constructor(pool, entityClass) { this.pool = pool; this.entityClass = entityClass; this.selectClause = '*'; this.fromClause = ''; this.whereClauses = []; this.whereParams = []; this.orderByClause = ''; this.limitClause = ''; this.joinClauses = []; const tableMetadata = Reflect.getMetadata(entity_1.TABLE_METADATA_KEY, entityClass); this.fromClause = tableMetadata?.name || entityClass.name.toLowerCase(); } select(fields) { this.selectClause = fields; return this; } where(condition, ...params) { this.whereClauses.push(condition); // Convert undefined to null for MySQL compatibility const normalizedParams = params.map(param => param === undefined ? null : param); this.whereParams.push(...normalizedParams); return this; } andWhere(condition, ...params) { return this.where(condition, ...params); } orderBy(field, direction = 'ASC') { this.orderByClause = `ORDER BY ${field} ${direction}`; return this; } limit(count, offset) { this.limitClause = offset !== undefined ? `LIMIT ${offset}, ${count}` : `LIMIT ${count}`; return this; } join(table, condition) { this.joinClauses.push(`JOIN ${table} ON ${condition}`); return this; } leftJoin(table, condition) { this.joinClauses.push(`LEFT JOIN ${table} ON ${condition}`); return this; } async getMany() { const sql = this.buildQuery(); const [rows] = await this.pool.execute(sql, this.whereParams); return rows; } async getOne() { const results = await this.getMany(); return results.length > 0 ? results[0] : null; } buildQuery() { let sql = `SELECT ${this.selectClause} FROM ${this.fromClause}`; if (this.joinClauses.length > 0) { sql += ' ' + this.joinClauses.join(' '); } if (this.whereClauses.length > 0) { sql += ' WHERE ' + this.whereClauses.join(' AND '); } if (this.orderByClause) { sql += ' ' + this.orderByClause; } if (this.limitClause) { sql += ' ' + this.limitClause; } return sql; } } exports.QueryBuilder = QueryBuilder; //# sourceMappingURL=EntityManager.js.map