@timescaledb/typeorm
Version:
This is the official TimescaleDB plugin for TypeORM.
325 lines • 15.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const typeorm_1 = require("typeorm");
const core_1 = require("@timescaledb/core");
const Hypertable_1 = require("../decorators/Hypertable");
const TimescaleRepository_1 = require("../repository/TimescaleRepository");
const ContinuousAggregate_1 = require("../decorators/ContinuousAggregate");
const AggregateColumn_1 = require("../decorators/AggregateColumn");
const schemas_1 = require("@timescaledb/schemas");
const BucketColumn_1 = require("../decorators/BucketColumn");
const Rollup_1 = require("../decorators/Rollup");
const CandlestickColumn_1 = require("../decorators/CandlestickColumn");
const RollupColumn_1 = require("../decorators/RollupColumn");
const TimeColumn_1 = require("../decorators/TimeColumn");
const debug_1 = require("../debug");
const debug = (0, debug_1.debugTypeOrm)('migration');
const originalRunMigrations = typeorm_1.DataSource.prototype.runMigrations;
const originalUndoLastMigration = typeorm_1.DataSource.prototype.undoLastMigration;
const originalSynchronize = typeorm_1.DataSource.prototype.synchronize;
const originalInitialize = typeorm_1.DataSource.prototype.initialize;
const eligibleEntityMetadatas = (entityMetadatas) => {
return entityMetadatas.filter((entity) => {
return typeof entity.target !== 'string';
});
};
typeorm_1.DataSource.prototype.initialize = async function () {
debug('Initializing TimescaleDB');
const connection = await originalInitialize.call(this);
for (const entity of eligibleEntityMetadatas(this.entityMetadatas)) {
const hypertableOptions = Reflect.getMetadata(Hypertable_1.HYPERTABLE_METADATA_KEY, entity.target);
const aggregateOptions = Reflect.getMetadata(ContinuousAggregate_1.CONTINUOUS_AGGREGATE_METADATA_KEY, entity.target);
const rollupOptions = Reflect.getMetadata(Rollup_1.ROLLUP_METADATA_KEY, entity.target);
if (hypertableOptions || aggregateOptions || rollupOptions) {
const repository = this.getRepository(entity.target);
Object.assign(repository, TimescaleRepository_1.timescaleMethods);
}
}
debug('TimescaleDB initialized');
return connection;
};
async function setupTimescaleExtension(dataSource) {
debug('Setting up TimescaleDB extension');
try {
const extension = core_1.TimescaleDB.createExtension();
await dataSource.query(extension.up().build());
await dataSource.query('CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit;');
}
catch (error) {
debug('Error setting up TimescaleDB extension:', error);
if (!error.message.includes('extension "timescaledb" already exists') &&
!error.message.includes('extension "timescaledb_toolkit" already exists')) {
throw error;
}
}
}
typeorm_1.DataSource.prototype.runMigrations = async function (options) {
debug('Running migrations');
const migrations = await originalRunMigrations.call(this, options);
await setupTimescaleObjects(this);
debug('Migrations complete');
return migrations;
};
typeorm_1.DataSource.prototype.undoLastMigration = async function (options) {
await removeTimescaleObjects(this);
return originalUndoLastMigration.call(this, options);
};
typeorm_1.DataSource.prototype.synchronize = async function (dropBeforeSync = false) {
if (dropBeforeSync) {
await removeTimescaleObjects(this);
}
await originalSynchronize.call(this, dropBeforeSync);
await setupTimescaleObjects(this);
};
async function setupHypertables(dataSource) {
debug('Setting up hypertables');
const entities = dataSource.entityMetadatas;
for await (const entity of entities) {
const options = Reflect.getMetadata(Hypertable_1.HYPERTABLE_METADATA_KEY, entity.target);
if (options) {
const hypertable = core_1.TimescaleDB.createHypertable(entity.tableName, options);
const hypertableCheck = await dataSource.query(hypertable.inspect().build());
if (!hypertableCheck[0].table_exists) {
debug(`Table ${entity.tableName} does not exist, skipping hypertable setup`);
continue;
}
if (hypertableCheck[0].is_hypertable) {
debug(`Hypertable for ${entity.tableName} already exists`);
continue;
}
debug(`Setting up hypertable for ${entity.tableName}`);
await dataSource.query(hypertable.up().build());
const repository = dataSource.getRepository(entity.target);
Object.assign(repository, TimescaleRepository_1.timescaleMethods);
}
}
debug('Hypertables setup');
}
async function removeHypertables(dataSource) {
debug('Removing hypertables');
const entities = dataSource.entityMetadatas;
for await (const entity of entities) {
const options = Reflect.getMetadata(Hypertable_1.HYPERTABLE_METADATA_KEY, entity.target);
if (options) {
const hypertable = core_1.TimescaleDB.createHypertable(entity.tableName, options);
const hypertableCheck = await dataSource.query(hypertable.inspect().build());
if (!hypertableCheck[0].is_hypertable) {
debug(`Table ${entity.tableName} is not a hypertable, skipping removal`);
continue;
}
await dataSource.query(hypertable.down().build());
debug(`Hypertable for ${entity.tableName} removed`);
}
}
}
async function setupTimescaleObjects(dataSource) {
if (!dataSource.isInitialized) {
throw new Error('DataSource must be initialized before setting up TimescaleDB objects');
}
await setupTimescaleExtension(dataSource);
await setupTimeColumns(dataSource);
await setupHypertables(dataSource);
await setupContinuousAggregates(dataSource);
await setupRollups(dataSource);
}
async function removeContinuousAggregates(dataSource) {
debug('Removing continuous aggregates');
const entities = dataSource.entityMetadatas;
for (const entity of entities) {
const aggregateMetadata = Reflect.getMetadata(ContinuousAggregate_1.CONTINUOUS_AGGREGATE_METADATA_KEY, entity.target);
if (!aggregateMetadata)
continue;
const aggregate = core_1.TimescaleDB.createContinuousAggregate(entity.tableName, '', // Source table not needed for down()
aggregateMetadata.options);
const statements = aggregate.down().build();
for (const sql of statements) {
await dataSource.query(sql);
debug(`Continuous aggregate for ${entity.tableName} removed`);
}
}
debug('Continuous aggregates removed');
}
async function removeTimescaleObjects(dataSource) {
if (!dataSource.isInitialized) {
throw new Error('DataSource must be initialized before removing TimescaleDB objects');
}
await removeRollups(dataSource);
await removeContinuousAggregates(dataSource);
await removeHypertables(dataSource);
}
async function validateAggregateColumns(dataSource) {
for (const entity of dataSource.entityMetadatas) {
const aggregateColumns =
// @ts-ignore
Reflect.getMetadata(AggregateColumn_1.AGGREGATE_COLUMN_METADATA_KEY, entity.target.prototype) ||
Reflect.getMetadata(AggregateColumn_1.AGGREGATE_COLUMN_METADATA_KEY, entity.target);
if (aggregateColumns) {
const continuousAggregateMetadata = Reflect.getMetadata(ContinuousAggregate_1.CONTINUOUS_AGGREGATE_METADATA_KEY, entity.target);
if (!continuousAggregateMetadata) {
throw new Error(`Class ${entity.name} uses @AggregateColumn but is not decorated with @ContinuousAggregate`);
}
const { sourceModel } = continuousAggregateMetadata;
const sourceMetadata = (0, typeorm_1.getMetadataArgsStorage)().tables.find((table) => table.target === sourceModel);
if (!sourceMetadata) {
throw new Error(`Source model for ${entity.name} is not a valid TypeORM entity`);
}
}
}
}
async function setupContinuousAggregates(dataSource) {
debug('Setting up continuous aggregates');
const entities = dataSource.entityMetadatas;
for (const entity of entities) {
const aggregateMetadata = Reflect.getMetadata(ContinuousAggregate_1.CONTINUOUS_AGGREGATE_METADATA_KEY, entity.target);
if (!aggregateMetadata)
continue;
const sourceMetadata = dataSource.getMetadata(aggregateMetadata.sourceModel);
const sourceTableName = sourceMetadata.tableName;
const sourceOptions = Reflect.getMetadata(Hypertable_1.HYPERTABLE_METADATA_KEY, aggregateMetadata.sourceModel);
const sourceHypertable = core_1.TimescaleDB.createHypertable(sourceTableName, sourceOptions);
const hypertableCheck = await dataSource.query(sourceHypertable.inspect().build());
if (!hypertableCheck[0].is_hypertable) {
debug(`Source table ${sourceTableName} is not a hypertable, skipping continuous aggregate setup`);
continue;
}
await validateAggregateColumns(dataSource);
const aggregateColumns =
// @ts-ignore
Reflect.getMetadata(AggregateColumn_1.AGGREGATE_COLUMN_METADATA_KEY, entity.target.prototype) ||
Reflect.getMetadata(AggregateColumn_1.AGGREGATE_COLUMN_METADATA_KEY, entity.target);
if (!aggregateColumns) {
const error = `No aggregates defined for continuous aggregate ${entity.tableName}`;
debug(error);
throw new Error(error);
}
// @ts-ignore
const bucketMetadata = (0, BucketColumn_1.validateBucketColumn)(entity.target);
const candlestickMetadata = Reflect.getMetadata(CandlestickColumn_1.CANDLESTICK_COLUMN_METADATA_KEY, entity.target);
if (candlestickMetadata) {
aggregateColumns[candlestickMetadata.propertyKey.toString()] = {
type: 'candlestick',
time_column: candlestickMetadata.time_column,
price_column: candlestickMetadata.price_column,
volume_column: candlestickMetadata.volume_column,
column_alias: candlestickMetadata.propertyKey.toString(),
};
}
aggregateMetadata.options.aggregates = {
[bucketMetadata.propertyKey.toString()]: {
type: schemas_1.AggregateType.Bucket,
column: bucketMetadata.source_column,
column_alias: bucketMetadata.propertyKey.toString(),
},
...aggregateMetadata.options.aggregates,
...Object.entries(aggregateColumns).reduce((acc, [key, value]) => {
// @ts-ignore
acc[key] = value;
return acc;
}, {}),
};
const aggregate = core_1.TimescaleDB.createContinuousAggregate(entity.tableName, sourceTableName, aggregateMetadata.options);
const exists = await dataSource.query(aggregate.inspect().build());
if (!exists[0].hypertable_exists) {
await dataSource.query(aggregate.up().build());
const refreshPolicy = aggregate.up().getRefreshPolicy();
if (refreshPolicy) {
await dataSource.query(refreshPolicy);
}
}
debug(`Continuous aggregate for ${entity.tableName} set up`);
}
debug('Continuous aggregates setup');
}
async function setupRollups(dataSource) {
debug('Setting up rollups');
for (const entity of eligibleEntityMetadatas(dataSource.entityMetadatas)) {
const rollupMetadata = Reflect.getMetadata(Rollup_1.ROLLUP_METADATA_KEY, entity.target);
if (!rollupMetadata)
continue;
const { rollupConfig } = rollupMetadata;
const builder = core_1.TimescaleDB.createRollup(rollupConfig);
const inspectResults = await dataSource.query(builder.inspect().build());
if (!inspectResults[0].source_view_exists) {
debug(`Source view ${rollupConfig.rollupOptions.sourceView} does not exist for rollup ${entity.tableName}`);
continue;
}
if (inspectResults[0].rollup_view_exists) {
debug(`Rollup view ${entity.tableName} already exists, skipping creation`);
continue;
}
const candlestickMetadata = Reflect.getMetadata(CandlestickColumn_1.CANDLESTICK_COLUMN_METADATA_KEY, entity.target);
const candlestick = candlestickMetadata
? {
propertyName: String(candlestickMetadata.propertyKey),
timeColumn: candlestickMetadata.time_column,
priceColumn: candlestickMetadata.price_column,
volumeColumn: candlestickMetadata.volume_column,
sourceColumn: candlestickMetadata.source_column,
}
: undefined;
const rollupColumns = Reflect.getMetadata(RollupColumn_1.ROLLUP_COLUMN_METADATA_KEY, entity.target) || {};
const rollupRules = Object.entries(rollupColumns).map(([, value]) => ({
sourceColumn: value.source_column,
targetColumn: String(value.propertyKey),
aggregateType: value.type,
rollupFn: value.rollup_fn || 'rollup',
}));
try {
const inspectResults = await dataSource.query(builder.inspect().build());
if (!inspectResults[0].source_view_exists) {
debug(`Source view ${rollupConfig.rollupOptions.sourceView} does not exist for rollup ${entity.tableName}`);
continue;
}
if (inspectResults[0].rollup_view_exists) {
debug(`Rollup view ${entity.tableName} already exists, skipping creation`);
continue;
}
const sql = builder.up().build({
candlestick,
rollupRules,
});
await dataSource.query(sql);
const refreshPolicy = builder.up().getRefreshPolicy();
if (refreshPolicy) {
await dataSource.query(refreshPolicy);
}
}
catch (error) {
debug(`Failed to setup rollup for ${entity.tableName}:`, error);
throw error;
}
const refreshPolicy = builder.up().getRefreshPolicy();
if (refreshPolicy) {
await dataSource.query(refreshPolicy);
}
debug(`Rollup for ${entity.tableName} set up`);
}
}
async function removeRollups(dataSource) {
debug('Removing rollups');
for (const entity of eligibleEntityMetadatas(dataSource.entityMetadatas)) {
const rollupMetadata = Reflect.getMetadata(Rollup_1.ROLLUP_METADATA_KEY, entity.target);
if (!rollupMetadata)
continue;
const { rollupConfig } = rollupMetadata;
const builder = core_1.TimescaleDB.createRollup(rollupConfig);
const statements = builder.down().build();
for (const sql of statements) {
await dataSource.query(sql);
debug(`Rollup for ${entity.tableName} removed`);
}
}
debug('Rollups removed');
}
async function setupTimeColumns(dataSource) {
debug('Setting up time columns');
for (const entity of eligibleEntityMetadatas(dataSource.entityMetadatas)) {
const timeColumnMetadata = Reflect.getMetadata(TimeColumn_1.TIME_COLUMN_METADATA_KEY, entity.target);
if (timeColumnMetadata) {
const checkSql = (0, core_1.generateTimestamptzCheck)(entity.tableName, timeColumnMetadata.columnName);
await dataSource.query(checkSql);
}
}
debug('Time columns setup');
}
//# sourceMappingURL=migration.js.map