digitaltwin-core
Version:
Minimalist framework to collect and handle data in a Digital Twin project
290 lines • 11.7 kB
JavaScript
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