Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrations support #51

Merged
merged 80 commits into from
Apr 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c8115fd
Restructure fs util tests
kabirbaidhya Mar 9, 2020
9c22e1f
Add tests for glob function
kabirbaidhya Mar 9, 2020
111d719
Add function to get migration entries
kabirbaidhya Mar 9, 2020
12077a4
Add tests for the migrator service
kabirbaidhya Mar 9, 2020
a2dd709
Make things consitent everywhere
kabirbaidhya Mar 9, 2020
f2bb1ef
Add interface SqlMigrationEntry
kabirbaidhya Mar 9, 2020
523b727
Implement migrations resolution logic
kabirbaidhya Mar 9, 2020
c507de1
Implement KnexMigrationSource class
kabirbaidhya Mar 9, 2020
a49dd2a
Write tests for the migration resolution logic
kabirbaidhya Mar 9, 2020
07f41f5
Move empty array test one level up
kabirbaidhya Mar 9, 2020
5ea3af7
Test KnexMigrationSource.getMigrations()
kabirbaidhya Mar 9, 2020
2311aa9
Test KnexMigrationSource.getMigrationName
kabirbaidhya Mar 9, 2020
5ec99d7
Add more tests for KnexMigrationSource
kabirbaidhya Mar 9, 2020
3e9a8d0
Update tests
kabirbaidhya Mar 9, 2020
22c1967
Add more tests for the migrator
kabirbaidhya Mar 9, 2020
a51b06f
Temporary workaroun with cli options
kabirbaidhya Mar 17, 2020
fa9a049
Simplify isCLI dependency
kabirbaidhya Mar 17, 2020
7261b7d
Add migration config to SyncConfig
kabirbaidhya Mar 17, 2020
4d6f873
Add migration service functions
kabirbaidhya Mar 17, 2020
07b9638
Dedup connection mapping
kabirbaidhya Mar 17, 2020
bea9fed
Organize directories
kabirbaidhya Mar 21, 2020
41af53d
Add interfaces for migration
kabirbaidhya Mar 22, 2020
a08577a
Revamp KnexMigrationSource
kabirbaidhya Mar 22, 2020
4eb8f7d
Implement SqlMigrationContext
kabirbaidhya Mar 22, 2020
99ca065
Remove old KnexMigrationSource
kabirbaidhya Mar 22, 2020
7adf06c
Enhance debug logs
kabirbaidhya Mar 22, 2020
67580fa
Rewrite KnexMigrationSource tests
kabirbaidhya Mar 22, 2020
73d01bb
Update migrator service test
kabirbaidhya Mar 22, 2020
4415571
Add tests for SqlMigrationContext
kabirbaidhya Mar 22, 2020
4748d00
Just do it for now
kabirbaidhya Mar 22, 2020
18d54a0
Update imports
kabirbaidhya Mar 23, 2020
5ef7596
Move migration flags to commands
kabirbaidhya Apr 18, 2020
4a10e6c
Remove commands/index file
kabirbaidhya Apr 18, 2020
6ecfcd1
Simplify promise.runSequentially function
kabirbaidhya Apr 18, 2020
0bf07f6
Simplify executePromisses() return type
kabirbaidhya Apr 18, 2020
3231c29
Make migrate-list command work
kabirbaidhya Apr 18, 2020
797887c
Build UX for the migrate-list command
kabirbaidhya Apr 18, 2020
8bee234
Add chalk
kabirbaidhya Apr 18, 2020
d672e3f
Print colored info & errors
kabirbaidhya Apr 18, 2020
9407d52
Support parametric modifiers on util/io
kabirbaidhya Apr 18, 2020
cca64c8
Improve UX for migrate-list command
kabirbaidhya Apr 18, 2020
8910ba0
Clean up debug logs
kabirbaidhya Apr 18, 2020
207eae0
Print final error message
kabirbaidhya Apr 18, 2020
c35b08a
Print info in the migrate-list command
kabirbaidhya Apr 18, 2020
3fe284d
Make migration command work
kabirbaidhya Apr 18, 2020
f1ebfa2
Make migration rollback work
kabirbaidhya Apr 18, 2020
1f1ed86
Refactor migration code
kabirbaidhya Apr 18, 2020
9d2037a
Deduplicate redundancy on migrations
kabirbaidhya Apr 19, 2020
fbb51da
Simplify codebase
kabirbaidhya Apr 19, 2020
a11ef73
Add migration.sourceType config option
kabirbaidhya Apr 19, 2020
3074396
Ability to lazy bind connectionId
kabirbaidhya Apr 19, 2020
f200128
Move knex migration source resolution upstream
kabirbaidhya Apr 19, 2020
b0fb908
Use init.prepare()
kabirbaidhya Apr 19, 2020
c6a0114
Skip validation on prepare for CLI
kabirbaidhya Apr 19, 2020
a798f3d
Improve debug logging
kabirbaidhya Apr 19, 2020
ef8416f
Rename interface SyncConfig -> Configuration
kabirbaidhya Apr 19, 2020
ce220b1
Rename SyncParams -> SynchronizeParams
kabirbaidhya Apr 19, 2020
f5c887b
Fix tests
kabirbaidhya Apr 19, 2020
c27b31b
Run migrations on sync unless skipped
kabirbaidhya Apr 19, 2020
70d2931
Make handlers optional
kabirbaidhya Apr 21, 2020
798dd0c
Reorganize command classes
kabirbaidhya Apr 21, 2020
793aac2
Handle cases gracefully
kabirbaidhya Apr 21, 2020
e416983
Error handling and better UX
kabirbaidhya Apr 21, 2020
698129d
Remove unnecessary blank line
kabirbaidhya Apr 22, 2020
cea9f19
Remove --generate-connections flag
kabirbaidhya Apr 22, 2020
574f4c5
Rename teardown to prune
kabirbaidhya Apr 22, 2020
a578336
Update imports
kabirbaidhya Apr 22, 2020
b0ce82f
Extract general domain interfaces
kabirbaidhya Apr 25, 2020
d95217f
Simplify the contract
kabirbaidhya Apr 25, 2020
905ac4c
Add executeOperation function
kabirbaidhya Apr 25, 2020
cec5886
Extract a base interface
kabirbaidhya Apr 25, 2020
83c81d4
Use executeOperation function
kabirbaidhya Apr 25, 2020
77281ba
Add prune command
kabirbaidhya Apr 25, 2020
76f3f02
Refactor to simplify the api
kabirbaidhya Apr 25, 2020
7ca11a3
Move mapToConnectionReferences() to util/db
kabirbaidhya Apr 25, 2020
53edbd2
Add note for the programmatic api
kabirbaidhya Apr 25, 2020
3975f2b
Revert util/io change
kabirbaidhya Apr 25, 2020
d644b77
Organize interfaces
kabirbaidhya Apr 25, 2020
284ed37
Fix tests
kabirbaidhya Apr 25, 2020
10fd58e
Tests, aesthetics and improvements
kabirbaidhya Apr 25, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions bin/run
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,4 @@
// Set CLI environment as true.
process.env.SYNC_DB_CLI = 'true';

require('@oclif/command')
.run()
.then(require('@oclif/command/flush'))
.catch(require('@oclif/errors/handle'));
require('@oclif/command').run().then(require('@oclif/command/flush')).catch(require('@oclif/errors/handle'));
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/plugin-help": "^2",
"chalk": "^4.0.0",
"debug": "^4.1.1",
"globby": "^10.0.2",
"knex": "^0.20.11",
Expand Down
237 changes: 184 additions & 53 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,204 @@
import * as Knex from 'knex';
import { mergeDeepRight } from 'ramda';

import { log } from './logger';
import { getConnectionId } from './config';
import SyncParams from './domain/SyncParams';
import SyncConfig from './domain/SyncConfig';
import SyncResult from './domain/SyncResult';
import { DEFAULT_SYNC_PARAMS } from './constants';
import ConnectionConfig from './domain/ConnectionConfig';
import ConnectionReference from './domain/ConnectionReference';
import { isKnexInstance, getConfig, createInstance } from './util/db';

// Services
import { synchronizeDatabase } from './service/sync';
/**
* Programmatic API
* ----------------
* This module defines the Programmatic API of sync-db.
* The functions exposed here are used by the CLI frontend and
* are also meant to be the public interface for the developers using it as a package.
*/

import * as init from './init';
import { log } from './util/logger';
import { withTransaction, mapToConnectionReferences, DatabaseConnections } from './util/db';

import Configuration from './domain/Configuration';
import SynchronizeParams from './domain/SynchronizeParams';
import OperationParams from './domain/operation/OperationParams';
import OperationResult from './domain/operation/OperationResult';

// Service
import { executeProcesses } from './service/execution';
import { runSynchronize, runPrune } from './service/sync';
import { invokeMigrationApi, KnexMigrationAPI } from './migration/service/knexMigrator';

/**
* Synchronize all the configured database connections.
*
* @param {SyncConfig} config
* @param {(ConnectionConfig[] | ConnectionConfig | Knex[] | Knex)} conn
* @param {SyncParams} [options]
* @returns {Promise<SyncResult[]>}
* @param {Configuration} config
* @param {DatabaseConnections} conn
* @param {SynchronizeParams} [options]
* @returns {Promise<OperationResult[]>}
*/
export async function synchronize(
config: SyncConfig,
conn: ConnectionConfig[] | ConnectionConfig | Knex[] | Knex,
options?: SyncParams
): Promise<SyncResult[]> {
log('Starting to synchronize.');
const connectionList = Array.isArray(conn) ? conn : [conn];
const connections = mapToConnectionReferences(connectionList);
const params = mergeDeepRight(DEFAULT_SYNC_PARAMS, options || {});
const processes = connections.map(({ connection, id: connectionId }) => () =>
synchronizeDatabase(connection, {
config,
params,
connectionId
})
config: Configuration,
conn: DatabaseConnections,
options?: SynchronizeParams
): Promise<OperationResult[]> {
log('Synchronize');

const params: SynchronizeParams = {
force: false,
'skip-migration': false,
...options
};

// TODO: Need to preload the SQL source code under this step.
const { knexMigrationConfig } = await init.prepare(config, {
loadSqlSources: true,
loadMigrations: !params['skip-migration']
});

const connections = mapToConnectionReferences(conn);
const processes = connections.map(connection => () =>
withTransaction(connection, trx =>
runSynchronize(trx, {
config,
params,
connectionId: connection.id,
migrateFunc: t =>
invokeMigrationApi(t, KnexMigrationAPI.MIGRATE_LATEST, {
config,
connectionId: connection.id,
knexMigrationConfig: knexMigrationConfig(connection.id),
params: { ...params, onSuccess: params.onMigrationSuccess, onFailed: params.onMigrationFailed }
})
})
)
);

return executeProcesses(processes, config);
}

/**
* Prune all synchronized objects from the databases (except the ones like tables made via migrations).
*
* TODO: An ability to prune only a handful of objects from the last.
*
* @param {Configuration} config
* @param {(DatabaseConnections)} conn
* @param {OperationParams} [options]
* @returns {Promise<OperationResult[]>}
*/
export async function prune(
config: Configuration,
conn: DatabaseConnections,
options?: OperationParams
): Promise<OperationResult[]> {
log('Prune');

const params: OperationParams = { ...options };
const connections = mapToConnectionReferences(conn);

// TODO: Need to preload the SQL source code under this step.
await init.prepare(config, { loadSqlSources: true });

const processes = connections.map(connection => () =>
withTransaction(connection, trx =>
runPrune(trx, {
config,
params,
connectionId: connection.id
})
)
);

// Explicitly suppressing the `| Error` type since
// all errors are already caught inside synchronizeDatabase().
const results = (await executeProcesses(processes, config)) as SyncResult[];
return executeProcesses(processes, config);
}

/**
* Migrate Latest.
*
* @param {Configuration} config
* @param {(DatabaseConnections)} conn
* @param {OperationParams} [options]
* @returns {Promise<OperationResult[]>}
*/
export async function migrateLatest(
config: Configuration,
conn: DatabaseConnections,
options?: OperationParams
): Promise<OperationResult[]> {
log('Migrate Latest');

const params: OperationParams = { ...options };
const connections = mapToConnectionReferences(conn);
const { knexMigrationConfig } = await init.prepare(config, { loadMigrations: true });

log('Synchronization completed.');
const processes = connections.map(connection => () =>
withTransaction(connection, trx =>
invokeMigrationApi(trx, KnexMigrationAPI.MIGRATE_LATEST, {
config,
params,
connectionId: connection.id,
knexMigrationConfig: knexMigrationConfig(connection.id)
})
)
);

return results;
return executeProcesses(processes, config);
}

/**
* Map connection configuration list to the connection instances.
* Migrate Rollback.
*
* @param {((ConnectionConfig | Knex)[])} connectionList
* @returns {ConnectionReference[]}
* @param {Configuration} config
* @param {(DatabaseConnections)} conn
* @param {OperationParams} [options]
* @returns {Promise<OperationResult[]>}
*/
function mapToConnectionReferences(connectionList: (ConnectionConfig | Knex)[]): ConnectionReference[] {
return connectionList.map(connection => {
if (isKnexInstance(connection)) {
log(`Received connection instance to database: ${connection.client.config.connection.database}`);
export async function migrateRollback(
config: Configuration,
conn: DatabaseConnections,
options?: OperationParams
): Promise<OperationResult[]> {
log('Migrate Rollback');

// TODO: Ask for `id` explicitly in for programmatic API,
// when Knex instance is passed directly.
// This implies a breaking change with the programmatic API.
return { connection, id: getConnectionId(getConfig(connection)) };
}
const params: OperationParams = { ...options };
const connections = mapToConnectionReferences(conn);
const { knexMigrationConfig } = await init.prepare(config, { loadMigrations: true });

log(`Creating a connection to database: ${connection.host}/${connection.database}`);
const processes = connections.map(connection => () =>
withTransaction(connection, trx =>
invokeMigrationApi(trx, KnexMigrationAPI.MIGRATE_ROLLBACK, {
config,
params,
connectionId: connection.id,
knexMigrationConfig: knexMigrationConfig(connection.id)
})
)
);

return { connection: createInstance(connection), id: getConnectionId(connection) };
});
return executeProcesses(processes, config);
}

/**
* List Migrations.
*
* @param {Configuration} config
* @param {(DatabaseConnections)} conn
* @param {OperationParams} [options]
* @returns {Promise<OperationResult[]>}
*/
export async function migrateList(
config: Configuration,
conn: DatabaseConnections,
options?: OperationParams
): Promise<OperationResult[]> {
log('Migrate List');

const params: OperationParams = { ...options };
const connections = mapToConnectionReferences(conn);
const { knexMigrationConfig } = await init.prepare(config, { loadMigrations: true });

const processes = connections.map(connection => () =>
withTransaction(connection, trx =>
invokeMigrationApi(trx, KnexMigrationAPI.MIGRATE_LIST, {
config,
params,
connectionId: connection.id,
knexMigrationConfig: knexMigrationConfig(connection.id)
})
)
);

return executeProcesses(processes, config);
}
37 changes: 0 additions & 37 deletions src/cli.ts

This file was deleted.

73 changes: 73 additions & 0 deletions src/commands/migrate-latest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Command } from '@oclif/command';
import { bold, red, cyan } from 'chalk';

import { migrateLatest } from '../api';
import { dbLogger } from '../util/logger';
import { loadConfig, resolveConnections } from '..';
import { printLine, printError, printInfo } from '../util/io';
import OperationResult from '../domain/operation/OperationResult';

class MigrateLatest extends Command {
static description = 'Run the migrations up to the latest changes.';

/**
* Success handler.
*/
onSuccess = async (result: OperationResult) => {
const log = dbLogger(result.connectionId);
const [num, list] = result.data;
const alreadyUpToDate = num && list.length === 0;

log('Up to date: ', alreadyUpToDate);

await printLine(bold(` ▸ ${result.connectionId} - Successful`) + ` (${result.timeElapsed}s)`);

if (alreadyUpToDate) {
await printInfo(' Already up to date.\n');

return;
}

// Completed migrations.
for (const item of list) {
await printLine(cyan(` - ${item}`));
}

await printInfo(`\n Ran ${list.length} migrations.\n`);
};

/**
* Failure handler.
*/
onFailed = async (result: OperationResult) => {
printLine(bold(red(` ▸ ${result.connectionId} - Failed`)));

await printError(` ${result.error}\n`);
};

/**
* CLI command execution handler.
*
* @returns {Promise<void>}
*/
async run(): Promise<void> {
const config = await loadConfig();
const connections = await resolveConnections();

const results = await migrateLatest(config, connections, {
onSuccess: this.onSuccess,
onFailed: this.onFailed
});

const failedCount = results.filter(({ success }) => !success).length;

if (failedCount === 0) {
return process.exit(0);
}

printError(`Error: Migration failed for ${failedCount} connection(s).`);
process.exit(-1);
}
}

export default MigrateLatest;
Loading