Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { pick } from 'lodash-es';
import { DataSource } from 'typeorm';
import {
AMALIA_VIEW_KEY,
Company,
DataConnectionName,
DefaultConnectionName,
getAmaliaViewSpecs,
} from '@amalia/core/models';
import { StringUtils } from '@amalia/ext/string';
import { isDuplicateObjectError, isDuplicateTableError } from '@amalia/ext/typeorm';
import { assert } from '@amalia/ext/typescript';
import { Registry } from '@amalia/kernel/nest-registry';
import { dataEnums, partitionsConfig } from './partitions.config';
export const FORBIDDEN_SLUGS = ['public'];
@Injectable()
export class DatabaseSetupService {
private readonly logger = new Logger(DatabaseSetupService.name);
public constructor(
@InjectDataSource(DefaultConnectionName)
private readonly defaultDataSource: DataSource,
@InjectDataSource(DataConnectionName)
private readonly dataDataSource: DataSource,
private readonly registry: Registry,
) {}
/**
* Create the master tables on the data database.
*
* Useful once for setup, bootstrapping a new environment or after adding new tables.
*/
public async createDataMasterTables() {
this.logger.log('Recreating master tables');
await this.createEnums();
for (const [tableName, tableConfig] of Object.entries(partitionsConfig)) {
try {
await this.dataDataSource.query(tableConfig.createMasterTableSql(tableName));
this.logger.log(`Recreated ${tableName}`);
} catch (err) {
if (isDuplicateTableError(err)) {
this.logger.log(`Table ${tableName} already exists, skipping.`);
} else {
throw err;
}
}
}
this.logger.log('Done recreating master tables');
}
private async createEnums() {
for (const [enumName, enumTerms] of Object.entries(dataEnums)) {
try {
await this.dataDataSource.query(
`CREATE TYPE "public".${enumName} AS ENUM (${Object.values(enumTerms)
.map((t) => `'${t}'`)
.join(', ')})`,
);
} catch (err) {
if (isDuplicateObjectError(err)) {
this.logger.log(`Enum ${enumName} already created, skipping.`);
} else {
throw err;
}
}
}
}
/**
* For each company in the default database, create a schema, partitions and views.
*
* Useful for setup, bootstrapping a new environment, or to sync a data database
* after loading a dump from the production environment.
*/
public async recreateSchemaAndPartitions() {
const companies = await this.defaultDataSource.getRepository(Company).find({ order: { name: 'ASC' } });
for (const company of companies) {
await this.setupDataDbForCompany(company);
}
}
/**
* Set up schema and partitions for the new company.
* @param company
*/
public async setupDataDbForCompany(company: Company) {
await this.setupSchemaForCompany(company);
await this.setupPartitionsForCompany(company);
await this.setupViewsForCompany(company);
}
private async setupSchemaForCompany(company: Company) {
assert(company.slug, 'Slug invalid');
assert(company.slug !== 'public', 'Slug invalid');
// Table creation doesn't accept parameters, so we have to check that the slug is
// not an SQL injection.
assert(StringUtils.isSlug(company.slug), 'Slug invalid');
await this.dataDataSource.query(`CREATE SCHEMA IF NOT EXISTS "${company.slug}";`);
this.logger.log({ message: 'Created data schema', company: pick(company, ['id', 'name']) });
}
private async setupPartitionsForCompany(company: Company) {
for (const tableName of Object.keys(partitionsConfig)) {
await this.dataDataSource.query(`
CREATE TABLE IF NOT EXISTS "${company.slug}".${tableName} PARTITION OF public.${tableName} FOR VALUES IN ('${company.id}');
`);
}
this.logger.log({ message: 'Created partitions', company: pick(company, ['id', 'name']) });
}
private async setupViewsForCompany(company: Company) {
const viewsToCreate = this.discoverViews();
await Promise.all(
viewsToCreate.map(async ({ name, ddl }) => {
// We could have used CREATE VIEW OR REPLACE, but it's not working
// when we add new columns, since it tries to alter its definition,
// and it's sometimes not possible. We'd rather delete all views
// and recreate them, so we're sure it's working.
await this.dataDataSource.query(`DROP VIEW IF EXISTS "${company.slug}"."${name}"`);
await this.dataDataSource.query(`CREATE VIEW "${company.slug}"."${name}" AS ${ddl(company.id)}`);
}),
);
this.logger.log({ message: 'Created views', company: pick(company, ['id', 'name']) });
}
private discoverViews() {
const viewsToCreate = this.registry.getProviders<unknown>(AMALIA_VIEW_KEY);
return viewsToCreate.map((view) => getAmaliaViewSpecs(view));
}
public async deleteDataDbCompanySchema(deletedCompany: Company) {
await this.dataDataSource.query(`DROP SCHEMA "${deletedCompany.slug}" CASCADE;`);
this.logger.log({ message: 'Deleted data schema', company: pick(deletedCompany, ['id', 'name']) });
}
}
|