diff --git a/server/account/src/collections/mongo.ts b/server/account/src/collections/mongo.ts index 1da601b0ec..1b4d53261f 100644 --- a/server/account/src/collections/mongo.ts +++ b/server/account/src/collections/mongo.ts @@ -48,10 +48,6 @@ export class MongoDbCollection> implements DbColle return this.db.collection(this.name) } - async init (): Promise { - // May be used to create indices in Mongo - } - /** * Ensures indices in the collection or creates new if needed. * Drops all other indices that are not in the list. @@ -152,17 +148,6 @@ export class AccountMongoDbCollection extends MongoDbCollection impleme super('account', db) } - async init (): Promise { - const indicesToEnsure: MongoIndex[] = [ - { - key: { email: 1 }, - options: { unique: true, name: 'hc_account_email_1' } - } - ] - - await this.ensureIndices(indicesToEnsure) - } - convertToObj (acc: Account): Account { return { ...acc, @@ -193,29 +178,6 @@ export class WorkspaceMongoDbCollection extends MongoDbCollection imp super('workspace', db) } - async init (): Promise { - // await this.collection.createIndex({ workspace: 1 }, { unique: true }) - - const indicesToEnsure: MongoIndex[] = [ - { - key: { workspace: 1 }, - options: { - unique: true, - name: 'hc_account_workspace_1' - } - }, - { - key: { workspaceUrl: 1 }, - options: { - unique: true, - name: 'hc_account_workspaceUrl_1' - } - } - ] - - await this.ensureIndices(indicesToEnsure) - } - async countWorkspacesInRegion (region: string, upToVersion?: Data, visitedSince?: number): Promise { const regionQuery = region === '' ? { $or: [{ region: { $exists: false } }, { region: '' }] } : { region } const query: Filter['$and'] = [ @@ -350,12 +312,28 @@ export class MongoAccountDB implements AccountDB { } async init (): Promise { - await Promise.all([ - this.workspace.init(), - this.account.init(), - this.otp.init(), - this.invite.init(), - this.upgrade.init() + await this.account.ensureIndices([ + { + key: { email: 1 }, + options: { unique: true, name: 'hc_account_email_1' } + } + ]) + + await this.workspace.ensureIndices([ + { + key: { workspace: 1 }, + options: { + unique: true, + name: 'hc_account_workspace_1' + } + }, + { + key: { workspaceUrl: 1 }, + options: { + unique: true, + name: 'hc_account_workspaceUrl_1' + } + } ]) } diff --git a/server/account/src/collections/postgres.ts b/server/account/src/collections/postgres.ts index 04bbb88aa4..fbc406e59b 100644 --- a/server/account/src/collections/postgres.ts +++ b/server/account/src/collections/postgres.ts @@ -31,29 +31,12 @@ import type { UpgradeStatistic } from '../types' -export abstract class PostgresDbCollection> implements DbCollection { +export class PostgresDbCollection> implements DbCollection { constructor ( readonly name: string, readonly client: Pool ) {} - async exists (): Promise { - const tableInfo = await this.client.query( - ` - SELECT table_name - FROM information_schema.tables - WHERE table_name = $1 - `, - [this.name] - ) - - return (tableInfo.rowCount ?? 0) > 0 - } - - async init (): Promise { - // Create tables, indexes, etc. - } - protected buildSelectClause (): string { return `SELECT * FROM ${this.name}` } @@ -232,33 +215,6 @@ export class AccountPostgresDbCollection extends PostgresDbCollection i super('account', client) } - async init (): Promise { - if (await this.exists()) return - - await this.client.query( - `CREATE TABLE ${this.name} ( - _id VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - hash BYTEA, - salt BYTEA NOT NULL, - first VARCHAR(255) NOT NULL, - last VARCHAR(255) NOT NULL, - admin BOOLEAN, - confirmed BOOLEAN, - "lastWorkspace" BIGINT, - "createdOn" BIGINT NOT NULL, - "lastVisit" BIGINT, - "githubId" VARCHAR(100), - "openId" VARCHAR(100), - PRIMARY KEY(_id) - )` - ) - - await this.client.query(` - CREATE INDEX ${this.name}_email ON ${this.name} ("email") - `) - } - protected buildSelectClause (): string { return `SELECT _id, @@ -299,40 +255,6 @@ export class WorkspacePostgresDbCollection extends PostgresDbCollection { - if (await this.exists()) return - - await this.client.query( - `CREATE TABLE ${this.name} ( - _id VARCHAR(255) NOT NULL, - workspace VARCHAR(255) NOT NULL, - disabled BOOLEAN, - "versionMajor" SMALLINT NOT NULL, - "versionMinor" SMALLINT NOT NULL, - "versionPatch" SMALLINT NOT NULL, - branding VARCHAR(255), - "workspaceUrl" VARCHAR(255), - "workspaceName" VARCHAR(255), - "createdOn" BIGINT NOT NULL, - "lastVisit" BIGINT, - "createdBy" VARCHAR(255), - mode VARCHAR(60), - progress SMALLINT, - endpoint VARCHAR(255), - region VARCHAR(100), - "lastProcessingTime" BIGINT, - attempts SMALLINT, - message VARCHAR(1000), - "backupInfo" JSONB, - PRIMARY KEY(_id) - )` - ) - - await this.client.query(` - CREATE INDEX ${this.name}_workspace ON ${this.name} ("workspace") - `) - } - protected buildSelectClause (): string { return `SELECT _id, @@ -495,48 +417,11 @@ export class WorkspacePostgresDbCollection extends PostgresDbCollection implements DbCollection { - constructor (readonly client: Pool) { - super('otp', client) - } - - async init (): Promise { - if (await this.exists()) return - - await this.client.query( - `CREATE TABLE ${this.name} ( - account VARCHAR(255) NOT NULL REFERENCES account (_id), - otp VARCHAR(20) NOT NULL, - expires BIGINT NOT NULL, - "createdOn" BIGINT NOT NULL, - PRIMARY KEY(account, otp) - )` - ) - } -} - export class InvitePostgresDbCollection extends PostgresDbCollection implements DbCollection { constructor (readonly client: Pool) { super('invite', client) } - async init (): Promise { - if (await this.exists()) return - - await this.client.query( - `CREATE TABLE ${this.name} ( - _id VARCHAR(255) NOT NULL, - workspace VARCHAR(255) NOT NULL, - exp BIGINT NOT NULL, - "emailMask" VARCHAR(100), - "limit" SMALLINT, - role VARCHAR(40), - "personId" VARCHAR(255), - PRIMARY KEY(_id) - )` - ) - } - protected buildSelectClause (): string { return `SELECT _id, @@ -572,30 +457,6 @@ export class InvitePostgresDbCollection extends PostgresDbCollection imp } } -export class UpgradePostgresDbCollection - extends PostgresDbCollection - implements DbCollection { - constructor (readonly client: Pool) { - super('upgrade', client) - } - - async init (): Promise { - if (await this.exists()) return - - await this.client.query( - `CREATE TABLE ${this.name} ( - region VARCHAR(100) NOT NULL, - version VARCHAR(100) NOT NULL, - "startTime" BIGINT NOT NULL, - total INTEGER NOT NULL, - "toProcess" INTEGER NOT NULL, - "lastUpdate" BIGINT, - PRIMARY KEY(region, version) - )` - ) - } -} - export class PostgresAccountDB implements AccountDB { readonly wsAssignmentName = 'workspace_assignment' @@ -608,39 +469,43 @@ export class PostgresAccountDB implements AccountDB { constructor (readonly client: Pool) { this.workspace = new WorkspacePostgresDbCollection(client) this.account = new AccountPostgresDbCollection(client) - this.otp = new OtpPostgresDbCollection(client) + this.otp = new PostgresDbCollection('otp', client) this.invite = new InvitePostgresDbCollection(client) - this.upgrade = new UpgradePostgresDbCollection(client) + this.upgrade = new PostgresDbCollection('upgrade', client) } async init (): Promise { - await Promise.all([this.workspace.init(), this.account.init(), this.upgrade.init()]) - - await Promise.all([this.otp.init(), this.invite.init()]) - await this._init() + + // Apply all the migrations + for (const migration of this.getMigrations()) { + await this.migrate(migration[0], migration[1]) + } + } + + async migrate (name: string, ddl: string): Promise { + const res = await this.client.query( + 'INSERT INTO _account_applied_migrations (identifier, ddl) VALUES ($1, $2) ON CONFLICT DO NOTHING', + [name, ddl] + ) + + if (res.rowCount === 1) { + console.log(`Applying migration: ${name}`) + await this.client.query(ddl) + } else { + console.log(`Migration ${name} already applied`) + } } async _init (): Promise { - const tableInfo = await this.client.query( - ` - SELECT table_name - FROM information_schema.tables - WHERE table_name = $1 - `, - [this.wsAssignmentName] - ) - - if ((tableInfo.rowCount ?? 0) > 0) { - return - } - await this.client.query( - `CREATE TABLE ${this.wsAssignmentName} ( - workspace VARCHAR(255) NOT NULL REFERENCES workspace (_id), - account VARCHAR(255) NOT NULL REFERENCES account (_id), - PRIMARY KEY(workspace, account) - )` + ` + CREATE TABLE IF NOT EXISTS _account_applied_migrations ( + identifier VARCHAR(255) NOT NULL PRIMARY KEY + , ddl TEXT NOT NULL + , applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + ); + ` ) } @@ -659,4 +524,102 @@ export class PostgresAccountDB implements AccountDB { getObjectId (id: string): ObjectId { return id } + + protected getMigrations (): [string, string][] { + return [this.getV1Migration()] + } + + // NOTE: NEVER MODIFY EXISTING MIGRATIONS. IF YOU NEED TO ADJUST THE SCHEMA, ADD A NEW MIGRATION. + private getV1Migration (): [string, string] { + return [ + 'account_db_v1_init', + ` + /* ======= ACCOUNT ======= */ + CREATE TABLE IF NOT EXISTS account ( + _id VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + hash BYTEA, + salt BYTEA NOT NULL, + first VARCHAR(255) NOT NULL, + last VARCHAR(255) NOT NULL, + admin BOOLEAN, + confirmed BOOLEAN, + "lastWorkspace" BIGINT, + "createdOn" BIGINT NOT NULL, + "lastVisit" BIGINT, + "githubId" VARCHAR(100), + "openId" VARCHAR(100), + PRIMARY KEY(_id) + ); + + CREATE INDEX IF NOT EXISTS account_email ON account ("email"); + + /* ======= WORKSPACE ======= */ + CREATE TABLE IF NOT EXISTS workspace ( + _id VARCHAR(255) NOT NULL, + workspace VARCHAR(255) NOT NULL, + disabled BOOLEAN, + "versionMajor" SMALLINT NOT NULL, + "versionMinor" SMALLINT NOT NULL, + "versionPatch" SMALLINT NOT NULL, + branding VARCHAR(255), + "workspaceUrl" VARCHAR(255), + "workspaceName" VARCHAR(255), + "createdOn" BIGINT NOT NULL, + "lastVisit" BIGINT, + "createdBy" VARCHAR(255), + mode VARCHAR(60), + progress SMALLINT, + endpoint VARCHAR(255), + region VARCHAR(100), + "lastProcessingTime" BIGINT, + attempts SMALLINT, + message VARCHAR(1000), + "backupInfo" JSONB, + PRIMARY KEY(_id) + ); + + CREATE INDEX IF NOT EXISTS workspace_workspace ON workspace ("workspace"); + + /* ======= OTP ======= */ + CREATE TABLE IF NOT EXISTS otp ( + account VARCHAR(255) NOT NULL REFERENCES account (_id), + otp VARCHAR(20) NOT NULL, + expires BIGINT NOT NULL, + "createdOn" BIGINT NOT NULL, + PRIMARY KEY(account, otp) + ); + + /* ======= INVITE ======= */ + CREATE TABLE IF NOT EXISTS invite ( + _id VARCHAR(255) NOT NULL, + workspace VARCHAR(255) NOT NULL, + exp BIGINT NOT NULL, + "emailMask" VARCHAR(100), + "limit" SMALLINT, + role VARCHAR(40), + "personId" VARCHAR(255), + PRIMARY KEY(_id) + ); + + /* ======= UPGRADE ======= */ + CREATE TABLE IF NOT EXISTS upgrade ( + region VARCHAR(100) NOT NULL, + version VARCHAR(100) NOT NULL, + "startTime" BIGINT NOT NULL, + total INTEGER NOT NULL, + "toProcess" INTEGER NOT NULL, + "lastUpdate" BIGINT, + PRIMARY KEY(region, version) + ); + + /* ======= SUPPLEMENTARY ======= */ + CREATE TABLE IF NOT EXISTS ${this.wsAssignmentName} ( + workspace VARCHAR(255) NOT NULL REFERENCES workspace (_id), + account VARCHAR(255) NOT NULL REFERENCES account (_id), + PRIMARY KEY(workspace, account) + ); + ` + ] + } } diff --git a/server/account/src/types.ts b/server/account/src/types.ts index 272465917e..9338ec0763 100644 --- a/server/account/src/types.ts +++ b/server/account/src/types.ts @@ -169,7 +169,6 @@ export type Operations = Partial & { export interface DbCollection> { name: string - init: () => Promise find: (query: Query, sort?: { [P in keyof T]?: 'ascending' | 'descending' }, limit?: number) => Promise findOne: (query: Query) => Promise insertOne: (data: Partial, idKey?: K) => Promise