UNPKG

digitaltwin-core

Version:

Minimalist framework to collect and handle data in a Digital Twin project

290 lines 11.7 kB
import knex from 'knex'; import { DatabaseAdapter } from '../database_adapter.js'; import { mapToDataRecord } from '../../utils/map_to_data_record.js'; /** * Knex-based implementation with extended querying capabilities. */ export class KnexDatabaseAdapter extends DatabaseAdapter { #knex; #storage; constructor(config, storage) { super(); this.#knex = knex(config); this.#storage = storage; } /** * Create a KnexDatabaseAdapter for PostgreSQL with simplified configuration */ static forPostgreSQL(pgConfig, storage, tableName = 'data_index') { const knexConfig = { client: 'pg', connection: { host: pgConfig.host, port: pgConfig.port || 5432, user: pgConfig.user, password: pgConfig.password, database: pgConfig.database, ssl: pgConfig.ssl || false }, pool: { min: 2, max: 15, acquireTimeoutMillis: 30000, createTimeoutMillis: 30000, destroyTimeoutMillis: 5000, idleTimeoutMillis: 30000, reapIntervalMillis: 1000 } }; return new KnexDatabaseAdapter(knexConfig, storage); } /** * Create a KnexDatabaseAdapter for SQLite with simplified configuration */ static forSQLite(sqliteConfig, storage, tableName = 'data_index') { const client = sqliteConfig.client || 'sqlite3'; const knexConfig = { client, connection: { filename: sqliteConfig.filename }, pool: { min: 1, max: 5, acquireTimeoutMillis: sqliteConfig.busyTimeout || 30000, afterCreate: (conn, cb) => { if (sqliteConfig.enableForeignKeys !== false) { // Both sqlite3 and better-sqlite3 support PRAGMA if (client === 'better-sqlite3') { conn.pragma('foreign_keys = ON'); conn.pragma('journal_mode = WAL'); conn.pragma('synchronous = NORMAL'); conn.pragma('cache_size = 10000'); cb(); } else { conn.run('PRAGMA foreign_keys = ON', () => { conn.run('PRAGMA journal_mode = WAL', () => { conn.run('PRAGMA synchronous = NORMAL', () => { conn.run('PRAGMA cache_size = 10000', cb); }); }); }); } } else { cb(); } } }, useNullAsDefault: true }; return new KnexDatabaseAdapter(knexConfig, storage); } // ========== Basic methods ========== async save(meta) { const insertData = { id: meta.id, name: meta.name, type: meta.type, url: meta.url, date: meta.date.toISOString() }; // Add asset-specific fields if present (for AssetMetadataRow) if ('description' in meta) insertData.description = meta.description; if ('source' in meta) insertData.source = meta.source; if ('owner_id' in meta) insertData.owner_id = meta.owner_id; if ('filename' in meta) insertData.filename = meta.filename; await this.#knex(meta.name).insert(insertData); return mapToDataRecord(meta, this.#storage); } async delete(id, name) { await this.#knex(name).where({ id }).delete(); } async getById(id, name) { const row = await this.#knex(name).where({ id }).first(); return row ? mapToDataRecord(row, this.#storage) : undefined; } async getLatestByName(name) { const row = await this.#knex(name).select('*').orderBy('date', 'desc').limit(1).first(); return row ? mapToDataRecord(row, this.#storage) : undefined; } async doesTableExists(name) { return this.#knex.schema.hasTable(name); } async createTable(name) { const tableExists = await this.#knex.schema.hasTable(name); if (!tableExists) { await this.#knex.schema.createTable(name, table => { table.increments('id').primary(); table.string('name').notNullable(); table.string('type').notNullable(); table.string('url').notNullable(); table.datetime('date').notNullable(); // Asset-specific fields (optional, for AssetsManager components) table.text('description').nullable(); table.string('source').nullable(); table.string('owner_id').nullable(); table.string('filename').nullable(); // Optimized indexes for most frequent queries table.index('name', `${name}_idx_name`); table.index('date', `${name}_idx_date`); table.index(['name', 'date'], `${name}_idx_name_date`); table.index(['date', 'name'], `${name}_idx_date_name`); // For date range queries table.index('owner_id', `${name}_idx_owner_id`); // For asset filtering }); } } // ========== Extended methods ========== async getFirstByName(name) { const row = await this.#knex(name).orderBy('date', 'asc').first(); return row ? mapToDataRecord(row, this.#storage) : undefined; } async getByDateRange(name, startDate, endDate, limit) { let query = this.#knex(name).select('*').where('date', '>=', startDate.toISOString()); if (endDate) { query = query.where('date', '<', endDate.toISOString()); } query = query.orderBy('date', 'asc'); if (limit) { query = query.limit(limit); } const rows = await query; return rows.map(row => mapToDataRecord(row, this.#storage)); } async getAfterDate(name, afterDate, limit) { let query = this.#knex(name).where('date', '>', afterDate.toISOString()).orderBy('date', 'asc'); if (limit) { query = query.limit(limit); } const rows = await query; return rows.map(row => mapToDataRecord(row, this.#storage)); } async getLatestBefore(name, beforeDate) { const row = await this.#knex(name).where('date', '<', beforeDate.toISOString()).orderBy('date', 'desc').first(); return row ? mapToDataRecord(row, this.#storage) : undefined; } async getLatestRecordsBefore(name, beforeDate, limit) { const rows = await this.#knex(name) .where('date', '<', beforeDate.toISOString()) .orderBy('date', 'desc') .limit(limit); return rows.map(row => mapToDataRecord(row, this.#storage)); } async hasRecordsAfterDate(name, afterDate) { const result = await this.#knex(name) .where('date', '>', afterDate.toISOString()) .select(this.#knex.raw('1')) .limit(1) .first(); return !!result; } async countByDateRange(name, startDate, endDate) { let query = this.#knex(name).where('date', '>=', startDate.toISOString()); if (endDate) { query = query.where('date', '<', endDate.toISOString()); } const result = await query.count('* as count').first(); return Number(result?.count) || 0; } // ========== Batch operations for performance ========== async saveBatch(metadataList) { if (metadataList.length === 0) return []; // Group by table name for efficient batch inserts const groupedByTable = new Map(); for (const meta of metadataList) { if (!groupedByTable.has(meta.name)) { groupedByTable.set(meta.name, []); } groupedByTable.get(meta.name).push(meta); } const results = []; // Process each table in a transaction for consistency for (const [tableName, metas] of groupedByTable) { const insertData = metas.map(meta => { const data = { name: meta.name, type: meta.type, url: meta.url, date: meta.date.toISOString() }; // Only include ID if it's explicitly set (for updates) if (meta.id !== undefined) { data.id = meta.id; } // Add asset-specific fields if present if ('description' in meta) data.description = meta.description; if ('source' in meta) data.source = meta.source; if ('owner_id' in meta) data.owner_id = meta.owner_id; if ('filename' in meta) data.filename = meta.filename; return data; }); await this.#knex(tableName).insert(insertData); // Convert to DataRecords for (const meta of metas) { results.push(mapToDataRecord(meta, this.#storage)); } } return results; } async deleteBatch(deleteRequests) { if (deleteRequests.length === 0) return; // Group by table name for efficient batch deletes const groupedByTable = new Map(); for (const req of deleteRequests) { if (!groupedByTable.has(req.name)) { groupedByTable.set(req.name, []); } groupedByTable.get(req.name).push(req.id); } // Process each table for (const [tableName, ids] of groupedByTable) { await this.#knex(tableName).whereIn('id', ids).delete(); } } async getByIdsBatch(requests) { if (requests.length === 0) return []; const results = []; // Group by table name for efficient queries const groupedByTable = new Map(); for (const req of requests) { if (!groupedByTable.has(req.name)) { groupedByTable.set(req.name, []); } groupedByTable.get(req.name).push(req.id); } // Query each table for (const [tableName, ids] of groupedByTable) { const rows = await this.#knex(tableName).whereIn('id', ids); for (const row of rows) { results.push(mapToDataRecord(row, this.#storage)); } } return results; } // ========== Optimized query for assets manager ========== async getAllAssetsPaginated(name, offset = 0, limit = 100) { // Get total count efficiently const countResult = await this.#knex(name).count('* as count').first(); const total = Number(countResult?.count) || 0; // Get paginated results const rows = await this.#knex(name).select('*').orderBy('date', 'desc').offset(offset).limit(limit); const records = rows.map(row => mapToDataRecord(row, this.#storage)); return { records, total }; } async close() { await this.#knex.destroy(); } } //# sourceMappingURL=knex_database_adapter.js.map