diff --git a/packages/@best/api-db/README.md b/packages/@best/api-db/README.md index 6c75de1d..e1bd099f 100644 --- a/packages/@best/api-db/README.md +++ b/packages/@best/api-db/README.md @@ -2,41 +2,35 @@ This is the database adapter that the frontend uses to display results. The results are stored whenever a benchmark is run. -Below you can find instructions for using either Postgres or SQLite. +Below you can find instructions for using either Postgres or SQLite. By default Best uses a local SQLite file, however we recommend using Postgres if you are running on anything other than your local machine. -## Postgres +## SQLite +SQLite is configured by default, but if you would like to provide a custom path you can use the following configuration. ### Config Inside your `best.config.js` you need to have the following: ``` { apiDatabase: { - adapter: 'sql/postgres', - path: 'postgresql://dbuser:secretpassword@database.server.com:3211/mydb + adapter: 'sql/sqlite', + uri: 'PATH_TO_SQLITE_DB' } } ``` -For Postgres, you need to provision and manage your own database. - -### Migrations -In order to run the migrations required for the database you can run the following command: - -``` -yarn migrate:postgres up -``` +You do not need to create your own sqlite file, the adapter will handle that for you. -## SQLite +## Postgres ### Config Inside your `best.config.js` you need to have the following: ``` { apiDatabase: { - adapter: 'sql/sqlite', - path: 'PATH_TO_SQLITE_DB' + adapter: 'sql/postgres', + uri: 'postgresql://dbuser:secretpassword@database.server.com:3211/mydb } } ``` -You do not need to create your own sqlite file, the adapter will handle that for you. \ No newline at end of file +For Postgres, you need to provision and manage your own database. \ No newline at end of file diff --git a/packages/@best/api-db/package.json b/packages/@best/api-db/package.json index 9e8d532f..23897384 100644 --- a/packages/@best/api-db/package.json +++ b/packages/@best/api-db/package.json @@ -2,15 +2,11 @@ "name": "@best/api-db", "version": "4.0.0", "dependencies": { - "node-pg-migrate": "^3.21.1", "pg": "^7.11.0", "sqlite": "^3.0.3" }, "devDependencies": { "@types/pg": "^7.4.14" }, - "main": "build/index.js", - "scripts": { - "migrate:postgres": "node-pg-migrate -m src/sql/postgres/migrations" - } + "main": "build/index.js" } diff --git a/packages/@best/api-db/src/sql/adapter.ts b/packages/@best/api-db/src/sql/adapter.ts index 4c1dd060..61a3bc36 100644 --- a/packages/@best/api-db/src/sql/adapter.ts +++ b/packages/@best/api-db/src/sql/adapter.ts @@ -23,42 +23,36 @@ export class SQLAdapter extends ApiDBAdapter { } async saveSnapshots(snapshots: TemporarySnapshot[], projectName: string): Promise { - try { - let projectResult = await this.db.fetchProject(projectName) + let projectResult = await this.db.fetchProject(projectName) - if (projectResult.rows.length < 1) { - await this.db.createProject(projectName) - projectResult = await this.db.fetchProject(projectName) - } + if (projectResult.rows.length < 1) { + await this.db.createProject(projectName, true) + projectResult = await this.db.fetchProject(projectName) + } - const projectId = projectResult.rows[0].id + const projectId = projectResult.rows[0].id - await Promise.all(snapshots.map(async (snapshot) => { - return this.db.createOrUpdateSnapshot(snapshot, projectId) - })) - } catch (err) { - console.error('[API-DB] Could not save results into database.') - return false - } + await Promise.all(snapshots.map(async (snapshot) => { + return this.db.createOrUpdateSnapshot(snapshot, projectId) + })) return true } async updateLastRelease(projectName: string, release: string | Date): Promise { - try { - const projectResult = await this.db.fetchProject(projectName) + const projectResult = await this.db.fetchProject(projectName) - if (projectResult.rows.length > 0) { - const projectId = projectResult.rows[0].id - await this.db.updateProjectLastRelease(projectId, release) - } else { - throw new Error(`Project with name: '${projectName}' does not exist.`) - } - } catch (err) { - console.log('[API-DB] Could not update latest result') - return false + if (projectResult.rows.length > 0) { + const projectId = projectResult.rows[0].id + await this.db.updateProjectLastRelease(projectId, release) + } else { + throw new Error(`Project with name: '${projectName}' does not exist.`) } return true } + + migrate() { + return this.db.performMigrations() + } } diff --git a/packages/@best/api-db/src/sql/db.ts b/packages/@best/api-db/src/sql/db.ts index f2fa91fc..f8d3c36f 100644 --- a/packages/@best/api-db/src/sql/db.ts +++ b/packages/@best/api-db/src/sql/db.ts @@ -34,8 +34,16 @@ export abstract class SQLDatabase { return this.query('SELECT * FROM projects WHERE "name" = $1 LIMIT 1', [name]) } - createProject(name: string): Promise { - return this.query('INSERT INTO projects("name") VALUES ($1)', [name]) + async createProject(name: string, swallowNonUniqueErrors: boolean = false): Promise { + try { + return await this.query('INSERT INTO projects("name") VALUES ($1)', [name]); + } catch (err) { + if (swallowNonUniqueErrors && (err.constraint === 'projects_unique_name' || err.code === 'SQLITE_CONSTRAINT')) { + return this.fetchProject(name); + } + + throw err; + } } updateProjectLastRelease(id: number, release: string | Date): Promise { @@ -49,16 +57,19 @@ export abstract class SQLDatabase { async createOrUpdateSnapshot(snapshot: TemporarySnapshot, projectId: number): Promise { try { - return await this.createSnapshot(snapshot, projectId) + return await this.createSnapshot(snapshot, projectId); } catch (err) { - if (err.constraint === 'best_snapshot_unqiue_index') { + if (err.constraint === 'best_snapshot_unqiue_index' || err.code === 'SQLITE_CONSTRAINT') { const updatedAt = new Date() const values = [normalizeMetrics(snapshot.metrics), snapshot.environmentHash, snapshot.similarityHash, updatedAt, projectId, snapshot.commit, snapshot.name] return this.query('UPDATE snapshots SET "metrics" = $1, "environment_hash" = $2, "similarity_hash" = $3, "updated_at" = $4 WHERE "project_id" = $5 AND "commit" = $6 AND "name" = $7', values) - } else { - console.log(err) - throw err; } + + throw err; } } + + async performMigrations() { + throw new Error('Migrations are not implemented.') + } } \ No newline at end of file diff --git a/packages/@best/api-db/src/sql/postgres/db.ts b/packages/@best/api-db/src/sql/postgres/db.ts index de65fb1a..587bdef0 100644 --- a/packages/@best/api-db/src/sql/postgres/db.ts +++ b/packages/@best/api-db/src/sql/postgres/db.ts @@ -1,17 +1,27 @@ import { Pool } from 'pg' import { SQLDatabase, SQLQueryResult } from '../db' import { ApiDatabaseConfig } from '@best/types'; +import { migrate } from './migrate'; export default class PostgresDatabase extends SQLDatabase { pool: Pool + migrated = false; + constructor(config: ApiDatabaseConfig) { super() this.pool = new Pool({ - connectionString: config.path + connectionString: config.uri }) } query(text: string, params: any[]): Promise { + if (! this.migrated) { throw new Error('Database migrations have not been ensured.') } + return this.pool.query(text, params) } + + async performMigrations() { + await migrate(this.pool) + this.migrated = true; + } } diff --git a/packages/@best/api-db/src/sql/postgres/migrate.ts b/packages/@best/api-db/src/sql/postgres/migrate.ts new file mode 100644 index 00000000..c23c1972 --- /dev/null +++ b/packages/@best/api-db/src/sql/postgres/migrate.ts @@ -0,0 +1,93 @@ +import path from 'path' +import fs from 'fs'; +import { promisify } from 'util'; +import { Pool } from 'pg'; + +const asyncReadDir = promisify(fs.readdir); + +interface MigrationContent { + up: string; + down: string; +} + +interface PartialMigration { + id: number; + name: string; + filename: string; +} + +type Migration = PartialMigration & MigrationContent + +const buildMigrations = async (location: string): Promise => { + const migrationsPath = path.resolve(__dirname, location); + + const files = await asyncReadDir(migrationsPath); + + // we look for .js files since these will be pre-compiled by js + const partialMigrations: PartialMigration[] = files.map(f => f.match(/^((\d+).(.*?))\.js$/)).reduce((migrations, match): PartialMigration[] => { + if (! match) { + return migrations; + } + + const migration = { id: Number(match[2]), name: match[3], filename: match[1] } + + return [...migrations, migration]; + }, []).sort((a, b) => Math.sign(a.id - b.id)); + + const migrations: Migration[] = await Promise.all(partialMigrations.map(async (partial): Promise => { + const filename = path.resolve(migrationsPath, partial.filename); + const content: MigrationContent = await import(filename); + + return { + ...partial, + ...content + } + })) + + return migrations +} + +export const migrate = async (db: Pool, redoLast: boolean = false, location: string = 'migrations/', table = 'migrations'): Promise => { + const migrations = await buildMigrations(location); + + const client = await db.connect(); + + await client.query(`CREATE TABLE IF NOT EXISTS "${table}" (id INTEGER PRIMARY KEY, name TEXT NOT NULL, up TEXT NOT NULL, down TEXT NOT NULL)`); + + const previous = await client.query(`SELECT * FROM ${table} ORDER BY id ASC`); + let lastMigration = previous.rows[previous.rows.length - 1]; + + if (redoLast && previous.rows.length > 0) { + await client.query('BEGIN'); + try { + await client.query(lastMigration.down); + await client.query(`DELETE FROM "${table}" WHERE id = ?`, lastMigration.id); + await client.query('COMMIT'); + lastMigration = null; + } catch (err) { + await client.query('ROLLBACK'); + client.release(); + throw err; + } + } + + const lastMigrationId = lastMigration ? lastMigration.id : 0; + await Promise.all(migrations.map(async migration => { + if (migration.id > lastMigrationId) { + await client.query('BEGIN'); + try { + await client.query(migration.up); + await client.query(`INSERT INTO "${table}" (id, name, up, down) VALUES ($1, $2, $3, $4)`, [migration.id, migration.name, migration.up, migration.down]); + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + client.release(); + throw err; + } + } + })) + + client.release(); + + return true; +} \ No newline at end of file diff --git a/packages/@best/api-db/src/sql/postgres/migrations/001-projects.ts b/packages/@best/api-db/src/sql/postgres/migrations/001-projects.ts new file mode 100644 index 00000000..5626215d --- /dev/null +++ b/packages/@best/api-db/src/sql/postgres/migrations/001-projects.ts @@ -0,0 +1,12 @@ +export const up = ` +CREATE TABLE projects ( + id SERIAL PRIMARY KEY, + name character varying(100) NOT NULL, + created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_release_date timestamp without time zone +); + +CREATE UNIQUE INDEX projects_unique_name ON projects(name text_ops); +` + +export const down = `DROP TABLE projects;` \ No newline at end of file diff --git a/packages/@best/api-db/src/sql/postgres/migrations/002-snapshots.ts b/packages/@best/api-db/src/sql/postgres/migrations/002-snapshots.ts new file mode 100644 index 00000000..51f96efd --- /dev/null +++ b/packages/@best/api-db/src/sql/postgres/migrations/002-snapshots.ts @@ -0,0 +1,20 @@ +export const up = ` +CREATE TABLE snapshots ( + id SERIAL PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + name character varying(200) NOT NULL, + metrics character varying(2000) NOT NULL, + environment_hash character varying(100) NOT NULL, + similarity_hash character varying(100) NOT NULL, + commit character varying(100) NOT NULL, + commit_date timestamp without time zone NOT NULL, + created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + temporary boolean NOT NULL, + updated_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX snapshots_project_id_index ON snapshots(project_id int4_ops); +CREATE UNIQUE INDEX best_snapshot_unqiue_index ON snapshots(project_id int4_ops,commit text_ops,name text_ops) WHERE temporary = false; +` + +export const down = `DROP TABLE snapshots;` \ No newline at end of file diff --git a/packages/@best/api-db/src/sql/postgres/migrations/1559599488979_setup-projects.js b/packages/@best/api-db/src/sql/postgres/migrations/1559599488979_setup-projects.js deleted file mode 100644 index e803ca5b..00000000 --- a/packages/@best/api-db/src/sql/postgres/migrations/1559599488979_setup-projects.js +++ /dev/null @@ -1,17 +0,0 @@ -exports.shorthands = undefined; - -exports.up = pgm => { - pgm.createTable('projects', { - id: 'id', - name: { type: 'varchar(100)', notNull: true }, - created_at: { - type: 'timestamptz', - notNull: true, - default: pgm.func('current_timestamp'), - }, - }); -}; - -exports.down = pgm => { - pgm.dropTable('projects'); -}; diff --git a/packages/@best/api-db/src/sql/postgres/migrations/1559600258932_benchmark-snapshot.js b/packages/@best/api-db/src/sql/postgres/migrations/1559600258932_benchmark-snapshot.js deleted file mode 100644 index efbf6fa4..00000000 --- a/packages/@best/api-db/src/sql/postgres/migrations/1559600258932_benchmark-snapshot.js +++ /dev/null @@ -1,34 +0,0 @@ -exports.shorthands = undefined; - -exports.up = pgm => { - pgm.createTable('snapshots', { - id: 'id', - project_id: { - type: 'integer', - notNull: true, - references: '"projects"', - onDelete: 'cascade', - }, - name: { type: 'varchar(200)', notNull: true }, - metrics: { type: 'varchar(2000)', notNull: true }, - environment_hash: { type: 'varchar(100)', notNull: true }, - similarity_hash: { type: 'varchar(100)', notNull: true }, - commit: { type: 'varchar(100)', notNull: true }, - commit_date: { - type: 'timestamptz', - notNull: true, - }, - created_at: { - type: 'timestamptz', - notNull: true, - default: pgm.func('current_timestamp'), - }, - temporary: { type: 'boolean', notNull: true }, - }); - - pgm.createIndex('snapshots', 'project_id'); -}; - -exports.down = pgm => { - pgm.dropTable('snapshots'); -}; diff --git a/packages/@best/api-db/src/sql/postgres/migrations/1559603549891_add-updated-at.js b/packages/@best/api-db/src/sql/postgres/migrations/1559603549891_add-updated-at.js deleted file mode 100644 index 22a4ab22..00000000 --- a/packages/@best/api-db/src/sql/postgres/migrations/1559603549891_add-updated-at.js +++ /dev/null @@ -1,11 +0,0 @@ -exports.shorthands = undefined; - -exports.up = pgm => { - pgm.addColumns('snapshots', { - updated_at: { - type: 'timestamptz', - notNull: true, - default: pgm.func('current_timestamp'), - }, - }); -}; diff --git a/packages/@best/api-db/src/sql/postgres/migrations/1559770417215_project-last-release.js b/packages/@best/api-db/src/sql/postgres/migrations/1559770417215_project-last-release.js deleted file mode 100644 index 01cc9aa9..00000000 --- a/packages/@best/api-db/src/sql/postgres/migrations/1559770417215_project-last-release.js +++ /dev/null @@ -1,9 +0,0 @@ -exports.shorthands = undefined; - -exports.up = pgm => { - pgm.addColumns('projects', { - last_release_date: { - type: 'timestamptz', - }, - }); -}; diff --git a/packages/@best/api-db/src/sql/postgres/migrations/1560444988970_unique-snapshots.js b/packages/@best/api-db/src/sql/postgres/migrations/1560444988970_unique-snapshots.js deleted file mode 100644 index a380db5d..00000000 --- a/packages/@best/api-db/src/sql/postgres/migrations/1560444988970_unique-snapshots.js +++ /dev/null @@ -1,5 +0,0 @@ -exports.shorthands = undefined; - -exports.up = (pgm) => { - pgm.createIndex('snapshots', ['project_id', 'commit', 'name'], { unique: true, where: `temporary = 'f'`, name: 'best_snapshot_unqiue_index' }) -}; \ No newline at end of file diff --git a/packages/@best/api-db/src/sql/sqlite/db.ts b/packages/@best/api-db/src/sql/sqlite/db.ts index 65ca8ef5..20874a25 100644 --- a/packages/@best/api-db/src/sql/sqlite/db.ts +++ b/packages/@best/api-db/src/sql/sqlite/db.ts @@ -1,15 +1,20 @@ -import sqlite from 'sqlite' +import sqlite, { Database } from 'sqlite' import { SQLDatabase, SQLQueryResult } from '../db' +import { migrate } from './migrate'; import { ApiDatabaseConfig } from '@best/types'; export default class SQLiteDatabase extends SQLDatabase { - dbPromise: any + dbPromise: Promise + migrated = false; + constructor(config: ApiDatabaseConfig) { super() - this.dbPromise = sqlite.open(config.path, { verbose: true }) + this.dbPromise = sqlite.open(config.uri, { verbose: true }) } async query(text: string, params: any[]): Promise { + if (! this.migrated) { throw new Error('Database migrations have not been ensured.') } + const database = await this.dbPromise const query = this.transformQuery(text) @@ -21,4 +26,10 @@ export default class SQLiteDatabase extends SQLDatabase { transformQuery(original: string): string { return original.replace(/(\$[\d]+)/g, '?') } + + async performMigrations() { + const database = await this.dbPromise + await migrate(database) + this.migrated = true; + } } diff --git a/packages/@best/api-db/src/sql/sqlite/migrate.ts b/packages/@best/api-db/src/sql/sqlite/migrate.ts new file mode 100644 index 00000000..2ffba51c --- /dev/null +++ b/packages/@best/api-db/src/sql/sqlite/migrate.ts @@ -0,0 +1,88 @@ +import path from 'path' +import fs from 'fs'; +import { promisify } from 'util'; +import { Database } from 'sqlite'; + +const asyncReadDir = promisify(fs.readdir); + +interface MigrationContent { + up: string; + down: string; +} + +interface PartialMigration { + id: number; + name: string; + filename: string; +} + +type Migration = PartialMigration & MigrationContent + +const buildMigrations = async (location: string): Promise => { + const migrationsPath = path.resolve(__dirname, location); + + const files = await asyncReadDir(migrationsPath); + + // we look for .js files since these will be pre-compiled by js + const partialMigrations: PartialMigration[] = files.map(f => f.match(/^((\d+).(.*?))\.js$/)).reduce((migrations, match): PartialMigration[] => { + if (! match) { + return migrations; + } + + const migration = { id: Number(match[2]), name: match[3], filename: match[1] } + + return [...migrations, migration]; + }, []).sort((a, b) => Math.sign(a.id - b.id)); + + const migrations: Migration[] = await Promise.all(partialMigrations.map(async (partial): Promise => { + const filename = path.resolve(migrationsPath, partial.filename); + const content: MigrationContent = await import(filename); + + return { + ...partial, + ...content + } + })) + + return migrations +} + +// modified from https://github.com/kriasoft/node-sqlite/blob/master/src/Database.js#L148 +export const migrate = async (db: Database, redoLast: boolean = false, location: string = 'migrations/', table = 'migrations'): Promise => { + const migrations = await buildMigrations(location); + + await db.run(`CREATE TABLE IF NOT EXISTS "${table}" (id INTEGER PRIMARY KEY, name TEXT NOT NULL, up TEXT NOT NULL, down TEXT NOT NULL)`); + + const previous = await db.all(`SELECT * FROM ${table} ORDER BY id ASC`); + let lastMigration = previous[previous.length - 1]; + + if (redoLast && previous.length > 0) { + await db.run('BEGIN'); + try { + await db.exec(lastMigration.down); + await db.run(`DELETE FROM "${table}" WHERE id = ?`, lastMigration.id); + await db.run('COMMIT'); + lastMigration = null; + } catch (err) { + await db.run('ROLLBACK'); + throw err; + } + } + + const lastMigrationId = lastMigration ? lastMigration.id : 0; + await Promise.all(migrations.map(async migration => { + if (migration.id > lastMigrationId) { + await db.run('BEGIN'); + try { + await db.exec(migration.up); + await db.run(`INSERT INTO "${table}" (id, name, up, down) VALUES (?, ?, ?, ?)`, migration.id, migration.name, migration.up, migration.down); + await db.run('COMMIT'); + } catch (err) { + await db.run('ROLLBACK'); + throw err; + } + } + })) + + return true; +} \ No newline at end of file diff --git a/packages/@best/api-db/src/sql/sqlite/migrations/001-initial.ts b/packages/@best/api-db/src/sql/sqlite/migrations/001-initial.ts new file mode 100644 index 00000000..cc84d31a --- /dev/null +++ b/packages/@best/api-db/src/sql/sqlite/migrations/001-initial.ts @@ -0,0 +1,34 @@ +export const up = ` +CREATE TABLE projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name character varying(100) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_release_date timestamp +); + +CREATE UNIQUE INDEX projects_pkey ON projects(id); +CREATE UNIQUE INDEX projects_unique_name ON projects(name); + +CREATE TABLE snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id integer NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + name character varying(200) NOT NULL, + metrics character varying(2000) NOT NULL, + environment_hash character varying(100) NOT NULL, + similarity_hash character varying(100) NOT NULL, + "commit" character varying(100) NOT NULL, + commit_date timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + temporary boolean NOT NULL, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX snapshots_pkey ON snapshots(id); +CREATE INDEX snapshots_project_id_index ON snapshots(project_id); +CREATE UNIQUE INDEX best_snapshot_unqiue_index ON snapshots(project_id,"commit",name) WHERE temporary = false; +` + +export const down = ` +DROP TABLE projects; +DROP TABLE snapshots; +` \ No newline at end of file diff --git a/packages/@best/api-db/src/store.ts b/packages/@best/api-db/src/store.ts index 933d3d82..b0525954 100644 --- a/packages/@best/api-db/src/store.ts +++ b/packages/@best/api-db/src/store.ts @@ -53,10 +53,10 @@ const generateSnapshots = (runSettings: RunSettings, benchmarks: StatsNode[], gr }, []) } -export const saveBenchmarkSummaryInDB = (benchmarkResults: BenchmarkResultsSnapshot[], globalConfig: FrozenGlobalConfig) => { +export const saveBenchmarkSummaryInDB = async (benchmarkResults: BenchmarkResultsSnapshot[], globalConfig: FrozenGlobalConfig) => { const db = loadDbFromConfig(globalConfig); - if (! db) { return; } + await db.migrate() return Promise.all( benchmarkResults.map(async (benchmarkResult) => { diff --git a/packages/@best/api-db/src/types.ts b/packages/@best/api-db/src/types.ts index 4c06964b..6d8c8f05 100644 --- a/packages/@best/api-db/src/types.ts +++ b/packages/@best/api-db/src/types.ts @@ -48,4 +48,8 @@ export class ApiDBAdapter { updateLastRelease(projectName: string, release: string | Date): Promise { throw new Error('ApiDB.updateLastRelease() not implemented') } + + migrate() { + throw new Error('ApiDB.migrate() not implemented') + } } diff --git a/packages/@best/api-db/src/utils.ts b/packages/@best/api-db/src/utils.ts index f125c504..731e01a2 100644 --- a/packages/@best/api-db/src/utils.ts +++ b/packages/@best/api-db/src/utils.ts @@ -10,9 +10,8 @@ function req(id: string) { return r.default || r; } -export const loadDbFromConfig = (globalConfig: FrozenGlobalConfig | FrontendConfig): ApiDBAdapter | undefined => { +export const loadDbFromConfig = (globalConfig: FrozenGlobalConfig | FrontendConfig): ApiDBAdapter => { const config = globalConfig.apiDatabase; - if (! config) { return; } if (LOCAL_ADAPTERS.includes(config.adapter)) { const localAdapter: typeof ApiDBAdapter = req(path.resolve(__dirname, config.adapter)); diff --git a/packages/@best/cli/src/cli/args.ts b/packages/@best/cli/src/cli/args.ts index 6b2c73ca..2fc106b6 100644 --- a/packages/@best/cli/src/cli/args.ts +++ b/packages/@best/cli/src/cli/args.ts @@ -78,6 +78,16 @@ export const options: { [key: string]: Options } = { description: 'Generate a static HTML version of the results of the benchmrak or comparison', type: 'boolean', }, + dbAdapter: { + default: undefined, + description: 'Override which database adapter is used. By default Best comes with `sql/sqlite` and `sql/postgres`', + type: 'string', + }, + dbURI: { + default: undefined, + description: 'Provide a connection URI or path to be passed to the database adapter', + type: 'string', + }, runner: { default: 'default', description: @@ -99,7 +109,7 @@ export const options: { [key: string]: Options } = { }; export function normalize(args: { [x: string]: any; _: string[]; $0: string }): CliConfig { - const { _, help, clearCache, clearResults, showConfigs, disableInteractive, gitIntegration, generateHTML, useHttp, externalStorage, runner, runnerConfig, config, projects, iterations, compareStats } = args; + const { _, help, clearCache, clearResults, showConfigs, disableInteractive, gitIntegration, generateHTML, useHttp, externalStorage, runner, runnerConfig, config, projects, iterations, compareStats, dbAdapter, dbURI } = args; return { _, help: Boolean(help), @@ -116,6 +126,8 @@ export function normalize(args: { [x: string]: any; _: string[]; $0: string }): config, projects: projects || [], iterations: iterations ? parseInt(iterations, 10): undefined, - compareStats + compareStats, + dbAdapter, + dbURI }; } diff --git a/packages/@best/config/src/utils/defaults.ts b/packages/@best/config/src/utils/defaults.ts index 171bea24..34070f73 100644 --- a/packages/@best/config/src/utils/defaults.ts +++ b/packages/@best/config/src/utils/defaults.ts @@ -5,6 +5,10 @@ const defaultOptions = { gitIntegration: false, commentThreshold: 5, generateHTML: false, + apiDatabase: { + adapter: 'sql/sqlite', + uri: '/__benchmarks_results__/best.sqlite' + }, cacheDirectory: cacheDirectory(), useHttp: false, openPages: false, diff --git a/packages/@best/config/src/utils/normalize.ts b/packages/@best/config/src/utils/normalize.ts index 3c2681c0..48ea425c 100644 --- a/packages/@best/config/src/utils/normalize.ts +++ b/packages/@best/config/src/utils/normalize.ts @@ -76,13 +76,20 @@ function setCliOptionOverrides(initialOptions: UserConfig, argsCLI: CliConfig): case 'generateHTML': options.generateHTML = Boolean(argsCLI[key]); break; + case 'dbAdapter': + if (argsCLI[key] !== undefined) { + options.apiDatabase ={ adapter: argsCLI[key], uri: argsCLI['dbURI'] } + } + break; + case 'dbURI': + break default: options[key] = argsCLI[key]; break; } return options; }, {}); - + return { ...initialOptions, ...argvToOptions }; } function normalizeObjectPathPatterns(options: { [key: string]: any }, rootDir: string) { @@ -151,24 +158,28 @@ export function normalizeConfig(userConfig: UserConfig, cliOptions: CliConfig): const userCliMergedConfig = normalizeRootDir(setCliOptionOverrides(userConfig, cliOptions)); const normalizedConfig: NormalizedConfig = { ...DEFAULT_CONFIG, ...userCliMergedConfig }; - // Normalize anything thats coming from the user - Object.keys(userCliMergedConfig).reduce((mergeConfig: NormalizedConfig, key: string) => { + Object.keys(normalizedConfig).reduce((mergeConfig: NormalizedConfig, key: string) => { switch (key) { case 'projects': - mergeConfig[key] = normalizeModulePathPatterns(userCliMergedConfig, key); + mergeConfig[key] = normalizeModulePathPatterns(normalizedConfig, key); break; case 'plugins': - mergeConfig[key] = normalizePlugins(userCliMergedConfig[key], userCliMergedConfig); + mergeConfig[key] = normalizePlugins(normalizedConfig[key], normalizedConfig); break; case 'runner': - mergeConfig[key] = normalizeRunner(userCliMergedConfig[key], mergeConfig.runners); + mergeConfig[key] = normalizeRunner(normalizedConfig[key], mergeConfig.runners); break; case 'runnerConfig': - mergeConfig[key] = normalizeRunnerConfig(userCliMergedConfig['runner'], mergeConfig.runners); + mergeConfig[key] = normalizeRunnerConfig(normalizedConfig['runner'], mergeConfig.runners); break; case 'compareStats': - mergeConfig[key] = normalizeCommits(userCliMergedConfig[key]); + mergeConfig[key] = normalizeCommits(normalizedConfig[key]); break; + case 'apiDatabase': { + const apiDatabaseConfig = normalizedConfig[key]; + mergeConfig[key] = apiDatabaseConfig ? normalizeObjectPathPatterns(apiDatabaseConfig, normalizedConfig.rootDir) : apiDatabaseConfig; + break; + } default: break; } diff --git a/packages/@best/frontend/server/api.ts b/packages/@best/frontend/server/api.ts index 392b8ed3..87d09ab1 100644 --- a/packages/@best/frontend/server/api.ts +++ b/packages/@best/frontend/server/api.ts @@ -7,8 +7,6 @@ export default (config: FrontendConfig): Router => { const db = loadDbFromConfig(config); const router = Router() - if (! db) { return router; } - router.get('/info/:commit', async (req, res): Promise => { const { commit } = req.params; @@ -51,7 +49,10 @@ export default (config: FrontendConfig): Router => { router.get('/projects', async (req, res): Promise => { try { + await db.migrate() + const projects = await db.fetchProjects() + res.send({ projects }) @@ -65,10 +66,13 @@ export default (config: FrontendConfig): Router => { const { since } = req.query try { + await db.migrate() + let parsedSince: Date | undefined; if (since && since.length > 0) { parsedSince = new Date(parseInt(since, 10)) } + const snapshots = await db.fetchSnapshots(project, parsedSince) res.send({ diff --git a/packages/@best/frontend/server/best-fe.config.ts b/packages/@best/frontend/server/best-fe.config.ts index bdc3a6a6..36467870 100644 --- a/packages/@best/frontend/server/best-fe.config.ts +++ b/packages/@best/frontend/server/best-fe.config.ts @@ -1,7 +1,7 @@ export default { apiDatabase: { adapter: 'sql/postgres', - path: `postgresql://localhost` + uri: `postgresql://localhost` }, githubConfig: { owner: 'salesforce', diff --git a/packages/@best/frontend/server/static/builder.ts b/packages/@best/frontend/server/static/builder.ts index 053e5e2f..bdbe9514 100644 --- a/packages/@best/frontend/server/static/builder.ts +++ b/packages/@best/frontend/server/static/builder.ts @@ -38,8 +38,6 @@ export const buildMockedDataFromApi = async (options: MockerOptions): Promise<{ } | null> => { const db = loadDbFromConfig(options.config); - if (! db) { return null } - const allProjects = await db.fetchProjects(); const projects = allProjects.filter((proj): boolean => options.projectNames.includes(proj.name)); diff --git a/packages/@best/github-integration/src/index.ts b/packages/@best/github-integration/src/index.ts index d4d7918e..ee123b27 100644 --- a/packages/@best/github-integration/src/index.ts +++ b/packages/@best/github-integration/src/index.ts @@ -54,8 +54,6 @@ export async function updateLatestRelease(projectNames: string[], globalConfig: const db = loadDbFromConfig(globalConfig); - if (! db) { return false; } - const app = GithubApplicationFactory(); const gitHubInstallation = await app.authenticateAsAppAndInstallation({ repo, owner }); diff --git a/packages/@best/types/src/config.ts b/packages/@best/types/src/config.ts index 91788458..ff7f394b 100644 --- a/packages/@best/types/src/config.ts +++ b/packages/@best/types/src/config.ts @@ -23,7 +23,7 @@ export interface RunnerConfig { export interface ApiDatabaseConfig { adapter: string; - path: string; + uri: string; } export interface FrontendConfig { @@ -49,7 +49,9 @@ export interface CliConfig { projects: string[], iterations?: number, compareStats: string[] | undefined, - generateHTML: boolean | undefined + generateHTML: boolean | undefined, + dbAdapter: string | undefined, + dbURI: string | undefined } export interface NormalizedConfig { @@ -61,7 +63,7 @@ export interface NormalizedConfig { generateHTML: boolean, useHttp: boolean, externalStorage?: string, - apiDatabase?: ApiDatabaseConfig, + apiDatabase: ApiDatabaseConfig, commentThreshold: number, isInteractive?: boolean, openPages: boolean, @@ -97,7 +99,7 @@ export interface GlobalConfig { nonFlagArgs: string[]; isInteractive?: boolean; gitInfo: GitConfig; - apiDatabase?: ApiDatabaseConfig; + apiDatabase: ApiDatabaseConfig; commentThreshold: number; externalStorage?: string; } diff --git a/yarn.lock b/yarn.lock index fa5c11eb..86556480 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2184,7 +2184,7 @@ dependencies: moment ">=2.14.0" -"@types/pg@^7.4.0", "@types/pg@^7.4.14": +"@types/pg@^7.4.14": version "7.4.14" resolved "https://registry.yarnpkg.com/@types/pg/-/pg-7.4.14.tgz#8235910120e81ca671e0e62b7de77d048b9a22b2" integrity sha512-2e4XapP9V/X42IGByC5IHzCzHqLLCNJid8iZBbkk6lkaDMvli8Rk62YE9wjGcLD5Qr5Zaw1ShkQyXy91PI8C0Q== @@ -3778,15 +3778,6 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -4034,13 +4025,6 @@ config-chain@^1.1.11: ini "^1.3.4" proto-list "~1.2.1" -config@>=1.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/config/-/config-3.1.0.tgz#c7d49c4171e8f6cb61c1d69e22647ffd60492548" - integrity sha512-t6oDeNQbsIWa+D/KF4959TANzjSHLv1BA/hvL8tHEA3OUSWgBXELKaONSI6nr9oanbKs0DXonjOWLcrtZ3yTAA== - dependencies: - json5 "^1.0.1" - configstore@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" @@ -4973,11 +4957,6 @@ dot-prop@^4.1.0, dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" -dotenv@>=1.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440" - integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg== - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -6001,11 +5980,6 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - get-pkg-repo@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" @@ -8256,7 +8230,7 @@ lodash.zip@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA= -lodash@4.17.11, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.0: +lodash@4.17.11, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -8961,19 +8935,6 @@ node-notifier@^5.2.1: shellwords "^0.1.1" which "^1.3.0" -node-pg-migrate@^3.21.1: - version "3.21.1" - resolved "https://registry.yarnpkg.com/node-pg-migrate/-/node-pg-migrate-3.21.1.tgz#5b7b1661328106a28a53a5605a2897d4b83a5703" - integrity sha512-gTd8NquEcJjhkT96iwnch6DyAdZ9bhi+avdAvV4kPw/ya7T4OijUoSNVJzCodCG2kIjv+fjssnZHC/LzEQdewg== - dependencies: - "@types/pg" "^7.4.0" - lodash "~4.17.0" - mkdirp "~0.5.0" - yargs "~13.2.0" - optionalDependencies: - config ">=1.0.0" - dotenv ">=1.0.0" - node-pre-gyp@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" @@ -9330,7 +9291,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -11850,7 +11811,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0, string-width@^3.1.0: +string-width@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== @@ -12996,15 +12957,6 @@ wrap-ansi@^4.0.0: string-width "^2.1.1" strip-ansi "^4.0.0" -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -13161,14 +13113,6 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.1.0: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" @@ -13212,23 +13156,6 @@ yargs@~11.0.0: y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@~13.2.0: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"