UBERF-8426: Controlled account db migration (#6885)

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev 2024-10-11 19:40:41 +04:00 committed by GitHub
parent dbbd653e09
commit 5d7d347c4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 149 additions and 209 deletions

View File

@ -48,10 +48,6 @@ export class MongoDbCollection<T extends Record<string, any>> implements DbColle
return this.db.collection<T>(this.name)
}
async init (): Promise<void> {
// 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<Account> impleme
super('account', db)
}
async init (): Promise<void> {
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<Workspace> imp
super('workspace', db)
}
async init (): Promise<void> {
// 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<Version>, visitedSince?: number): Promise<number> {
const regionQuery = region === '' ? { $or: [{ region: { $exists: false } }, { region: '' }] } : { region }
const query: Filter<Workspace>['$and'] = [
@ -350,12 +312,28 @@ export class MongoAccountDB implements AccountDB {
}
async init (): Promise<void> {
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'
}
}
])
}

View File

@ -31,29 +31,12 @@ import type {
UpgradeStatistic
} from '../types'
export abstract class PostgresDbCollection<T extends Record<string, any>> implements DbCollection<T> {
export class PostgresDbCollection<T extends Record<string, any>> implements DbCollection<T> {
constructor (
readonly name: string,
readonly client: Pool
) {}
async exists (): Promise<boolean> {
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<void> {
// Create tables, indexes, etc.
}
protected buildSelectClause (): string {
return `SELECT * FROM ${this.name}`
}
@ -232,33 +215,6 @@ export class AccountPostgresDbCollection extends PostgresDbCollection<Account> i
super('account', client)
}
async init (): Promise<void> {
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<Workspac
super('workspace', client)
}
async init (): Promise<void> {
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<Workspac
}
}
export class OtpPostgresDbCollection extends PostgresDbCollection<OtpRecord> implements DbCollection<OtpRecord> {
constructor (readonly client: Pool) {
super('otp', client)
}
async init (): Promise<void> {
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<Invite> implements DbCollection<Invite> {
constructor (readonly client: Pool) {
super('invite', client)
}
async init (): Promise<void> {
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<Invite> imp
}
}
export class UpgradePostgresDbCollection
extends PostgresDbCollection<UpgradeStatistic>
implements DbCollection<UpgradeStatistic> {
constructor (readonly client: Pool) {
super('upgrade', client)
}
async init (): Promise<void> {
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<OtpRecord>('otp', client)
this.invite = new InvitePostgresDbCollection(client)
this.upgrade = new UpgradePostgresDbCollection(client)
this.upgrade = new PostgresDbCollection<UpgradeStatistic>('upgrade', client)
}
async init (): Promise<void> {
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<void> {
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<void> {
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)
);
`
]
}
}

View File

@ -169,7 +169,6 @@ export type Operations<T> = Partial<T> & {
export interface DbCollection<T extends Record<string, any>> {
name: string
init: () => Promise<void>
find: (query: Query<T>, sort?: { [P in keyof T]?: 'ascending' | 'descending' }, limit?: number) => Promise<T[]>
findOne: (query: Query<T>) => Promise<T | null>
insertOne: <K extends keyof T>(data: Partial<T>, idKey?: K) => Promise<any>