UNPKG

@sehirapp/core-microservice

Version:

Modern mikroservis core paketi - MongoDB 6.7, Express API, Mongoose, PM2 cluster desteği

1,157 lines (999 loc) 38.9 kB
import { mongoose } from "./database.js"; import dbManager from "./database.js"; import * as f from "../utils/functions.js"; import { logger } from "./logger.js"; /** * Modern CoreClass - Mongoose şemaları ile entegrasyon * Mikroservislerde temel model sınıfı */ export default class CoreClass { constructor() { this.dbManager = dbManager; this._mongooseModel = null; this._useMongoose = process.env.USE_MONGOOSE !== 'false'; this._schema = null; this.connectionType = this._useMongoose ? 'mongoose' : 'mongodb'; // Default timestamp'ler - epoch milisaniye const now = Date.now(); if (!this.createdAt) this.createdAt = now; if (!this.updatedAt) this.updatedAt = now; } /** * Mongoose şeması tanımla - alt sınıflarda override edilecek */ Schema() { throw new Error('Schema() method must be implemented in subclass'); } /** * Collection adı - alt sınıflarda override edilecek */ Collection() { throw new Error('Collection() method must be implemented in subclass'); } /** * Unique alanlar - alt sınıflarda override edilecek */ Uniques() { return ['id']; } /** * Mongoose model'ini al veya oluştur */ getMongooseModel() { if (!this._mongooseModel) { const schema = this.Schema(); if (!schema) { throw new Error('Schema is required for Mongoose model'); } // Şemaya otomatik alanlar ekle - Epoch milisaniye timestamp olarak if (!schema.paths.createdAt) { schema.add({ createdAt: { type: Number, default: Date.now, index: true } }); } if (!schema.paths.updatedAt) { schema.add({ updatedAt: { type: Number, default: Date.now, index: true } }); } const modelName = this.constructor.name; // Model zaten var mı kontrol et if (mongoose.models[modelName]) { this._mongooseModel = mongoose.models[modelName]; } else { this._mongooseModel = mongoose.model(modelName, schema, this.Collection()); } } return this._mongooseModel; } /** * Veri parametrelerini ayarla */ setParameters(data, schema = null) { if (!data) return; const schemaDefinition = schema || this.Schema(); if (!schemaDefinition) return; const paths = schemaDefinition.paths || {}; Object.keys(data).forEach(key => { if (data[key] !== undefined && data[key] !== null) { const schemaPath = paths[key]; // Date alanlarını epoch milisaniye'ye çevir (Number tip) if (schemaPath && schemaPath.instance === 'Number' && (key === 'createdAt' || key === 'updatedAt' || key === 'deletedAt' || key.includes('At'))) { if (data[key] instanceof Date) { this[key] = data[key].getTime(); } else if (typeof data[key] === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(data[key])) { this[key] = new Date(data[key]).getTime(); } else if (typeof data[key] === 'number') { this[key] = data[key]; // Zaten epoch format } else { this[key] = data[key]; } // Formatlanmış tarih alanları ekle this[key + "_F"] = f.dateFormat(this[key]); this[key + "_T"] = f.timeFormat(this[key]); this[key + "_FT"] = f.dateFormat(this[key]) + " " + f.timeFormat(this[key]); } else { // Normal alanlar this[key] = data[key]; // Legacy Date formatlaması (eski Date tip alanları için) if (schemaPath && schemaPath.instance === 'Date') { this[key + "_F"] = f.dateFormat(data[key]); this[key + "_T"] = f.timeFormat(data[key]); this[key + "_FT"] = f.dateFormat(data[key]) + " " + f.timeFormat(data[key]); } // Para formatlaması if (schemaPath && schemaPath.options && schemaPath.options.type === 'money') { this[key + "_F"] = f.monetize(data[key]); } } } }); } /** * Database bağlantısını sağla */ async ensureConnection() { if (this._useMongoose) { await this.dbManager.connectMongoose(); } else { await this.dbManager.connect(); } } /** * Kayıt ara - Mongoose ve MongoDB native destekli */ async find(findBy = null) { const startTime = Date.now(); const filter = findBy || { id: this.id }; try { logger.debug(`Finding ${this.constructor.name}`, { filter }); await this.ensureConnection(); let result = null; if (this._useMongoose) { const Model = this.getMongooseModel(); result = await Model.findOne(filter).lean(); } else { const results = await this.dbManager.find(this.Collection(), filter, { limit: 1 }); result = results[0] || null; } const duration = Date.now() - startTime; if (result) { // Sonucu bu objeye ata Object.keys(result).forEach(key => { this[key] = result[key]; }); logger.info(`${this.constructor.name} found`, { id: this.id, duration: `${duration}ms` }); return true; } else { logger.warn(`${this.constructor.name} not found`, { filter, duration: `${duration}ms` }); return false; } } catch (error) { const duration = Date.now() - startTime; logger.error(`Error finding ${this.constructor.name}`, { error: error.message, filter, duration: `${duration}ms` }); throw error; } } /** * Kayıt var mı kontrol et - unique alanlar kontrollü */ async ifExists() { const startTime = Date.now(); try { const uniques = this.Uniques(); const orConditions = [{ id: this.id }]; // Unique alanları kontrol et uniques.forEach(key => { if (this[key] !== undefined && this[key] !== null && this[key] !== '') { const condition = {}; condition[key] = this[key]; orConditions.push(condition); } }); const filter = { $or: orConditions }; await this.ensureConnection(); let result = null; if (this._useMongoose) { const Model = this.getMongooseModel(); result = await Model.findOne(filter).lean(); } else { const results = await this.dbManager.find(this.Collection(), filter, { limit: 1 }); result = results[0] || null; } const duration = Date.now() - startTime; if (result) { // Hangi unique alan çakışıyor bul for (const key of uniques) { if (result[key] === this[key]) { logger.info(`${this.constructor.name} exists - unique conflict`, { conflictKey: key, duration: `${duration}ms` }); return key; } } logger.info(`${this.constructor.name} exists`, { id: result.id, duration: `${duration}ms` }); return true; } else { logger.debug(`${this.constructor.name} does not exist`, { duration: `${duration}ms` }); return false; } } catch (error) { const duration = Date.now() - startTime; logger.error(`Error checking existence of ${this.constructor.name}`, { error: error.message, duration: `${duration}ms` }); throw error; } } /** * Yeni kayıt ekle */ async insert() { const startTime = Date.now(); try { logger.debug(`Inserting ${this.constructor.name}`, { id: this.id }); const exists = await this.ifExists(); if (exists === false) { // Temiz veri hazırla const cleanData = this.getCleanData(); await this.ensureConnection(); if (this._useMongoose) { const Model = this.getMongooseModel(); const doc = new Model(cleanData); await doc.save(); // Kaydedilen veriyi geri al Object.keys(doc.toObject()).forEach(key => { this[key] = doc[key]; }); } else { await this.dbManager.insert(this.Collection(), cleanData); } const duration = Date.now() - startTime; logger.info(`${this.constructor.name} inserted`, { id: this.id, duration: `${duration}ms` }); return true; } else if (exists === true) { const duration = Date.now() - startTime; logger.warn(`${this.constructor.name} insert failed - already exists`, { id: this.id, duration: `${duration}ms` }); return "Bu kayıt zaten mevcut!"; } else { const duration = Date.now() - startTime; logger.warn(`${this.constructor.name} insert failed - unique constraint`, { id: this.id, conflictKey: exists, duration: `${duration}ms` }); return `Bu kayıt zaten mevcut! (${exists})`; } } catch (error) { const duration = Date.now() - startTime; logger.error(`Error inserting ${this.constructor.name}`, { error: error.message, id: this.id, duration: `${duration}ms` }); throw error; } } /** * Kayıt güncelle */ async update() { const startTime = Date.now(); try { logger.debug(`Updating ${this.constructor.name}`, { id: this.id }); const cleanData = this.getCleanData(); cleanData.updatedAt = Date.now(); // Epoch milisaniye timestamp await this.ensureConnection(); if (this._useMongoose) { const Model = this.getMongooseModel(); const result = await Model.updateOne({ id: this.id }, cleanData); const duration = Date.now() - startTime; if (result.modifiedCount > 0) { logger.info(`${this.constructor.name} updated`, { id: this.id, duration: `${duration}ms` }); } else { logger.warn(`${this.constructor.name} update - no changes`, { id: this.id, duration: `${duration}ms` }); } } else { await this.dbManager.update(this.Collection(), { id: this.id }, cleanData); const duration = Date.now() - startTime; logger.info(`${this.constructor.name} updated`, { id: this.id, duration: `${duration}ms` }); } return true; } catch (error) { const duration = Date.now() - startTime; logger.error(`Error updating ${this.constructor.name}`, { error: error.message, id: this.id, duration: `${duration}ms` }); throw error; } } /** * Kaydet (insert veya update) */ async save() { const startTime = Date.now(); try { logger.debug(`Saving ${this.constructor.name}`, { id: this.id }); const exists = await this.find(); let result; if (exists) { result = await this.update(); logger.info(`${this.constructor.name} saved via update`, { id: this.id, duration: `${Date.now() - startTime}ms` }); } else { result = await this.insert(); logger.info(`${this.constructor.name} saved via insert`, { id: this.id, duration: `${Date.now() - startTime}ms` }); } return result; } catch (error) { const duration = Date.now() - startTime; logger.error(`Error saving ${this.constructor.name}`, { error: error.message, id: this.id, duration: `${duration}ms` }); return false; } } /** * Kayıt sil */ async delete() { const startTime = Date.now(); try { logger.debug(`Deleting ${this.constructor.name}`, { id: this.id }); await this.ensureConnection(); if (this._useMongoose) { const Model = this.getMongooseModel(); const result = await Model.deleteOne({ id: this.id }); const duration = Date.now() - startTime; if (result.deletedCount > 0) { logger.info(`${this.constructor.name} deleted`, { id: this.id, duration: `${duration}ms` }); return true; } else { logger.warn(`${this.constructor.name} delete - not found`, { id: this.id, duration: `${duration}ms` }); return false; } } else { const result = await this.dbManager.remove(this.Collection(), { id: this.id }); const duration = Date.now() - startTime; logger.info(`${this.constructor.name} deleted`, { id: this.id, duration: `${duration}ms` }); return result; } } catch (error) { const duration = Date.now() - startTime; logger.error(`Error deleting ${this.constructor.name}`, { error: error.message, id: this.id, duration: `${duration}ms` }); throw error; } } /** * Tüm kayıtları getir */ async GetAll(filter = {}, options = {}) { const startTime = Date.now(); try { const { sort = null, limit = 1000000, skip = 0, project = null, populate = null } = options; logger.debug(`Getting all ${this.constructor.name} records`, { filter, sort, limit, skip, project }); await this.ensureConnection(); let results = []; if (this._useMongoose) { const Model = this.getMongooseModel(); let query = Model.find(filter); if (sort) query = query.sort(sort); if (skip > 0) query = query.skip(skip); if (limit < 1000000) query = query.limit(limit); if (project) query = query.select(project); if (populate) query = query.populate(populate); results = await query.lean(); } else { results = await this.dbManager.find(this.Collection(), filter, { sort, limit, skip, project }); } const duration = Date.now() - startTime; logger.info(`Retrieved ${results.length} ${this.constructor.name} records`, { count: results.length, duration: `${duration}ms` }); return results; } catch (error) { const duration = Date.now() - startTime; logger.error(`Error getting all ${this.constructor.name} records`, { error: error.message, filter, duration: `${duration}ms` }); return []; } } /** * Tek kayıt getir */ async findOne(filter = {}) { const startTime = Date.now(); try { await this.ensureConnection(); let result = null; if (this._useMongoose) { const Model = this.getMongooseModel(); result = await Model.findOne(filter).lean(); } else { const results = await this.dbManager.find(this.Collection(), filter, { limit: 1 }); result = results[0] || null; } const duration = Date.now() - startTime; if (result) { logger.debug(`${this.constructor.name} findOne success`, { filter, duration: `${duration}ms` }); return result; } else { logger.debug(`${this.constructor.name} findOne - not found`, { filter, duration: `${duration}ms` }); return null; } } catch (error) { const duration = Date.now() - startTime; logger.error(`Error in ${this.constructor.name} findOne`, { error: error.message, filter, duration: `${duration}ms` }); throw error; } } /** * Kayıt sayısı */ async count(filter = {}) { const startTime = Date.now(); try { await this.ensureConnection(); let count = 0; if (this._useMongoose) { const Model = this.getMongooseModel(); count = await Model.countDocuments(filter); } else { count = await this.dbManager.count(this.Collection(), filter); } const duration = Date.now() - startTime; logger.debug(`${this.constructor.name} count: ${count}`, { filter, duration: `${duration}ms` }); return count; } catch (error) { const duration = Date.now() - startTime; logger.error(`Error counting ${this.constructor.name}`, { error: error.message, filter, duration: `${duration}ms` }); return 0; } } /** * Temiz veri hazırla - formatlanmış alanları çıkar */ getCleanData() { const cleanData = {}; Object.keys(this).forEach(key => { // Formatlanmış alanları ve private alanları çıkar if (!key.includes("_F") && !key.includes("_T") && !key.includes("_FT") && !key.startsWith("_") && key !== 'dbManager' && key !== 'connectionType' && // Database connection type'ı çıkar typeof this[key] !== 'function' ) { cleanData[key] = this[key]; } }); return cleanData; } /** * JSON çıktısı - Database'de zaten epoch milisaniye formatında */ toJSON() { const data = this.getCleanData(); // Date alanları zaten epoch formatında (Number tip) // Legacy support için ISO string'leri epoch'a çevir Object.keys(data).forEach(key => { const value = data[key]; // Legacy Date object'leri epoch'a çevir if (value instanceof Date) { data[key] = value.getTime(); } // Legacy ISO string'leri epoch'a çevir else if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) { data[key] = new Date(value).getTime(); } // Nested object'lerde de kontrol et if (value && typeof value === 'object' && !Array.isArray(value)) { Object.keys(value).forEach(nestedKey => { const nestedValue = value[nestedKey]; if (nestedValue instanceof Date) { value[nestedKey] = nestedValue.getTime(); } else if (typeof nestedValue === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(nestedValue)) { value[nestedKey] = new Date(nestedValue).getTime(); } }); } }); return data; } /** * Toplu işlemler framework'ü - çoklu kayıt işlemleri için * @param {Array} items - İşlenecek kayıtlar * @param {string} operation - İşlem tipi ('create', 'update', 'delete') * @param {Object} options - İşlem seçenekleri * @returns {Promise<Object>} İşlem sonuçları */ async executeBulkOperation(items, operation, options = {}) { const startTime = Date.now(); const { continueOnError = false, validateBefore = true, batchSize = 100, parallel = false } = options; const results = { success: [], failed: [], total: items.length, operation, duration: 0, successCount: 0, failedCount: 0 }; if (!Array.isArray(items) || items.length === 0) { logger.warn('Bulk operation called with invalid items', { itemType: typeof items, itemLength: items?.length, operation }); return results; } try { logger.info(`Starting bulk ${operation} operation`, { totalItems: items.length, batchSize, parallel, model: this.constructor.name }); // Batch işlem fonksiyonu const processBatch = async (batch, batchIndex) => { const batchResults = { success: [], failed: [] }; if (parallel) { // Paralel işlem const promises = batch.map(async (item, itemIndex) => { const globalIndex = batchIndex * batchSize + itemIndex; return this.processItem(item, operation, validateBefore, globalIndex); }); const batchResponses = await Promise.allSettled(promises); batchResponses.forEach((response, itemIndex) => { const globalIndex = batchIndex * batchSize + itemIndex; if (response.status === 'fulfilled') { batchResults.success.push({ index: globalIndex, item: batch[itemIndex], result: response.value }); } else { batchResults.failed.push({ index: globalIndex, item: batch[itemIndex], error: response.reason?.message || 'Unknown error' }); } }); } else { // Seri işlem for (let itemIndex = 0; itemIndex < batch.length; itemIndex++) { const globalIndex = batchIndex * batchSize + itemIndex; const item = batch[itemIndex]; try { const result = await this.processItem(item, operation, validateBefore, globalIndex); batchResults.success.push({ index: globalIndex, item, result }); } catch (error) { batchResults.failed.push({ index: globalIndex, item, error: error.message }); if (!continueOnError) { logger.warn('Bulk operation stopped due to error', { error: error.message, itemIndex: globalIndex, operation }); break; } } } } return batchResults; }; // Batch'lere ayır ve işle for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); const batchIndex = Math.floor(i / batchSize); logger.debug(`Processing batch ${batchIndex + 1}`, { batchSize: batch.length, startIndex: i, operation }); const batchResults = await processBatch(batch, batchIndex); results.success.push(...batchResults.success); results.failed.push(...batchResults.failed); if (!continueOnError && batchResults.failed.length > 0) { break; } } results.successCount = results.success.length; results.failedCount = results.failed.length; results.duration = Date.now() - startTime; logger.info(`Bulk ${operation} operation completed`, { total: results.total, success: results.successCount, failed: results.failedCount, duration: `${results.duration}ms`, model: this.constructor.name }); return results; } catch (error) { results.duration = Date.now() - startTime; logger.error(`Bulk ${operation} operation failed`, { error: error.message, processed: results.success.length + results.failed.length, total: results.total, duration: `${results.duration}ms`, stack: error.stack }); throw error; } } /** * Tek kayıt işlemi - bulk operation helper */ async processItem(item, operation, validateBefore, index) { const modelInstance = new this.constructor(item); if (validateBefore) { const validation = modelInstance.validate(); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } } let result; switch (operation.toLowerCase()) { case 'create': case 'insert': result = await modelInstance.insert(); break; case 'update': if (item.id) { const found = await modelInstance.find({ id: item.id }); if (!found) { throw new Error(`Record not found for update: ${item.id}`); } modelInstance.setParameters(item); result = await modelInstance.update(); } else { throw new Error('ID is required for update operation'); } break; case 'delete': if (item.id) { const found = await modelInstance.find({ id: item.id }); if (!found) { throw new Error(`Record not found for delete: ${item.id}`); } result = await modelInstance.delete(); } else { throw new Error('ID is required for delete operation'); } break; case 'save': case 'upsert': result = await modelInstance.save(); break; default: throw new Error(`Unsupported bulk operation: ${operation}`); } return result; } /** * Metadata yönetimi - model'e metadata ekleme/güncelleme * @param {string} field - Metadata alanı * @param {any} value - Değer */ updateMetadata(field, value) { if (!field || typeof field !== 'string') { throw new Error('Metadata field must be a non-empty string'); } // Metadata objesi yoksa oluştur if (!this.metadata || typeof this.metadata !== 'object') { this.metadata = {}; } // Eski değeri kaydet (history için) const oldValue = this.metadata[field]; // Yeni değeri ata this.metadata[field] = value; this.metadata.lastUpdatedAt = Date.now(); // Metadata history oluştur if (!this.metadata._history) { this.metadata._history = []; } this.metadata._history.push({ field, oldValue, newValue: value, updatedAt: Date.now(), updatedBy: this.metadata.updatedBy || 'system' }); // History'yi maksimum 50 kayıt ile sınırla if (this.metadata._history.length > 50) { this.metadata._history = this.metadata._history.slice(-50); } // updatedAt'i güncelle this.updatedAt = Date.now(); logger.debug(`Metadata updated`, { model: this.constructor.name, id: this.id, field, oldValue: typeof oldValue, newValue: typeof value }); return this; } /** * Metadata silme * @param {string} field - Silinecek metadata alanı */ removeMetadata(field) { if (!this.metadata || !field) return this; const oldValue = this.metadata[field]; delete this.metadata[field]; // History'ye ekle this.updateMetadata('_removed', { field, value: oldValue, removedAt: Date.now() }); logger.debug(`Metadata removed`, { model: this.constructor.name, id: this.id, field, oldValue: typeof oldValue }); return this; } /** * Metadata sorgulama * @param {string} field - Sorgulanacak alan * @returns {any} Metadata değeri */ getMetadata(field) { if (!this.metadata) return undefined; return this.metadata[field]; } /** * Status yönetimi - doğrulama ile * @param {string} newStatus - Yeni status * @param {Array<string>} validStatuses - Geçerli status'lar * @param {Object} options - Seçenekler */ updateStatus(newStatus, validStatuses = [], options = {}) { const { reason = null, updatedBy = 'system', allowSameStatus = false, validateTransition = true } = options; if (!newStatus || typeof newStatus !== 'string') { throw new Error('Status must be a non-empty string'); } // Geçerli status kontrolü if (validStatuses.length > 0 && !validStatuses.includes(newStatus)) { throw new Error(`Invalid status "${newStatus}". Valid statuses: ${validStatuses.join(', ')}`); } const oldStatus = this.status; // Aynı status kontrolü if (!allowSameStatus && oldStatus === newStatus) { logger.debug(`Status update skipped - same status`, { model: this.constructor.name, id: this.id, status: newStatus }); return this; } // Status geçiş validasyonu (alt sınıflarda override edilebilir) if (validateTransition && this.validateStatusTransition) { const isValidTransition = this.validateStatusTransition(oldStatus, newStatus); if (!isValidTransition) { throw new Error(`Invalid status transition from "${oldStatus}" to "${newStatus}"`); } } // Status güncelle this.status = newStatus; this.updatedAt = Date.now(); // Status history oluştur if (!this.statusHistory) { this.statusHistory = []; } this.statusHistory.push({ from: oldStatus, to: newStatus, updatedAt: Date.now(), updatedBy, reason }); // History'yi maksimum 100 kayıt ile sınırla if (this.statusHistory.length > 100) { this.statusHistory = this.statusHistory.slice(-100); } // Metadata'ya da kaydet this.updateMetadata('lastStatusChange', { from: oldStatus, to: newStatus, updatedAt: Date.now(), updatedBy, reason }); logger.info(`Status updated`, { model: this.constructor.name, id: this.id, from: oldStatus, to: newStatus, reason, updatedBy }); return this; } /** * Status geçiş validasyonu - alt sınıflarda override edilebilir * @param {string} fromStatus - Mevcut status * @param {string} toStatus - Hedef status * @returns {boolean} Geçiş geçerli mi */ validateStatusTransition(fromStatus, toStatus) { // Varsayılan: tüm geçişlere izin ver // Alt sınıflarda specific business logic uygulanabilir return true; } /** * Model instance'ının son aktivite zamanını güncelle */ touch() { this.updatedAt = Date.now(); this.updateMetadata('lastTouch', Date.now()); return this; } /** * Model soft delete * @param {Object} options - Silme seçenekleri */ async softDelete(options = {}) { const { deletedBy = 'system', reason = null } = options; this.deletedAt = Date.now(); this.isDeleted = true; // Metadata güncelle this.updateMetadata('deletion', { deletedAt: Date.now(), deletedBy, reason }); // Status güncelle if (this.status && this.status !== 'deleted') { this.updateStatus('deleted', [], { reason: `Soft deleted: ${reason || 'No reason provided'}`, updatedBy: deletedBy }); } const result = await this.update(); logger.info(`Model soft deleted`, { model: this.constructor.name, id: this.id, deletedBy, reason }); return result; } /** * Soft delete'i geri al * @param {Object} options - Restore seçenekleri */ async restore(options = {}) { const { restoredBy = 'system', newStatus = 'active' } = options; this.deletedAt = null; this.isDeleted = false; // Metadata güncelle this.updateMetadata('restoration', { restoredAt: Date.now(), restoredBy }); // Status güncelle this.updateStatus(newStatus, [], { reason: 'Restored from soft delete', updatedBy: restoredBy }); const result = await this.update(); logger.info(`Model restored`, { model: this.constructor.name, id: this.id, restoredBy, newStatus }); return result; } /** * Validation - alt sınıflarda override edilebilir */ validate() { return { isValid: true, errors: [] }; } }