mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-14 04:08:19 +00:00
UBERF-8426: Controlled account db migration (#6885)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
dbbd653e09
commit
5d7d347c4d
@ -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'
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
);
|
||||
`
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user